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