|
1 # copyright 2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
2 # contact http://www.logilab.fr -- mailto:contact@logilab.fr |
|
3 # |
|
4 # This program is free software: you can redistribute it and/or modify it under |
|
5 # the terms of the GNU Lesser General Public License as published by the Free |
|
6 # Software Foundation, either version 2.1 of the License, or (at your option) |
|
7 # any later version. |
|
8 # |
|
9 # This program is distributed in the hope that it will be useful, but WITHOUT |
|
10 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|
11 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
|
12 # details. |
|
13 # |
|
14 # You should have received a copy of the GNU Lesser General Public License along |
|
15 # with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 """Instrumentation utilities""" |
|
17 |
|
18 import os |
|
19 |
|
20 try: |
|
21 import pygraphviz |
|
22 except ImportError: |
|
23 pygraphviz = None |
|
24 |
|
25 from cubicweb.cwvreg import CWRegistryStore |
|
26 from cubicweb.devtools.devctl import DevConfiguration |
|
27 |
|
28 |
|
29 ALL_COLORS = [ |
|
30 "00FF00", "0000FF", "FFFF00", "FF00FF", "00FFFF", "000000", |
|
31 "800000", "008000", "000080", "808000", "800080", "008080", "808080", |
|
32 "C00000", "00C000", "0000C0", "C0C000", "C000C0", "00C0C0", "C0C0C0", |
|
33 "400000", "004000", "000040", "404000", "400040", "004040", "404040", |
|
34 "200000", "002000", "000020", "202000", "200020", "002020", "202020", |
|
35 "600000", "006000", "000060", "606000", "600060", "006060", "606060", |
|
36 "A00000", "00A000", "0000A0", "A0A000", "A000A0", "00A0A0", "A0A0A0", |
|
37 "E00000", "00E000", "0000E0", "E0E000", "E000E0", "00E0E0", "E0E0E0", |
|
38 ] |
|
39 _COLORS = {} |
|
40 def get_color(key): |
|
41 try: |
|
42 return _COLORS[key] |
|
43 except KeyError: |
|
44 _COLORS[key] = '#'+ALL_COLORS[len(_COLORS) % len(ALL_COLORS)] |
|
45 return _COLORS[key] |
|
46 |
|
47 def warn(msg, *args): |
|
48 print 'WARNING: %s' % (msg % args) |
|
49 |
|
50 def info(msg): |
|
51 print 'INFO: ' + msg |
|
52 |
|
53 |
|
54 class PropagationAnalyzer(object): |
|
55 """Abstract propagation analyzer, providing utility function to extract |
|
56 entities involved in propagation from a schema, as well as propagation |
|
57 rules from hooks (provided they use intrumentalized sets, see |
|
58 :class:`CubeTracerSet`). |
|
59 |
|
60 Concrete classes should at least define `prop_rel` class attribute and |
|
61 implements the `is_root` method. |
|
62 |
|
63 See `localperms` or `nosylist` cubes for example usage (`ccplugin` module). |
|
64 """ |
|
65 prop_rel = None # name of the propagation relation |
|
66 |
|
67 def init(self, cube): |
|
68 """Initialize analyze for the given cube, returning the (already loaded) |
|
69 vregistry and a set of entities which we're interested in. |
|
70 """ |
|
71 config = DevConfiguration(cube) |
|
72 schema = config.load_schema() |
|
73 vreg = CWRegistryStore(config) |
|
74 vreg.set_schema(schema) # set_schema triggers objects registrations |
|
75 eschemas = set(eschema for eschema in schema.entities() |
|
76 if self.should_include(eschema)) |
|
77 return vreg, eschemas |
|
78 |
|
79 def is_root(self, eschema): |
|
80 """Return `True` if given entity schema is a root of the graph""" |
|
81 raise NotImplementedError() |
|
82 |
|
83 def should_include(self, eschema): |
|
84 """Return `True` if given entity schema should be included by the graph. |
|
85 """ |
|
86 |
|
87 if self.prop_rel in eschema.subjrels or self.is_root(eschema): |
|
88 return True |
|
89 return False |
|
90 |
|
91 def prop_edges(self, s_rels, o_rels, eschemas): |
|
92 """Return a set of edges where propagation has been detected. |
|
93 |
|
94 Each edge is defined by a 4-uple (from node, to node, rtype, package) |
|
95 where `rtype` is the relation type bringing from <from node> to <to |
|
96 node> and `package` is the cube adding the rule to the propagation |
|
97 control set (see see :class:`CubeTracerSet`). |
|
98 """ |
|
99 schema = iter(eschemas).next().schema |
|
100 prop_edges = set() |
|
101 for rtype in s_rels: |
|
102 found = False |
|
103 for subj, obj in schema.rschema(rtype).rdefs: |
|
104 if subj in eschemas and obj in eschemas: |
|
105 found = True |
|
106 prop_edges.add( (subj, obj, rtype, s_rels.value_cube[rtype]) ) |
|
107 if not found: |
|
108 warn('no rdef match for %s', rtype) |
|
109 for rtype in o_rels: |
|
110 found = False |
|
111 for subj, obj in schema.rschema(rtype).rdefs: |
|
112 if subj in eschemas and obj in eschemas: |
|
113 found = True |
|
114 prop_edges.add( (obj, subj, rtype, o_rels.value_cube[rtype]) ) |
|
115 if not found: |
|
116 warn('no rdef match for %s', rtype) |
|
117 return prop_edges |
|
118 |
|
119 def detect_problems(self, eschemas, edges): |
|
120 """Given the set of analyzed entity schemas and edges between them, |
|
121 return a set of entity schemas where a problem has been detected. |
|
122 """ |
|
123 problematic = set() |
|
124 for eschema in eschemas: |
|
125 if self.has_problem(eschema, edges): |
|
126 problematic.add(eschema) |
|
127 not_problematic = set(eschemas).difference(problematic) |
|
128 if not_problematic: |
|
129 info('nothing problematic in: %s' % |
|
130 ', '.join(e.type for e in not_problematic)) |
|
131 return problematic |
|
132 |
|
133 def has_problem(self, eschema, edges): |
|
134 """Return `True` if the given schema is considered problematic, |
|
135 considering base propagation rules. |
|
136 """ |
|
137 root = self.is_root(eschema) |
|
138 has_prop_rel = self.prop_rel in eschema.subjrels |
|
139 # root but no propagation relation |
|
140 if root and not has_prop_rel: |
|
141 warn('%s is root but miss %s', eschema, self.prop_rel) |
|
142 return True |
|
143 # propagated but without propagation relation / not propagated but |
|
144 # with propagation relation |
|
145 if not has_prop_rel and \ |
|
146 any(edge for edge in edges if edge[1] == eschema): |
|
147 warn("%s miss %s but is reached by propagation", |
|
148 eschema, self.prop_rel) |
|
149 return True |
|
150 elif has_prop_rel and not root: |
|
151 rdef = eschema.rdef(self.prop_rel, takefirst=True) |
|
152 edges = [edge for edge in edges if edge[1] == eschema] |
|
153 if not edges: |
|
154 warn("%s has %s but isn't reached by " |
|
155 "propagation", eschema, self.prop_rel) |
|
156 return True |
|
157 # require_permission relation / propagation rule not added by |
|
158 # the same cube |
|
159 elif not any(edge for edge in edges if edge[-1] == rdef.package): |
|
160 warn('%s has %s relation / propagation rule' |
|
161 ' not added by the same cube (%s / %s)', eschema, |
|
162 self.prop_rel, rdef.package, edges[0][-1]) |
|
163 return True |
|
164 return False |
|
165 |
|
166 def init_graph(self, eschemas, edges, problematic): |
|
167 """Initialize and return graph, adding given nodes (entity schemas) and |
|
168 edges between them. |
|
169 |
|
170 Require pygraphviz installed. |
|
171 """ |
|
172 if pygraphviz is None: |
|
173 raise RuntimeError('pygraphviz is not installed') |
|
174 graph = pygraphviz.AGraph(strict=False, directed=True) |
|
175 for eschema in eschemas: |
|
176 if eschema in problematic: |
|
177 params = {'color': '#ff0000', 'fontcolor': '#ff0000'} |
|
178 else: |
|
179 params = {}#'color': get_color(eschema.package)} |
|
180 graph.add_node(eschema.type, **params) |
|
181 for subj, obj, rtype, package in edges: |
|
182 graph.add_edge(str(subj), str(obj), label=rtype, |
|
183 color=get_color(package)) |
|
184 return graph |
|
185 |
|
186 def add_colors_legend(self, graph): |
|
187 """Add a legend of used colors to the graph.""" |
|
188 for package, color in sorted(_COLORS.iteritems()): |
|
189 graph.add_node(package, color=color, fontcolor=color, shape='record') |
|
190 |
|
191 |
|
192 class CubeTracerSet(object): |
|
193 """Dumb set implementation whose purpose is to keep track of which cube is |
|
194 being loaded when something is added to the set. |
|
195 |
|
196 Results will be found in the `value_cube` attribute dictionary. |
|
197 |
|
198 See `localperms` or `nosylist` cubes for example usage (`hooks` module). |
|
199 """ |
|
200 def __init__(self, vreg, wrapped): |
|
201 self.vreg = vreg |
|
202 self.wrapped = wrapped |
|
203 self.value_cube = {} |
|
204 |
|
205 def add(self, value): |
|
206 self.wrapped.add(value) |
|
207 cube = self.vreg.currently_loading_cube |
|
208 if value in self.value_cube: |
|
209 warn('%s is propagated by cube %s and cube %s', |
|
210 value, self.value_cube[value], cube) |
|
211 else: |
|
212 self.value_cube[value] = cube |
|
213 |
|
214 def __iter__(self): |
|
215 return iter(self.wrapped) |
|
216 |
|
217 def __ior__(self, other): |
|
218 for value in other: |
|
219 self.add(value) |
|
220 return self |
|
221 |
|
222 def __ror__(self, other): |
|
223 other |= self.wrapped |
|
224 return other |