Merge lp:~bcsaller/pyjuju/ensemble-status into lp:pyjuju
- ensemble-status
- Merge into trunk
Status: | Rejected |
---|---|
Rejected by: | Gustavo Niemeyer |
Proposed branch: | lp:~bcsaller/pyjuju/ensemble-status |
Merge into: | lp:pyjuju |
Diff against target: |
1394 lines (+1158/-0) (has conflicts) 13 files modified
.bzrignore (+1/-0) ensemble/control/__init__.py (+9/-0) ensemble/control/status.py (+496/-0) ensemble/control/tests/test_status.py (+275/-0) ensemble/providers/dummy.py (+5/-0) ensemble/state/base.py (+50/-0) ensemble/state/machine.py (+32/-0) ensemble/state/relation.py (+38/-0) ensemble/state/service.py (+108/-0) ensemble/state/tests/test_machine.py (+50/-0) ensemble/state/tests/test_relation.py (+7/-0) ensemble/state/tests/test_service.py (+81/-0) ensemble/state/topology.py (+6/-0) Text conflict in ensemble/control/__init__.py Text conflict in ensemble/providers/dummy.py Text conflict in ensemble/state/relation.py Text conflict in ensemble/state/service.py Text conflict in ensemble/state/tests/test_service.py |
To merge this branch: | bzr merge lp:~bcsaller/pyjuju/ensemble-status |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gustavo Niemeyer | Needs Fixing | ||
Benjamin Saller (community) | Needs Resubmitting | ||
Review via email: mp+45897@code.launchpad.net |
Commit message
Description of the change
This is being pushed for a sanity check. I think its generally fine, but lacks the addition of machine instance/dns level data that makes using the status information useful. Feedback welcome.
Gustavo Niemeyer (niemeyer) wrote : | # |
Btw, in terms of poking at internals, this might be useful feedback:
http://
http://
- 118. By Benjamin Saller
-
cleaned up dot rendering with clusters, added more complex topography
- 119. By Benjamin Saller
-
changes to support iteration over state managers
- 120. By Benjamin Saller
-
checkpoint status work
still issues with machine assignment and relation endpoint reporting
- 121. By Benjamin Saller
-
Basic Dot rendering vs higher level api
- 122. By Benjamin Saller
-
Merged state-changes into status-api.
- 123. By Benjamin Saller
-
more robust testing topology
- 124. By Benjamin Saller
-
moving some inspection methods into the state managers
removing non-passing tests to show current state of pipeline
Benjamin Saller (bcsaller) wrote : | # |
This stage represents a number of changes from the previous phase.
The higher level state API is used and InternalTopology isn't touched. The tests create a topology through the higher level API as well.
A number of changes will be forthcoming. I expect to simplify the collect phase of the status command by making the state objects themselves able to gather information. I intend to change the rendering phase to also take the environment so that things like environment name can be included in the output.
The reason for wanting to change the collection phase mainly deals with the need to filter which elements make it into the result set based on which command line arguments are passed.
The current filtering model is a bit ugly for doing things like
#ensemble status memcache
where this might be a service
#ensemble status wordpress/1
and this is a unit.
Gustavo Niemeyer (niemeyer) wrote : | # |
Thanks Ben, this implementation is going into a much better direction,
and doesn't feel too far from getting in.
Please address the following issues, without tweaking the branch further,
so that we can get this merged. You can follow with further changes in
a separate branch.
[1]
+ def get_services(self):
+ """Get all the services associated with a given relation.
+ """
s/get_services/
method immediately above). We could call it get_service_states, but the
_all will make it more distinguishable when reading the code.
These new methods feel good.
[2]
+ for service in topology.
+ self.internal_
+ service_name = topology.
Stylistic minor: the indentation after the for confuses the
line continuation with the block content. An extra indent
in will avoid this.
I'm actually wondering if we should stop using the 80 columns rule.
I've been using larger terminals and am pretty happy with the
readability benefits so far. Let's talk about this in our standup.
[3]
+ service_manager = ServiceStateMan
Creating these objects internally under each method is starting to
feel wrong. Don't worry about this in your branch, but let's add
that as a topic for discussion in the near future.
[4]
+ def get_endpoints(
+ """Return a list of RelationEndpoints associated with this relation."""
+ from ensemble.
+ service_manager = ServiceStateMan
+ endpoints = yield service_
If the method is called get_relation_
like a good idea to call it get_relation_
Again, these helpers feel good. Just the fact we're creating managers everywhere
feels like a bad idea, but again, not a problem to be handled in your branch.
[5]
+ @inlineCallbacks
+ def get_machines(self):
+ topology = yield self._read_
+ machines = []
+ for machine_id in topology.
+ # ugh, string manipulation
+ machine_id = int(machine_
s/get_machines/
Also, there's an _internal_
Since that kind of issue is recurring a bit, a general advice is to observe
the surroundings of what is being touched to see how problems are currently
solved. This is valid for algorithms, code style, method names, etc.
Consistency is very important to make a large code base tolerable.
[6]
@inlineCal
+ def get_units(self):
+ """Return a list of ServiceUnitState instances associated with
s/get_units/
[7]
+ def as_dict(self):
+ instance_id = yield self.get_
Those serialization methods should be inside the status command implementation
itself. It doesn't feel like they're useful anywhere else, and the content of
these dictionaries is tightly related to the usage inside the command.
You ca...
Kapil Thangavelu (hazmat) wrote : | # |
Looks very nice.
[1]
pep8isms
control/status.py
line 81: help="An optional filename to output the result to",
line over 80
line 124: EnvironmentsCon
line 233: two blank lines before func
line 354: extra blank line at the end of the file
[2]
renderer = getattr(globals(), "render_%s" % options.format, render_json)
I'm not a big fan of grabbing things of globals, it feels a bit
implicit. It would be nice to toss the renderers into a class,
dictionary, or instance as namespace, and then just use getattr
against the namespace.
[3]
def encode_state(o):
Its not entirely clear what this is doing, or why it would get a
deferred instead of an object. it would be nice to have doc strings,
and in general its better to spell out variable names, then use
abbreviations.
[4]
import pydot
python-pydot probably should get added to the debian/control as dep, or at least a suggestion.
[5]
machine.py line 81: machine_id = int(machine_
there's an _internal_id_to_id function in the same module that should be used for this.
[6]
relation.py line 264: get_services
it doesn't feel right to have a method to return all the services
associated to a servicerelation
itself as well. The relation stuff feels a bit unwieldy in this
respect, but it would be nice to have this sort of method on the
relation state manager. i'm not entirely sure what that should like
though.
[7] unrelated and for the future enhancement of status in a separate
branch, but i just added code to expose at least the machine provider
dns names on the provider machines, as well an accessor api to get
them individual (in addition to the existing bulk api).
- 125. By Benjamin Saller
-
pass environment to render methods, used in dot to title graph
- 126. By Benjamin Saller
-
uniform api for state object iteration for services and machines.
- 127. By Benjamin Saller
-
use _internal_id_to_id for machine id conversion
tests for get_all_unit_states
more method pattern renames - 128. By Benjamin Saller
-
ignore pdb history file
- 129. By Benjamin Saller
-
test set membership functions
- 130. By Benjamin Saller
-
remove unused method
- 131. By Benjamin Saller
-
relation iteration
- 132. By Benjamin Saller
-
checkpoint node based mark/sweep status code
- 133. By Benjamin Saller
-
nodes work with nested paths and mark/sweep algorithm for filtering, checkpoint before changing the status state construction to use Nodes
Benjamin Saller (bcsaller) wrote : | # |
> [1]
>
> + def get_services(self):
> + """Get all the services associated with a given relation.
> + """
>
> s/get_services/
> method immediately above). We could call it get_service_states, but the
> _all will make it more distinguishable when reading the code.
>
> These new methods feel good.
>
Renamed many methods in this style.
> [2]
>
> + for service in topology.
> + self.internal_
> + service_name = topology.
>
> Stylistic minor: the indentation after the for confuses the
> line continuation with the block content. An extra indent
> in will avoid this.
>
> I'm actually wondering if we should stop using the 80 columns rule.
> I've been using larger terminals and am pretty happy with the
> readability benefits so far. Let's talk about this in our standup.
>
Many developers consider this important from a sense of tradition I
think. I think that setting a guideline is a good idea but the age of
small fixed terminals ended a long time ago.
For pep8 adding
--ignore E501
(or adding E501) to the ignore comma delimited list will mask that error.
> [3]
>
> + service_manager = ServiceStateMan
>
> Creating these objects internally under each method is starting to
> feel wrong. Don't worry about this in your branch, but let's add
> that as a topic for discussion in the near future.
>
agreed, maybe a facade over the whole thing? something that can
translate from elements in the conceptual graph to other connected
objects. it could hold reference to all the managers. Our tests
usually need to keep handles to more than one state manager to do
anything useful so this might make sense moving forward.
> [4]
>
> + def get_endpoints(
> + """Return a list of RelationEndpoints associated with this relation."""
> + from ensemble.
> + service_manager = ServiceStateMan
> + endpoints = yield service_
>
> If the method is called get_relation_
> like a good idea to call it get_relation_
>
> Again, these helpers feel good. Just the fact we're creating managers everywhere
> feels like a bad idea, but again, not a problem to be handled in your branch.
>
changed and test added. Though in this case its arguable that
'_relation_' is implied given 'self'.
> [5]
>
> + @inlineCallbacks
> + def get_machines(self):
> + topology = yield self._read_
> + machines = []
> + for machine_id in topology.
> + # ugh, string manipulation
> + machine_id = int(machine_
>
> s/get_machines/
>
> Also, there's an _internal_
>
> Since that kind of issue is recurring a bit, a general advice is to observe
> the surroundings of what is being touched to see how problems are currently
> solved. This is valid for algor...
- 134. By Benjamin Saller
-
removed deprecated as_dict methods from state objects
- 135. By Benjamin Saller
-
json rendering works for the pure data state
removed yaml output, doesn't make sense in this context
state diagrams 95% back to rendering on the correct data, next patch should fix the relationships (again)
prune some dead code
Benjamin Saller (bcsaller) wrote : | # |
On Wed, Jan 26, 2011 at 9:23 PM, Kapil Thangavelu
<email address hidden> wrote:
>
> Looks very nice.
>
> [1]
> pep8isms
> control/status.py
>
> line 81: help="An optional filename to output the result to",
> line over 80
>
> line 124: EnvironmentsCon
>
> line 233: two blank lines before func
>
> line 354: extra blank line at the end of the file
>
>
Fixed 81/124
> [2]
> renderer = getattr(globals(), "render_%s" % options.format, render_json)
>
> I'm not a big fan of grabbing things of globals, it feels a bit
> implicit. It would be nice to toss the renderers into a class,
> dictionary, or instance as namespace, and then just use getattr
> against the namespace.
>
added registry
> [3]
> def encode_state(o):
>
> Its not entirely clear what this is doing, or why it would get a
> deferred instead of an object. it would be nice to have doc strings,
> and in general its better to spell out variable names, then use
> abbreviations.
>
removed
> [4]
> import pydot
>
> python-pydot probably should get added to the debian/control as dep, or at least a suggestion.
added as optional
>
> [5]
>
> machine.py line 81: machine_id = int(machine_
> there's an _internal_id_to_id function in the same module that should be used for this.
>
corrected
>
> [6]
> relation.py line 264: get_services
>
> it doesn't feel right to have a method to return all the services
> associated to a servicerelation
> itself as well. The relation stuff feels a bit unwieldy in this
> respect, but it would be nice to have this sort of method on the
> relation state manager. i'm not entirely sure what that should like
> though.
>
yeah, this is still an issue where I am with the branch now too (even
though the underlying code has changed a bit), my next step might be
to refactor this more.
> [7] unrelated and for the future enhancement of status in a separate
> branch, but i just added code to expose at least the machine provider
> dns names on the provider machines, as well an accessor api to get
> them individual (in addition to the existing bulk api).
I'll need this, great.
> --
> https:/
> You are the owner of lp:~bcsaller/ensemble/ensemble-status.
>
Gustavo Niemeyer (niemeyer) wrote : | # |
The branch is still at revision 124, from the 24th. Can you please push the changes?
- 136. By Benjamin Saller
-
remove yaml renderer, added renderers registry
Gustavo Niemeyer (niemeyer) wrote : | # |
As discussed over the phone, this will be split in multiple smaller branches.
Unmerged revisions
- 136. By Benjamin Saller
-
remove yaml renderer, added renderers registry
- 135. By Benjamin Saller
-
json rendering works for the pure data state
removed yaml output, doesn't make sense in this context
state diagrams 95% back to rendering on the correct data, next patch should fix the relationships (again)
prune some dead code - 134. By Benjamin Saller
-
removed deprecated as_dict methods from state objects
- 133. By Benjamin Saller
-
nodes work with nested paths and mark/sweep algorithm for filtering, checkpoint before changing the status state construction to use Nodes
- 132. By Benjamin Saller
-
checkpoint node based mark/sweep status code
- 131. By Benjamin Saller
-
relation iteration
- 130. By Benjamin Saller
-
remove unused method
- 129. By Benjamin Saller
-
test set membership functions
- 128. By Benjamin Saller
-
ignore pdb history file
- 127. By Benjamin Saller
-
use _internal_id_to_id for machine id conversion
tests for get_all_unit_states
more method pattern renames
Preview Diff
1 | === modified file '.bzrignore' |
2 | --- .bzrignore 2010-11-18 05:29:38 +0000 |
3 | +++ .bzrignore 2011-02-01 15:32:39 +0000 |
4 | @@ -2,3 +2,4 @@ |
5 | /_trial_temp |
6 | /tags |
7 | zookeeper.log |
8 | +.pdb-pyhist |
9 | |
10 | === modified file 'ensemble/control/__init__.py' |
11 | --- ensemble/control/__init__.py 2011-01-31 22:58:29 +0000 |
12 | +++ ensemble/control/__init__.py 2011-02-01 15:32:39 +0000 |
13 | @@ -11,7 +11,11 @@ |
14 | import destroy_service |
15 | import remove_relation |
16 | import shutdown |
17 | +<<<<<<< TREE |
18 | import ssh |
19 | +======= |
20 | +import status |
21 | +>>>>>>> MERGE-SOURCE |
22 | |
23 | import initialize |
24 | |
25 | @@ -19,10 +23,15 @@ |
26 | add_relation, |
27 | deploy, |
28 | bootstrap, |
29 | +<<<<<<< TREE |
30 | destroy_service, |
31 | remove_relation, |
32 | ssh, |
33 | shutdown] |
34 | +======= |
35 | + shutdown, |
36 | + status] |
37 | +>>>>>>> MERGE-SOURCE |
38 | |
39 | |
40 | ADMIN_SUBCOMMANDS = [ |
41 | |
42 | === added file 'ensemble/control/status.py' |
43 | --- ensemble/control/status.py 1970-01-01 00:00:00 +0000 |
44 | +++ ensemble/control/status.py 2011-02-01 15:32:39 +0000 |
45 | @@ -0,0 +1,496 @@ |
46 | +import argparse |
47 | +import json |
48 | +import fnmatch |
49 | +import sys |
50 | + |
51 | +from twisted.internet.defer import (Deferred, |
52 | + inlineCallbacks, |
53 | + returnValue) |
54 | + |
55 | +from ensemble.environment.errors import EnvironmentsConfigError |
56 | +from ensemble.state.service import ServiceStateManager |
57 | +from ensemble.state.machine import MachineStateManager |
58 | +from ensemble.state.relation import RelationStateManager |
59 | + |
60 | +_marker = object() |
61 | + |
62 | +renderers = {} |
63 | + |
64 | +# suppliment information passed to |
65 | +# the DOT methods, expands as kwargs for the |
66 | +# indicated data type |
67 | +DEFAULT_STYLE = { |
68 | + "service_container": { |
69 | + "bgcolor": "#dedede", |
70 | + }, |
71 | + "service": { |
72 | + "color": "#772953", |
73 | + "shape": "component", |
74 | + "style": "filled", |
75 | + "fontcolor": "#ffffff", |
76 | + }, |
77 | + "unit": { |
78 | + "color": "#DD4814", |
79 | + "fontcolor": "#ffffff", |
80 | + "shape": "box", |
81 | + "style": "filled", |
82 | + }, |
83 | + "relation": {}, |
84 | + } |
85 | + |
86 | + |
87 | +def configure_subparser(subparsers): |
88 | + sub_parser = subparsers.add_parser("status", help=command.__doc__) |
89 | + |
90 | + sub_parser.add_argument( |
91 | + "--environment", "-e", |
92 | + help="Environment to status.") |
93 | + |
94 | + sub_parser.add_argument("--output", |
95 | + help="An optional filename to output " |
96 | + "the result to", |
97 | + type=argparse.FileType("w"), |
98 | + default=sys.stdout) |
99 | + |
100 | + sub_parser.add_argument("--format", |
101 | + help="Select an output format", |
102 | + default="json" |
103 | + ) |
104 | + |
105 | + sub_parser.add_argument("--watch", "-w", |
106 | + action="store_true", |
107 | + help="""Interval in seconds to return |
108 | + status updates""") |
109 | + |
110 | + sub_parser.add_argument("scope", |
111 | + nargs="+", |
112 | + help="""scope of status request""") |
113 | + |
114 | + return sub_parser |
115 | + |
116 | + |
117 | +def command(options): |
118 | + """ |
119 | + Output status information about a deployment. |
120 | + """ |
121 | + environment = options.environments.get(options.environment) |
122 | + if environment is None and options.environment: |
123 | + raise EnvironmentsConfigError( |
124 | + "Invalid environment %r" % options.environment) |
125 | + elif environment is None: |
126 | + environment = options.environments.get_default() |
127 | + |
128 | + renderer = renderers.get("render_%s" % options.format) |
129 | + if renderer is None: |
130 | + raise SystemExit("Unsupported render format %s" % ( |
131 | + options.format)) |
132 | + |
133 | + return status(environment, options.scope, renderer, options.output) |
134 | + |
135 | + |
136 | +@inlineCallbacks |
137 | +def status(environment, scope, renderer, output): |
138 | + """Collect and render status information about a given |
139 | + environment. |
140 | + """ |
141 | + provider = environment.get_machine_provider() |
142 | + client = yield provider.connect() |
143 | + |
144 | + # watch topology change, any change can trigger |
145 | + # a new collection/render cycle |
146 | + #if options.watch: |
147 | + # pass |
148 | + # Collect status information |
149 | + state = yield collect(environment, scope) |
150 | + # Render |
151 | + renderer(state, output, environment) |
152 | + yield client.close() |
153 | + |
154 | + |
155 | +class Node(dict): |
156 | + """ |
157 | + Represents a graph node. Behaves as a dict with an explicit (and |
158 | + optional) parent reference. When using a visitor pattern this has |
159 | + helper methods for supporting mark/seen annotations |
160 | + """ |
161 | + |
162 | + def __init__(self, name="", parent=None, **kwargs): |
163 | + self.parent = parent |
164 | + self["name"] = name |
165 | + self.update(kwargs) |
166 | + |
167 | + # vistor assistance |
168 | + self._marked = False |
169 | + if parent: |
170 | + parent.add_child_node(self) |
171 | + |
172 | + def __hash__(self): |
173 | + return hash(self.name) |
174 | + |
175 | + def __eq__(self, other): |
176 | + return self.path == other.path |
177 | + |
178 | + @property |
179 | + def name(self): |
180 | + return self["name"] |
181 | + |
182 | + @property |
183 | + def path(self): |
184 | + path = [] |
185 | + obj = self |
186 | + while obj: |
187 | + path.insert(0, obj.name) |
188 | + obj = obj.parent |
189 | + if path and not path[0]: |
190 | + path.pop(0) |
191 | + return path |
192 | + |
193 | + @property |
194 | + def pathname(self): |
195 | + return ".".join(self.path) |
196 | + |
197 | + def __contains__(self, key): |
198 | + try: |
199 | + self.__getitem__(key) |
200 | + except KeyError: |
201 | + return False |
202 | + return True |
203 | + |
204 | + def __getitem__(self, key): |
205 | + """Support '.' delimited nested node paths as arguments to |
206 | + dict access""" |
207 | + if "." not in key: |
208 | + return super(Node, self).__getitem__(key) |
209 | + |
210 | + path = key.split(".") |
211 | + ob = self |
212 | + for p in path: |
213 | + # each element of the path must be a node |
214 | + # either return it or create it accordingly |
215 | + ob = ob.get(p, _marker) |
216 | + if ob is _marker: |
217 | + raise KeyError(key) |
218 | + |
219 | + return ob |
220 | + __getattr__ = __getitem__ |
221 | + |
222 | + def __setitem__(self, key, value): |
223 | + """Support dict styled sets with '.' delimited path support |
224 | + where using '.' will nest child nodes automatically |
225 | + """ |
226 | + if "." not in key: |
227 | + # This is a plain set, value shouldn't be a node |
228 | + # if trying to set a child directly call add_child_node |
229 | + super(Node, self).__setitem__(key, value) |
230 | + return |
231 | + |
232 | + if not isinstance(value, Node): |
233 | + raise ValueError("""Nested paths only allow setting Node children""") |
234 | + # Its a nested path |
235 | + path = key.split(".") |
236 | + ob = self |
237 | + for p in path: |
238 | + # each element of the path must be a node |
239 | + # either return it or create it accordingly |
240 | + ob = ob.setdefault(p, Node(p, parent=ob)) |
241 | + |
242 | + # then on the last element add the final node |
243 | + ob.add_child_node(value) |
244 | + |
245 | + @property |
246 | + def is_marked(self): |
247 | + return self._marked is True |
248 | + |
249 | + def add_child_node(self, other): |
250 | + super(Node, self).__setitem__(other.name, other) |
251 | + other.parent = self |
252 | + |
253 | + def mark(self, value=True): |
254 | + """Mark a node as seen, its value should be included in the |
255 | + output set. |
256 | + """ |
257 | + self._marked = value |
258 | + |
259 | + def mark_parents(self): |
260 | + """A convenience method to mark all the ancestors of a node.""" |
261 | + ob = self |
262 | + while ob: |
263 | + ob.mark() |
264 | + ob = ob.parent |
265 | + |
266 | + def mark_children(self): |
267 | + """A convenience method to recursively mark all children of a |
268 | + node""" |
269 | + for node in self.iternodes(True): |
270 | + node.mark() |
271 | + |
272 | + def filter(self, patterns): |
273 | + """ |
274 | + Return a new node tree containing only those nodes that match |
275 | + a pattern in patterns (and the children of any such nodes). |
276 | + |
277 | + `patterns`: list or string of fnmatch style descriptors to be |
278 | + compared to node names. |
279 | + """ |
280 | + result = Node() |
281 | + nodes = list(self.iternodes(True)) |
282 | + nodes = sorted(nodes, key=lambda x: x.name) |
283 | + |
284 | + # reset the tree (very non-concurrent) |
285 | + [n.mark(False) for n in nodes] |
286 | + |
287 | + for node in nodes: |
288 | + if filter_name(node.name, patterns): |
289 | + # mark this node and its parents |
290 | + # and its children |
291 | + node.mark_parents() |
292 | + node.mark_children() |
293 | + |
294 | + # now walk the tree |
295 | + for node in nodes: |
296 | + if node.is_marked: |
297 | + result[node.pathname] = node |
298 | + |
299 | + return result |
300 | + |
301 | + def iternodes(self, recurse=False): |
302 | + """Iterator method yielding back only Node instances among |
303 | + children. The output can optionally include the nodes of those |
304 | + children as well. |
305 | + |
306 | + """ |
307 | + for value in self.itervalues(): |
308 | + if isinstance(value, Node): |
309 | + yield value |
310 | + if recurse: |
311 | + for v in value.iternodes(recurse=recurse): |
312 | + yield v |
313 | + |
314 | + |
315 | +@inlineCallbacks |
316 | +def collect(environment, scopes): |
317 | + """Collect the objects which will be rendered for status information |
318 | + keyword arguments: |
319 | + |
320 | + `environment`: ensemble environment |
321 | + |
322 | + `scope`: an optional list of name specifiers. Globbing based |
323 | + wildcards supported. Defaults to all units, services and |
324 | + relations. |
325 | + """ |
326 | + provider = environment.get_machine_provider() |
327 | + client = yield provider.connect() |
328 | + |
329 | + service_manager = ServiceStateManager(client) |
330 | + machine_manager = MachineStateManager(client) |
331 | + relation_manager = RelationStateManager(client) |
332 | + |
333 | + # status the runtime environment |
334 | + # build a graph of nested dicts from services down to relations |
335 | + # with the machines and the units mapped under them |
336 | + # then process this tree as a graph annotating each node when the |
337 | + # following is observed |
338 | + # if a nodes name matches the scope all its children are marked |
339 | + # if a nodes name matches its parents are marked |
340 | + # at the end of this process unmarked nodes are removed from the |
341 | + # data. This is then passed to the renderer. |
342 | + |
343 | + # this produces a (manager, element_name, category) tuple which is added to |
344 | + # the components list |
345 | + components = [] |
346 | + state = Node() |
347 | + |
348 | + # for (category, manager) in (("services", service_manager), |
349 | + # ("machines", machine_manager), |
350 | + # ("relation", relation_manager)): |
351 | + # names = yield manager.keys() |
352 | + # for name in names: |
353 | + # components.append((manager, name, category)) |
354 | + |
355 | + # # ask each manager for the object by name including its |
356 | + # for (manager, name, category) in components: |
357 | + # instance = yield manager.get(name) |
358 | + # data = None |
359 | + # # create the data dicts expected by the render phase |
360 | + # if manager is service_manager: |
361 | + # # serialize instance |
362 | + # # add relations |
363 | + # data = serialize(instance) |
364 | + # relations ={} |
365 | + # relation_states = yield relation_manager.get_relations_for_service(instance) |
366 | + # for relation_state in relation_states: |
367 | + # relations[relation_state.internal_relation_id] = serialize(relation_state) |
368 | + |
369 | + # data["relations"] = relations |
370 | + |
371 | + |
372 | + # elif manager is machine_manager: |
373 | + # pass |
374 | + # elif manager is relation_manager: |
375 | + # pass |
376 | + |
377 | + # if data: |
378 | + # state.setdefault(category, list()).append(data) |
379 | + |
380 | + services = yield service_manager.values() |
381 | + for service in services: |
382 | + s = Node(service.service_name, state) |
383 | + # and serialize data from the service into n |
384 | + s["service_name"] = service.service_name |
385 | + units = yield service.get_all_unit_states() |
386 | + us = Node("units", s) |
387 | + for unit in units: |
388 | + u = Node(unit.unit_name, us) |
389 | + u["unit_name"] = unit.unit_name |
390 | + machine = yield unit.get_machine() |
391 | + machine_name = yield machine.get_instance_id() |
392 | + u["machine_name"] = machine_name |
393 | + m = Node(machine_name, u) |
394 | + |
395 | + relations = yield relation_manager.get_relations_for_service(service) |
396 | + rs = Node("relations", s) |
397 | + for relation in relations: |
398 | + r = Node(relation.internal_relation_id, rs) |
399 | + r["relation_role"] = relation.relation_role |
400 | + rel_services = yield relation.get_service_states() |
401 | + r.setdefault("services", []).append( |
402 | + [s.service_name for s in rel_services]) |
403 | + |
404 | + state = state.filter(scopes) |
405 | + print state |
406 | + returnValue(state) |
407 | + |
408 | + |
409 | +def filter_name(name, patterns): |
410 | + """Returns boolean if name is in patterns. |
411 | + |
412 | + `name`: a string |
413 | + `patterns`: either a string or a list of strings containing either |
414 | + direct matching specfications or glob based wildcards |
415 | + """ |
416 | + |
417 | + if not patterns: |
418 | + return True |
419 | + |
420 | + if isinstance(patterns, basestring): |
421 | + patterns = [patterns] |
422 | + |
423 | + for pattern in patterns: |
424 | + if fnmatch.fnmatch(name, pattern): |
425 | + return True |
426 | + return False |
427 | + |
428 | + |
429 | +def jsonify(data, filelike, pretty=True, **kwargs): |
430 | + args = dict(skipkeys=True) |
431 | + args.update(kwargs) |
432 | + if pretty: |
433 | + args["sort_keys"] = True |
434 | + args["indent"] = 4 |
435 | + return json.dump(data, filelike, **args) |
436 | + |
437 | + |
438 | +def render_json(data, filelike, environment): |
439 | + jsonify(data, filelike) |
440 | +renderers["json"] = render_json |
441 | + |
442 | +def dot_method(format): |
443 | + """ |
444 | + Pydot supports many output formats. Support many common ones with |
445 | + a function template. |
446 | + """ |
447 | + try: |
448 | + import pydot |
449 | + except ImportError: |
450 | + raise SystemExit("""You need to install the pydot """ |
451 | + """library to support DOT visualizations""") |
452 | + |
453 | + def render_dot(data, filelike, environment, style=DEFAULT_STYLE): |
454 | + dot = pydot.Dot( |
455 | + graph_name=environment.name, |
456 | + simplify=True, |
457 | + ) |
458 | + # first create a cluster for each service |
459 | + for service in data.iternodes(): |
460 | + cluster = pydot.Cluster( |
461 | + service.service_name, |
462 | + shape="component", |
463 | + label="%s service" % (service.service_name), |
464 | + **style["service_container"]) |
465 | + |
466 | + snode = pydot.Node(service.service_name, |
467 | + label="<%s>" % (service.service_name,), |
468 | + **style["service"]) |
469 | + cluster.add_node(snode) |
470 | + |
471 | + for unit in service.units.iternodes(): |
472 | + machine_name = unit.machine_name |
473 | + un = pydot.Node(unit.unit_name, |
474 | + label="<%s<br/><i>%s</i>>" % (unit.unit_name, |
475 | + machine_name), |
476 | + **style["unit"]) |
477 | + cluster.add_node(un) |
478 | + |
479 | + cluster.add_edge(pydot.Edge(snode, un)) |
480 | + |
481 | + dot.add_subgraph(cluster) |
482 | + |
483 | + # now map the relationships |
484 | + for relation in service.relations.iternodes(): |
485 | + kind = relation.relation_role |
486 | + dirtype = None |
487 | + if len(relation.services) == 2: |
488 | + src, dest = relation.services |
489 | + if kind == "peer": |
490 | + dirtype = "both" |
491 | + elif kind == "client": |
492 | + # only draw one side of |
493 | + # client-server relationships |
494 | + dirtype = "forward" |
495 | + |
496 | + if dirtype: |
497 | + dot.add_edge(pydot.Edge( |
498 | + src.service_name, |
499 | + dest.service_name, |
500 | + #constraint=False, |
501 | + dirType=dirtype, |
502 | + label=kind, |
503 | + **style["relation"] |
504 | + )) |
505 | + |
506 | + filelike.write(dot.create(format=format)) |
507 | + |
508 | + renderers[format] = render_dot |
509 | + return render_dot |
510 | + |
511 | +render_dot = dot_method("dot") |
512 | +render_svg = dot_method("svg") |
513 | +render_png = dot_method("png") |
514 | + |
515 | + |
516 | +def render_human(data, filelike, environment): |
517 | + """human readable output version of the status""" |
518 | + # first create a cluster for each service |
519 | + for name, service in data["services"].items(): |
520 | + print >>filelike, "%s: %s/%s" % (name, |
521 | + service["name"], |
522 | + service["sequence"] |
523 | + ) |
524 | + for unit in service["units"]: |
525 | + print >>filelike, " %s" % (unit) |
526 | + |
527 | + # now map the relationships |
528 | + for name, (kind, services) in data["relations"].items(): |
529 | + src, dest = services.keys() |
530 | + if kind == "peer": |
531 | + dirType = "<->" |
532 | + |
533 | + elif kind == "client-server": |
534 | + # determin the client and the server |
535 | + dirType = "->" |
536 | + if services[src]["role"] != "server": |
537 | + src, dest = dest, src |
538 | + print >>filelike, "%s %s %s" % ( |
539 | + src, dirType, dest) |
540 | + |
541 | + |
542 | |
543 | === added file 'ensemble/control/tests/test_status.py' |
544 | --- ensemble/control/tests/test_status.py 1970-01-01 00:00:00 +0000 |
545 | +++ ensemble/control/tests/test_status.py 2011-02-01 15:32:39 +0000 |
546 | @@ -0,0 +1,275 @@ |
547 | +from StringIO import StringIO |
548 | +import json |
549 | +import yaml |
550 | + |
551 | +from twisted.internet.defer import inlineCallbacks |
552 | + |
553 | +from .common import ControlToolTest |
554 | +from ensemble.control import status |
555 | +from ensemble.state.tests.test_service import ServiceStateManagerTestBase |
556 | + |
557 | + |
558 | +class StatusTest(ServiceStateManagerTestBase, ControlToolTest): |
559 | + |
560 | + @inlineCallbacks |
561 | + def setUp(self): |
562 | + yield ServiceStateManagerTestBase.setUp(self) |
563 | + yield ControlToolTest.setUp(self) |
564 | + |
565 | + self.capture_logging() |
566 | + config = { |
567 | + "ensemble": "environments", |
568 | + "environments": { |
569 | + "firstenv": { |
570 | + "type": "dummy", |
571 | + "admin-secret": "homer"}}} |
572 | + self.write_config(yaml.dump(config)) |
573 | + self.config.load() |
574 | + self.output = StringIO() |
575 | + |
576 | + @inlineCallbacks |
577 | + def build_topology(self, base=None): |
578 | + # build out the topology using the state managers |
579 | + m1 = yield self.machine_state_manager.add_machine_state() |
580 | + m2 = yield self.machine_state_manager.add_machine_state() |
581 | + m3 = yield self.machine_state_manager.add_machine_state() |
582 | + m4 = yield self.machine_state_manager.add_machine_state() |
583 | + m5 = yield self.machine_state_manager.add_machine_state() |
584 | + m6 = yield self.machine_state_manager.add_machine_state() |
585 | + m7 = yield self.machine_state_manager.add_machine_state() |
586 | + |
587 | + yield m1.set_instance_id("alpha") |
588 | + yield m2.set_instance_id("beta") |
589 | + yield m3.set_instance_id("cappa") |
590 | + yield m4.set_instance_id("delta") |
591 | + yield m5.set_instance_id("gamma") |
592 | + yield m6.set_instance_id("mc1") |
593 | + yield m7.set_instance_id("mc2") |
594 | + |
595 | + wordpress = yield self.add_service_from_formula("wordpress") |
596 | + mysql = yield self.add_service_from_formula("mysql") |
597 | + varnish = yield self.add_service_from_formula("varnish") |
598 | + # w/o additional metadata |
599 | + memcache = yield self.add_service("memcache") |
600 | + |
601 | + yield self.add_relation("client-server", |
602 | + (wordpress, "app", "client"), |
603 | + (mysql, "db", "server")) |
604 | + |
605 | + yield self.add_relation("client-server", |
606 | + (varnish, "proxy", "client"), |
607 | + (wordpress, "app", "server")) |
608 | + |
609 | + yield self.add_relation("client-server", |
610 | + (memcache, "cache", "server"), |
611 | + (wordpress, "app", "client")) |
612 | + |
613 | + wpu = yield wordpress.add_unit_state() |
614 | + yield wpu.assign_to_machine(m1) |
615 | + |
616 | + myu = yield mysql.add_unit_state() |
617 | + myu2 = yield mysql.add_unit_state() |
618 | + yield myu.assign_to_machine(m2) |
619 | + yield myu2.assign_to_machine(m3) |
620 | + |
621 | + vu1 = yield varnish.add_unit_state() |
622 | + vu2 = yield varnish.add_unit_state() |
623 | + yield vu1.assign_to_machine(m4) |
624 | + yield vu2.assign_to_machine(m5) |
625 | + |
626 | + mc1 = yield memcache.add_unit_state() |
627 | + mc2 = yield memcache.add_unit_state() |
628 | + yield mc1.assign_to_machine(m6) |
629 | + yield mc2.assign_to_machine(m7) |
630 | + |
631 | + def test_filter_name(self): |
632 | + # patterns is string |
633 | + self.assertTrue(status.filter_name("alpha/1", "*/1")) |
634 | + self.assertTrue(status.filter_name("alpha/1", "alpha*")) |
635 | + self.assertFalse(status.filter_name("alpha/1", "beta*")) |
636 | + |
637 | + # patterns is a list |
638 | + self.assertTrue(status.filter_name("alpha/1", ["*/1"])) |
639 | + self.assertTrue(status.filter_name("alpha/1", ["alpha*"])) |
640 | + self.assertFalse(status.filter_name("alpha/1", ["beta*"])) |
641 | + |
642 | + # patterns is a list of more than one |
643 | + # (where the first element matches) |
644 | + self.assertTrue(status.filter_name("alpha/1", ["*/1", "beta"])) |
645 | + self.assertTrue(status.filter_name("alpha/1", ["alpha*", "beta"])) |
646 | + self.assertFalse(status.filter_name("alpha/1", ["beta*", "gamma"])) |
647 | + # (and where only the second matches) |
648 | + self.assertTrue(status.filter_name("alpha/1", ["beta", "*/1"])) |
649 | + self.assertTrue(status.filter_name("alpha/1", ["beta", "alpha*"])) |
650 | + self.assertFalse(status.filter_name("alpha/1", ["gamma", "beta*"])) |
651 | + |
652 | + @inlineCallbacks |
653 | + def test_render_json(self): |
654 | + environment = self.config.get("firstenv") |
655 | + yield self.build_topology() |
656 | + |
657 | + yield status.status(environment, [], |
658 | + status.render_json, self.output) |
659 | + |
660 | + # @inlineCallbacks |
661 | + # def test_render_human(self): |
662 | + # environment = self.config.get("firstenv") |
663 | + # yield self.build_topology() |
664 | + |
665 | + # yield status.status(self.config, environment, [], |
666 | + # status.render_human, self.output) |
667 | + # print self.output.getvalue() |
668 | + # #self.assertIn("service-0000000000: mysql/1", self.output.getvalue()) |
669 | + |
670 | + # @inlineCallbacks |
671 | + # def test_render_dot(self): |
672 | + # environment = self.config.get("firstenv") |
673 | + # yield self.build_topology() |
674 | + |
675 | + # yield status.status(self.config, environment, [], |
676 | + # status.render_human, self.output) |
677 | + # print self.output.getvalue() |
678 | + # # verify various expected clauses (without caring about things |
679 | + # # like spatial positioning |
680 | + # self.assertIn("mysql -> wordpress", result) |
681 | + # self.assertIn('mysql -> "unit-0000000000"', result) |
682 | + # self.assertIn('wordpress -> "unit-0000000001"', result) |
683 | + |
684 | + @inlineCallbacks |
685 | + def test_status_base_topology(self): |
686 | + # set up machine name mocks to map back to machines these |
687 | + # names should be output in the status information |
688 | + environment = self.config.get("firstenv") |
689 | + yield self.build_topology() |
690 | + |
691 | + yield status.status(environment, [], |
692 | + status.render_svg, self.output) |
693 | + fp = open("/tmp/ens.svg", "w") |
694 | + fp.write(self.output.getvalue()) |
695 | + fp.close() |
696 | + |
697 | + self.output = StringIO() |
698 | + yield status.status(environment, [], |
699 | + status.render_dot, self.output) |
700 | + fp = open("/tmp/ens.dot", "w") |
701 | + fp.write(self.output.getvalue()) |
702 | + fp.close() |
703 | + |
704 | + def test_node_basics(self): |
705 | + Node = status.Node |
706 | + |
707 | + wp = Node("wordpress") |
708 | + wp["service_id"] = "service-0000000000" |
709 | + |
710 | + self.assertEqual(wp["name"], "wordpress") |
711 | + self.assertEqual(wp.name, "wordpress") |
712 | + self.assertEqual(wp["service_id"], "service-0000000000") |
713 | + self.assertEqual(wp.parent, None) |
714 | + |
715 | + m1 = Node("machine1", wp) |
716 | + self.assertEqual(m1.parent, wp) |
717 | + self.assertEqual(m1.path, ["wordpress", "machine1"]) |
718 | + |
719 | + def build_simple_node_tree(self): |
720 | + Node = status.Node |
721 | + root = Node() |
722 | + wp = Node("wordpress", root) |
723 | + wp0 = Node("wordpress/0", wp) |
724 | + wp1 = Node("wordpress/1", wp) |
725 | + Node("machine1", wp0) |
726 | + Node("machine2", wp1) |
727 | + |
728 | + my = Node("mysql", root) |
729 | + my0 = Node("mysql/0", my) |
730 | + my1 = Node("mysql/1", my) |
731 | + Node("machine3", my0) |
732 | + Node("machine4", my1) |
733 | + return root |
734 | + |
735 | + def test_node_mark(self): |
736 | + root = self.build_simple_node_tree() |
737 | + wp0 = root["wordpress.wordpress/0"] |
738 | + |
739 | + wp0.mark_parents() |
740 | + self.assertTrue(root.is_marked) |
741 | + self.assertTrue(root["wordpress"].is_marked) |
742 | + self.assertTrue(wp0.is_marked) |
743 | + |
744 | + self.assertFalse(root["mysql"].is_marked) |
745 | + self.assertFalse(wp0["machine1"].is_marked) |
746 | + |
747 | + # now mark the children |
748 | + wp0.mark_children() |
749 | + self.assertTrue(wp0["machine1"].is_marked) |
750 | + |
751 | + def test_node_nested_paths(self): |
752 | + # test with missing path |
753 | + root = self.build_simple_node_tree() |
754 | + |
755 | + # literal value |
756 | + try: |
757 | + root["alpha.beta"] = "literal" |
758 | + except ValueError: |
759 | + pass |
760 | + else: |
761 | + self.fail("""Literal value with nested path key in node didn't fail properly""") |
762 | + |
763 | + # node child |
764 | + another = status.Node("another") |
765 | + root["alpha.beta"] = another |
766 | + |
767 | + alpha = root["alpha"] |
768 | + self.assertInstance(alpha, status.Node) |
769 | + self.assertEqual(alpha.parent, root) |
770 | + |
771 | + beta = alpha["beta"] |
772 | + self.assertEqual(beta.pathname, "alpha.beta") |
773 | + |
774 | + # test path base dict access |
775 | + self.assertEqual(another, root["alpha.beta.another"]) |
776 | + |
777 | + # get a literal with a path |
778 | + another["foo"] = "bar" |
779 | + self.assertEqual(another["foo"], "bar") |
780 | + self.assertEqual(root["alpha.beta.another.foo"], "bar") |
781 | + |
782 | + self.assertTrue("alpha.beta" in root) |
783 | + |
784 | + def test_node_iteration(self): |
785 | + root = self.build_simple_node_tree() |
786 | + nodes = set(root.iternodes()) |
787 | + names = set(n.pathname for n in nodes) |
788 | + self.assertEqual(names, set(("wordpress", "mysql"))) |
789 | + |
790 | + # and with recursion |
791 | + nodes = set(root.iternodes(True)) |
792 | + names = set(n.pathname for n in nodes) |
793 | + self.assertEqual(names, |
794 | + set(["mysql", |
795 | + "mysql.mysql/0", |
796 | + "mysql.mysql/0.machine3", |
797 | + "mysql.mysql/1", |
798 | + "mysql.mysql/1.machine4", |
799 | + "wordpress", |
800 | + "wordpress.wordpress/0", |
801 | + "wordpress.wordpress/0.machine1", |
802 | + "wordpress.wordpress/1", |
803 | + "wordpress.wordpress/1.machine2"])) |
804 | + |
805 | + def test_node_mark_sweep(self): |
806 | + root = self.build_simple_node_tree() |
807 | + # now attempt to mark/sweep with a name based filter |
808 | + result = root.filter("wordpress") |
809 | + self.assertIn("wordpress", result) |
810 | + self.assertFalse("mysql" in result) |
811 | + |
812 | + # now try a lower level filter |
813 | + result = root.filter("wordpress/1") |
814 | + # the top level expectations are the same but because we |
815 | + # matched a name at the middle layer of the graph we wouldn't |
816 | + # have recursively marked down from wordpress and don't expect |
817 | + # to find wordpress/0 in the result |
818 | + self.assertIn("wordpress", result) |
819 | + self.assertFalse("mysql" in result) |
820 | + |
821 | + |
822 | |
823 | === modified file 'ensemble/providers/dummy.py' |
824 | --- ensemble/providers/dummy.py 2011-01-26 23:51:42 +0000 |
825 | +++ ensemble/providers/dummy.py 2011-02-01 15:32:39 +0000 |
826 | @@ -37,8 +37,13 @@ |
827 | if not "machine-id" in machine_data: |
828 | return fail(ProviderError( |
829 | "Machine state `machine-id` required in machine_data")) |
830 | +<<<<<<< TREE |
831 | dns_name = machine_data.get("dns-name") |
832 | machine = DummyMachine(len(self._machines), dns_name) |
833 | +======= |
834 | + machine = DummyMachine(len(self._machines)) |
835 | + machine.machine_data = machine_data |
836 | +>>>>>>> MERGE-SOURCE |
837 | self._machines.append(machine) |
838 | return succeed([machine]) |
839 | |
840 | |
841 | === modified file 'ensemble/state/base.py' |
842 | --- ensemble/state/base.py 2010-12-07 01:26:01 +0000 |
843 | +++ ensemble/state/base.py 2011-02-01 15:32:39 +0000 |
844 | @@ -124,3 +124,53 @@ |
845 | return |
846 | self._old_topology = new_topology |
847 | watch.addCallback(self.__topology_changed, watch_topology_function) |
848 | + |
849 | + # Common Iteration API methods |
850 | + # These depend on the subclasses defining |
851 | + # state_accessor and state_iter shown below |
852 | + @inlineCallbacks |
853 | + def keys(self): |
854 | + """Iterate the top level objects of this manager returning ids |
855 | + that can be passed to the get() method. |
856 | + """ |
857 | + keys = yield self.state_iter() |
858 | + returnValue(keys) |
859 | + |
860 | + @inlineCallbacks |
861 | + def values(self): |
862 | + """Iterate the top level objects of this manager. |
863 | + """ |
864 | + result = [] |
865 | + keys = yield self.keys() |
866 | + for key in keys: |
867 | + state = yield self.get(key) |
868 | + result.append(state) |
869 | + |
870 | + returnValue(result) |
871 | + |
872 | + @inlineCallbacks |
873 | + def items(self): |
874 | + """Iterate the top level objects of a this manager returning |
875 | + (id, instance) tuples. |
876 | + """ |
877 | + result = [] |
878 | + keys = yield self.keys() |
879 | + for key in keys: |
880 | + state = yield self.get(key) |
881 | + result.append((key, state)) |
882 | + returnValue(result) |
883 | + |
884 | + @inlineCallbacks |
885 | + def get(self, key): |
886 | + """ |
887 | + Return the state object for this manager |
888 | + """ |
889 | + state = yield self.state_accessor(key) |
890 | + returnValue(state) |
891 | + |
892 | + # These define a simple way to bind existing methods to a common |
893 | + # API shared by state managers |
894 | + # accessor for state by key |
895 | + state_accessor = None |
896 | + # iterator over state instances by name |
897 | + state_iter = None |
898 | |
899 | === modified file 'ensemble/state/machine.py' |
900 | --- ensemble/state/machine.py 2011-01-31 22:57:52 +0000 |
901 | +++ ensemble/state/machine.py 2011-02-01 15:32:39 +0000 |
902 | @@ -76,6 +76,30 @@ |
903 | machine_state = MachineState(self._client, internal_id) |
904 | returnValue(machine_state) |
905 | |
906 | + @inlineCallbacks |
907 | + def get_all_machine_states(self): |
908 | + topology = yield self._read_topology() |
909 | + machines = [] |
910 | + for machine_id in topology.get_machines(): |
911 | + # ugh, string manipulation |
912 | + machine_id = _internal_id_to_id(machine_id) |
913 | + machine = yield self.get_machine_state(machine_id) |
914 | + machines.append(machine) |
915 | + returnValue(machines) |
916 | + |
917 | + @inlineCallbacks |
918 | + def get_all_machine_names(self): |
919 | + topology = yield self._read_topology() |
920 | + machines = [] |
921 | + for machine_id in topology.get_machines(): |
922 | + # ugh, string manipulation |
923 | + machine_id = int(machine_id[-10:]) |
924 | + machines.append(machine_id) |
925 | + returnValue(machines) |
926 | + |
927 | + state_accessor = get_machine_state |
928 | + state_iter = get_all_machine_names |
929 | + |
930 | def watch_machine_states(self, callback): |
931 | """Observe changes in the known machines through the watch function. |
932 | |
933 | @@ -113,6 +137,14 @@ |
934 | super(MachineState, self).__init__(client) |
935 | self._internal_id = internal_id |
936 | |
937 | + def __hash__(self): |
938 | + return hash(self.id) |
939 | + |
940 | + def __eq__(self, other): |
941 | + if not isinstance(other, MachineState): |
942 | + return False |
943 | + return self.id == other.id |
944 | + |
945 | @property |
946 | def id(self): |
947 | """High-level id built using the sequence as an int.""" |
948 | |
949 | === modified file 'ensemble/state/relation.py' |
950 | --- ensemble/state/relation.py 2011-01-31 16:13:10 +0000 |
951 | +++ ensemble/state/relation.py 2011-02-01 15:32:39 +0000 |
952 | @@ -137,6 +137,15 @@ |
953 | yield self._retry_topology_change(remove_relation) |
954 | |
955 | @inlineCallbacks |
956 | + def get_relation_state(self, relation_id): |
957 | + """Return a RelationState for a given id or None""" |
958 | + topology = yield self._read_topology() |
959 | + relation = None |
960 | + if topology.has_relation(relation_id): |
961 | + relation = RelationState(self._client, relation_id) |
962 | + returnValue(relation) |
963 | + |
964 | + @inlineCallbacks |
965 | def get_relations_for_service(self, service_state): |
966 | """Get the relations associated to the service. |
967 | """ |
968 | @@ -153,6 +162,7 @@ |
969 | **service_info)) |
970 | returnValue(relations) |
971 | |
972 | +<<<<<<< TREE |
973 | @inlineCallbacks |
974 | def get_relation_state(self, *endpoints): |
975 | """Return `relation_state` connecting the endpoints. |
976 | @@ -165,6 +175,19 @@ |
977 | raise RelationStateNotFound() |
978 | returnValue(RelationState(self._client, internal_id)) |
979 | |
980 | +======= |
981 | + @inlineCallbacks |
982 | + def get_all_relation_names(self): |
983 | + """ |
984 | + Return the names of the |
985 | + """ |
986 | + topology = yield self._read_topology() |
987 | + returnValue(topology.get_all_relation_names()) |
988 | + |
989 | + state_iter = get_all_relation_names |
990 | + state_accessor = get_relation_state |
991 | + |
992 | +>>>>>>> MERGE-SOURCE |
993 | |
994 | class RelationState(StateBase): |
995 | """Represents a connection between one or more services. |
996 | @@ -284,6 +307,21 @@ |
997 | unit_state.internal_id, |
998 | self._relation_id)) |
999 | |
1000 | + @inlineCallbacks |
1001 | + def get_service_states(self): |
1002 | + """Get all the services associated with a given relation. |
1003 | + """ |
1004 | + from ensemble.state.service import ServiceStateManager |
1005 | + service_manager = ServiceStateManager(self._client) |
1006 | + services = [] |
1007 | + topology = yield service_manager._read_topology() |
1008 | + for service in topology.get_relation_services( |
1009 | + self.internal_relation_id): |
1010 | + service_name = topology.get_service_name(service) |
1011 | + service = yield service_manager.get_service_state(service_name) |
1012 | + services.append(service) |
1013 | + returnValue(services) |
1014 | + |
1015 | |
1016 | class UnitRelationState(StateBase): |
1017 | """A service unit's relation state. |
1018 | |
1019 | === modified file 'ensemble/state/service.py' |
1020 | --- ensemble/state/service.py 2011-01-31 22:57:52 +0000 |
1021 | +++ ensemble/state/service.py 2011-02-01 15:32:39 +0000 |
1022 | @@ -94,6 +94,7 @@ |
1023 | returnValue(ServiceState(self._client, internal_id, service_name)) |
1024 | |
1025 | @inlineCallbacks |
1026 | +<<<<<<< TREE |
1027 | def get_unit_state(self, unit_name): |
1028 | """Returns the unit state with the given name. |
1029 | |
1030 | @@ -109,6 +110,34 @@ |
1031 | returnValue(unit_state) |
1032 | |
1033 | @inlineCallbacks |
1034 | +======= |
1035 | + def get_all_service_states(self): |
1036 | + """ |
1037 | + Yield a ServiceState for each service in the environment. |
1038 | + """ |
1039 | + topology = yield self._read_topology() |
1040 | + services = [] |
1041 | + for service_id in topology.get_services(): |
1042 | + service_name = topology.get_service_name(service_id) |
1043 | + service = yield self.get_service_state(service_name) |
1044 | + services.append(service) |
1045 | + returnValue(services) |
1046 | + |
1047 | + @inlineCallbacks |
1048 | + def get_all_service_names(self): |
1049 | + topology = yield self._read_topology() |
1050 | + names = [] |
1051 | + for service_id in topology.get_services(): |
1052 | + service_name = topology.get_service_name(service_id) |
1053 | + names.append(service_name) |
1054 | + returnValue(names) |
1055 | + |
1056 | + # support the common dict access API |
1057 | + state_iter = get_all_service_names |
1058 | + state_accessor = get_service_state |
1059 | + |
1060 | + @inlineCallbacks |
1061 | +>>>>>>> MERGE-SOURCE |
1062 | def get_relation_endpoints(self, descriptor): |
1063 | """Get all relation endpoints for `descriptor`. |
1064 | |
1065 | @@ -180,6 +209,19 @@ |
1066 | self._internal_id = internal_id |
1067 | self._service_name = service_name |
1068 | |
1069 | + def __hash__(self): |
1070 | + return hash(self.internal_id) |
1071 | + |
1072 | + def __eq__(self, other): |
1073 | + if not isinstance(other, ServiceState): |
1074 | + return False |
1075 | + return self.internal_id == other.internal_id |
1076 | + |
1077 | + def __repr__(self): |
1078 | + return "<%s %s>" % ( |
1079 | + self.__class__.__name__, |
1080 | + self.internal_id) |
1081 | + |
1082 | @property |
1083 | def service_name(self): |
1084 | """Name of the service represented by this state. |
1085 | @@ -232,6 +274,7 @@ |
1086 | internal_unit_id)) |
1087 | |
1088 | @inlineCallbacks |
1089 | +<<<<<<< TREE |
1090 | def get_unit_names(self): |
1091 | topology = yield self._read_topology() |
1092 | if not topology.has_service(self._internal_id): |
1093 | @@ -269,6 +312,24 @@ |
1094 | self._client, "/units/%s" % service_unit.internal_id) |
1095 | |
1096 | @inlineCallbacks |
1097 | +======= |
1098 | + def get_all_unit_states(self): |
1099 | + """Return a list of ServiceUnitState instances associated with |
1100 | + this service. |
1101 | + """ |
1102 | + topology = yield self._read_topology() |
1103 | + units = [] |
1104 | + |
1105 | + for unit_id in topology.get_service_units(self._internal_id): |
1106 | + unit_name = topology.get_service_unit_name(self._internal_id, |
1107 | + unit_id) |
1108 | + unit = yield self.get_unit_state(unit_name) |
1109 | + units.append(unit) |
1110 | + |
1111 | + returnValue(units) |
1112 | + |
1113 | + @inlineCallbacks |
1114 | +>>>>>>> MERGE-SOURCE |
1115 | def get_unit_state(self, unit_name): |
1116 | """Return service unit state with the given unit name. |
1117 | |
1118 | @@ -338,6 +399,16 @@ |
1119 | |
1120 | self._watch_topology(watch_topology) |
1121 | |
1122 | + @inlineCallbacks |
1123 | + def get_relations(self): |
1124 | + """Return a list ServiceRelationState objects for the |
1125 | + relations of this service. |
1126 | + """ |
1127 | + from ensemble.state.relation import RelationStateManager |
1128 | + relation_manager = RelationStateManager(self.client) |
1129 | + relations = yield relation_manager.get_relations_for_service(self) |
1130 | + returnValue(relations) |
1131 | + |
1132 | |
1133 | def _to_service_relation_state(client, service_id, assigned_relations): |
1134 | """Helper method to construct a list of service relation states. |
1135 | @@ -373,6 +444,25 @@ |
1136 | self._unit_sequence = unit_sequence |
1137 | self._internal_id = internal_id |
1138 | |
1139 | + def __hash__(self): |
1140 | + return hash(self.unit_name) |
1141 | + |
1142 | + def __eq__(self, other): |
1143 | + if not isinstance(other, ServiceUnitState): |
1144 | + return False |
1145 | + return self.unit_name == other.unit_name |
1146 | + |
1147 | + def __repr__(self): |
1148 | + return "<%s %s>" % (self.__class__.__name__, |
1149 | + self.unit_name) |
1150 | + |
1151 | + @inlineCallbacks |
1152 | + def as_dict(self): |
1153 | + returnValue( |
1154 | + dict(service_name=self.service_name, |
1155 | + unit_name=self.unit_name, |
1156 | + type="ServiceUnit")) |
1157 | + |
1158 | @property |
1159 | def service_name(self): |
1160 | """Service name for the service from this unit.""" |
1161 | @@ -392,6 +482,7 @@ |
1162 | """Get the zookeeper path for the service unit agent.""" |
1163 | return "/units/%s/agent" % self._internal_id |
1164 | |
1165 | +<<<<<<< TREE |
1166 | @inlineCallbacks |
1167 | def get_assigned_machine_id(self): |
1168 | """ Get the assigned machine id or None if the unit is not assigned. |
1169 | @@ -403,6 +494,23 @@ |
1170 | machine_id = _public_machine_id(machine_id) |
1171 | returnValue(machine_id) |
1172 | |
1173 | +======= |
1174 | + @inlineCallbacks |
1175 | + def get_machine(self): |
1176 | + """Return the MachineState for the machine assignment of this |
1177 | + unit.""" |
1178 | + from ensemble.state.machine import MachineStateManager |
1179 | + machine_state_manager = MachineStateManager(self._client) |
1180 | + topology = yield self._read_topology() |
1181 | + machine_id = topology.get_service_unit_machine( |
1182 | + self._internal_service_id, |
1183 | + self.internal_id) |
1184 | + machine_state = yield machine_state_manager.get_machine_state( |
1185 | + int(machine_id[-10:])) |
1186 | + returnValue(machine_state) |
1187 | + |
1188 | + |
1189 | +>>>>>>> MERGE-SOURCE |
1190 | def assign_to_machine(self, machine_state): |
1191 | """Assign this service unit to the given machine. |
1192 | """ |
1193 | |
1194 | === modified file 'ensemble/state/tests/test_machine.py' |
1195 | --- ensemble/state/tests/test_machine.py 2011-01-20 18:13:50 +0000 |
1196 | +++ ensemble/state/tests/test_machine.py 2011-02-01 15:32:39 +0000 |
1197 | @@ -111,6 +111,56 @@ |
1198 | self.fail("Error not raised") |
1199 | |
1200 | @inlineCallbacks |
1201 | + def test_get_all_machine_states(self): |
1202 | + machines = yield self.machine_state_manager.get_all_machine_states() |
1203 | + self.assertFalse(machines) |
1204 | + |
1205 | + yield self.machine_state_manager.add_machine_state() |
1206 | + machines = yield self.machine_state_manager.get_all_machine_states() |
1207 | + self.assertEquals(len(machines), 1) |
1208 | + |
1209 | + yield self.machine_state_manager.add_machine_state() |
1210 | + machines = yield self.machine_state_manager.get_all_machine_states() |
1211 | + self.assertEquals(len(machines), 2) |
1212 | + |
1213 | + @inlineCallbacks |
1214 | + def test_state_iter(self): |
1215 | + wordpress = yield self.service_state_manager.add_service_state("wordpress", self.formula_state) |
1216 | + yield self.service_state_manager.add_service_state("mysql", self.formula_state) |
1217 | + |
1218 | + m1 = yield self.machine_state_manager.add_machine_state() |
1219 | + m2 = yield self.machine_state_manager.add_machine_state() |
1220 | + |
1221 | + mu1 = yield wordpress.add_unit_state() |
1222 | + mu2 = yield wordpress.add_unit_state() |
1223 | + yield mu1.assign_to_machine(m1) |
1224 | + yield mu2.assign_to_machine(m2) |
1225 | + |
1226 | + keys = yield self.machine_state_manager.keys() |
1227 | + values = yield self.machine_state_manager.values() |
1228 | + items = yield self.machine_state_manager.items() |
1229 | + # this exploits that subsequent calls to key() should yield |
1230 | + # the same result ordering for the same data and that this |
1231 | + # method is used to power values and items |
1232 | + self.assertEqual(zip(keys, values), items) |
1233 | + |
1234 | + @inlineCallbacks |
1235 | + def test_set_functions(self): |
1236 | + m1 = yield self.machine_state_manager.add_machine_state() |
1237 | + m2 = yield self.machine_state_manager.add_machine_state() |
1238 | + |
1239 | + m3 = yield self.machine_state_manager.get_machine_state(0) |
1240 | + m4 = yield self.machine_state_manager.get_machine_state(1) |
1241 | + |
1242 | + self.assertEquals(hash(m1), hash(m3)) |
1243 | + self.assertEquals(hash(m2), hash(m4)) |
1244 | + self.assertNotIdentical(hash(m1), hash(m2)) |
1245 | + self.assertEquals(m1, m3) |
1246 | + self.assertEquals(m2, m4) |
1247 | + |
1248 | + |
1249 | + |
1250 | + @inlineCallbacks |
1251 | def test_set_and_get_instance_id(self): |
1252 | """ |
1253 | Each provider must have its own notion of an id for machines it offers. |
1254 | |
1255 | === modified file 'ensemble/state/tests/test_relation.py' |
1256 | --- ensemble/state/tests/test_relation.py 2011-01-31 16:13:10 +0000 |
1257 | +++ ensemble/state/tests/test_relation.py 2011-02-01 15:32:39 +0000 |
1258 | @@ -702,6 +702,13 @@ |
1259 | self.service1_relation.get_unit_state(unit_state), |
1260 | UnitRelationStateNotFound) |
1261 | |
1262 | + @inlineCallbacks |
1263 | + def test_get_all_service_states(self): |
1264 | + services = yield self.service1_relation.get_service_states() |
1265 | + self.assertEqual(set(services), |
1266 | + set((self.service_state1, |
1267 | + self.service_state2))) |
1268 | + |
1269 | |
1270 | class UnitRelationStateTest(RelationTestBase): |
1271 | |
1272 | |
1273 | === modified file 'ensemble/state/tests/test_service.py' |
1274 | --- ensemble/state/tests/test_service.py 2011-01-31 22:22:29 +0000 |
1275 | +++ ensemble/state/tests/test_service.py 2011-02-01 15:32:39 +0000 |
1276 | @@ -277,6 +277,7 @@ |
1277 | yield self.assertFailure(d, StateChanged) |
1278 | |
1279 | @inlineCallbacks |
1280 | +<<<<<<< TREE |
1281 | def test_get_unit_names(self): |
1282 | """A service's units names are retrievable.""" |
1283 | service_state = yield self.service_state_manager.add_service_state( |
1284 | @@ -328,6 +329,36 @@ |
1285 | yield service_state.remove_unit_state(unit_state) |
1286 | |
1287 | @inlineCallbacks |
1288 | +======= |
1289 | + def test_get_all_service_states(self): |
1290 | + services = yield self.service_state_manager.get_all_service_states() |
1291 | + self.assertFalse(services) |
1292 | + |
1293 | + service_state0 = yield self.service_state_manager.add_service_state( |
1294 | + "wordpress", self.formula_state) |
1295 | + services = yield self.service_state_manager.get_all_service_states() |
1296 | + self.assertEquals(len(services), 1) |
1297 | + |
1298 | + service_state1 = yield self.service_state_manager.add_service_state( |
1299 | + "mysql", self.formula_state) |
1300 | + services = yield self.service_state_manager.get_all_service_states() |
1301 | + self.assertEquals(len(services), 2) |
1302 | + |
1303 | + @inlineCallbacks |
1304 | + def test_state_iter(self): |
1305 | + yield self.service_state_manager.add_service_state("wordpress", self.formula_state) |
1306 | + yield self.service_state_manager.add_service_state("mysql", self.formula_state) |
1307 | + |
1308 | + keys = yield self.service_state_manager.keys() |
1309 | + values = yield self.service_state_manager.values() |
1310 | + items = yield self.service_state_manager.items() |
1311 | + # this exploits that subsequent calls to key() should yield |
1312 | + # the same result ordering for the same data and that this |
1313 | + # method is used to power values and items |
1314 | + self.assertEqual(zip(keys, values), items) |
1315 | + |
1316 | + @inlineCallbacks |
1317 | +>>>>>>> MERGE-SOURCE |
1318 | def test_get_service_unit(self): |
1319 | """ |
1320 | Getting back service units should be possible using the |
1321 | @@ -359,6 +390,56 @@ |
1322 | self.assertEquals(unit_state3.unit_name, "mysql/1") |
1323 | |
1324 | @inlineCallbacks |
1325 | + def test_get_all_unit_states(self): |
1326 | + service_state0 = yield self.service_state_manager.add_service_state( |
1327 | + "wordpress", self.formula_state) |
1328 | + service_state1 = yield self.service_state_manager.add_service_state( |
1329 | + "mysql", self.formula_state) |
1330 | + |
1331 | + yield service_state0.add_unit_state() |
1332 | + yield service_state1.add_unit_state() |
1333 | + yield service_state0.add_unit_state() |
1334 | + yield service_state1.add_unit_state() |
1335 | + |
1336 | + unit_state0 = yield service_state0.get_unit_state("wordpress/0") |
1337 | + unit_state1 = yield service_state1.get_unit_state("mysql/0") |
1338 | + unit_state2 = yield service_state0.get_unit_state("wordpress/1") |
1339 | + unit_state3 = yield service_state1.get_unit_state("mysql/1") |
1340 | + |
1341 | + wordpress_units = yield service_state0.get_all_unit_states() |
1342 | + self.assertEquals(set(wordpress_units), set((unit_state0, unit_state2))) |
1343 | + |
1344 | + mysql_units = yield service_state1.get_all_unit_states() |
1345 | + self.assertEquals(set(mysql_units), set((unit_state1, unit_state3))) |
1346 | + |
1347 | + @inlineCallbacks |
1348 | + def test_set_functions(self): |
1349 | + wordpress = yield self.service_state_manager.add_service_state( |
1350 | + "wordpress", self.formula_state) |
1351 | + mysql = yield self.service_state_manager.add_service_state( |
1352 | + "mysql", self.formula_state) |
1353 | + |
1354 | + s1 = yield self.service_state_manager.get_service_state( |
1355 | + "wordpress") |
1356 | + s2 = yield self.service_state_manager.get_service_state( |
1357 | + "mysql") |
1358 | + self.assertEquals(hash(s1), hash(wordpress)) |
1359 | + self.assertEquals(hash(s2), hash(mysql)) |
1360 | + self.assertNotIdentical(hash(s1), hash(s2)) |
1361 | + self.assertEquals(s1, wordpress) |
1362 | + self.assertEquals(s2, mysql) |
1363 | + |
1364 | + us0 = yield wordpress.add_unit_state() |
1365 | + us1 = yield wordpress.add_unit_state() |
1366 | + |
1367 | + unit_state0 = yield wordpress.get_unit_state("wordpress/0") |
1368 | + unit_state1 = yield wordpress.get_unit_state("wordpress/1") |
1369 | + |
1370 | + self.assertEquals(us0, unit_state0) |
1371 | + self.assertEquals(us1, unit_state1) |
1372 | + self.assertEquals(hash(us1), hash(unit_state1)) |
1373 | + |
1374 | + @inlineCallbacks |
1375 | def test_get_service_unit_not_found(self): |
1376 | """ |
1377 | Attempting to retrieve a non-existent service unit should |
1378 | |
1379 | === modified file 'ensemble/state/topology.py' |
1380 | --- ensemble/state/topology.py 2011-01-26 19:50:07 +0000 |
1381 | +++ ensemble/state/topology.py 2011-02-01 15:32:39 +0000 |
1382 | @@ -294,6 +294,12 @@ |
1383 | """ |
1384 | return relation_id in self._state.get("relations", self._nil_dict) |
1385 | |
1386 | + def get_all_relation_names(self): |
1387 | + """Returns a list of relation_id that can be passed to other |
1388 | + get_relation methods. |
1389 | + """ |
1390 | + return self._state.get("relations", self._nil_dict).keys() |
1391 | + |
1392 | def get_relation_services(self, relation_id): |
1393 | """Get all the services associated to the relation. |
1394 | """ |
A few initial comments:
[1]
+import pydot
I suggest making the dependency on this optional, by importing it inside the
specific method which depends on it and erroring out with a nice message in
case it's missing.
[2]
+ topology = yield service_ manager. _read_topology( )
This is a private method, and the raw topology information has been private
so far. I'm not sure if we should simply expose it, or if we should instead
use the public API which already exists. Either way, let's not poke the
internals like this on the final merge.
Let's talk about it live to figure a good approach.
[3]
100 + # pretend to use an api rather than topology._state
101 + # directly
Let's talk about [2] before spending time with this, but the topology has
a proper interface. That's poking the internals of the internals. The
internals of the Topology, including the format of its dictionary, should
not be used anywhere else besides its own implementation. Its interface
gives access to everything stored in there, with proper namings and
conventions.
[4]
70 + """
71 + Output status information about a deployment.
72 + """
(...)
129 + """ walk the topology state looking for all the interesting names
130 + (as defined in filters) returning the state mapping with only the
Please use the agreed convention for comments.
144 + # walk state collecting names
145 + # collect services and units
(...)
166 + #now take the list of valid names and paths and pluck them from
167 + #the source data
Also proper capitalized/spaced comments on these.