Merge lp:~bcsaller/pyjuju/ensemble-status into lp:pyjuju

Proposed by Benjamin Saller
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
Reviewer Review Type Date Requested Status
Gustavo Niemeyer Needs Fixing
Benjamin Saller (community) Needs Resubmitting
Review via email: mp+45897@code.launchpad.net

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.

To post a comment you must log in.
Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

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.

review: Needs Fixing
Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :
lp:~bcsaller/pyjuju/ensemble-status updated
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

Revision history for this message
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.

review: Needs Resubmitting
Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :
Download full text (4.5 KiB)

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/get_all_service_states/, as per the existing convention (see
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.get_relation_services(
+ self.internal_relation_id):
+ service_name = topology.get_service_name(service)

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 = ServiceStateManager(self._client)

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(self):
+ """Return a list of RelationEndpoints associated with this relation."""
+ from ensemble.state.service import ServiceStateManager
+ service_manager = ServiceStateManager(self.client)
+ endpoints = yield service_manager.get_relation_endpoints(self.service_name)

If the method is called get_relation_endpoints under service_manager, it feels
like a good idea to call it get_relation_endpoints here as well.

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_topology()
+ machines = []
+ for machine_id in topology.get_machines():
+ # ugh, string manipulation
+ machine_id = int(machine_id[-10:])

s/get_machines/get_all_machine_states/, aligned with the function immediately above.

Also, there's an _internal_id_to_id() function there.

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]

     @inlineCallbacks
+ def get_units(self):
+ """Return a list of ServiceUnitState instances associated with

s/get_units/get_all_unit_states/

[7]

+ def as_dict(self):
+ instance_id = yield self.get_instance_id()

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...

Read more...

review: Needs Fixing
Revision history for this message
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: EnvironmentsConfigError undefined

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_id[-10:])
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 servicerelationstate, as its effectivly returning
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).

lp:~bcsaller/pyjuju/ensemble-status updated
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

Revision history for this message
Benjamin Saller (bcsaller) wrote :
Download full text (3.2 KiB)

> [1]
>
> +    def get_services(self):
> +        """Get all the services associated with a given relation.
> +        """
>
> s/get_services/get_all_service_states/, as per the existing convention (see
> 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.get_relation_services(
> +            self.internal_relation_id):
> +            service_name = topology.get_service_name(service)
>
> 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 = ServiceStateManager(self._client)
>
> 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(self):
> +        """Return a list of RelationEndpoints associated with this relation."""
> +        from ensemble.state.service import ServiceStateManager
> +        service_manager = ServiceStateManager(self.client)
> +        endpoints = yield service_manager.get_relation_endpoints(self.service_name)
>
> If the method is called get_relation_endpoints under service_manager, it feels
> like a good idea to call it get_relation_endpoints here as well.
>
> 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_topology()
> +        machines = []
> +        for machine_id in topology.get_machines():
> +            # ugh, string manipulation
> +            machine_id = int(machine_id[-10:])
>
> s/get_machines/get_all_machine_states/, aligned with the function immediately above.
>
> Also, there's an _internal_id_to_id() function there.
>
> 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...

Read more...

lp:~bcsaller/pyjuju/ensemble-status updated
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

Revision history for this message
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: EnvironmentsConfigError undefined
>
> 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_id[-10:])
> 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 servicerelationstate, as its effectivly returning
> 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://code.launchpad.net/~bcsaller/ensemble/ensemble-status/+merge/45897
> You are the owner of lp:~bcsaller/ensemble/ensemble-status.
>

Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

The branch is still at revision 124, from the 24th. Can you please push the changes?

lp:~bcsaller/pyjuju/ensemble-status updated
136. By Benjamin Saller

remove yaml renderer, added renderers registry

Revision history for this message
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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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 """

Subscribers

People subscribed via source and target branches

to status/vote changes: