Merge lp:~bcsaller/pyjuju/subordinate-control-status into lp:pyjuju

Proposed by Benjamin Saller
Status: Merged
Approved by: Kapil Thangavelu
Approved revision: 523
Merged at revision: 515
Proposed branch: lp:~bcsaller/pyjuju/subordinate-control-status
Merge into: lp:pyjuju
Diff against target: 7069 lines (+3577/-3059)
12 files modified
juju/control/add_unit.py (+5/-0)
juju/control/deploy.py (+8/-4)
juju/control/remove_unit.py (+5/-0)
juju/control/status.py (+423/-157)
juju/control/tests/test_add_unit.py (+14/-0)
juju/control/tests/test_deploy.py (+27/-0)
juju/control/tests/test_remove_unit.py (+12/-0)
juju/control/tests/test_status.py (+165/-44)
juju/state/service.py (+7/-1)
juju/state/tests/test_charm.py (+19/-3)
juju/state/tests/test_service.py (+2891/-2850)
juju/unit/tests/test_lifecycle.py (+1/-0)
To merge this branch: bzr merge lp:~bcsaller/pyjuju/subordinate-control-status
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+100489@code.launchpad.net

Description of the change

juju/control should be aware of subordinates

Includes support for add-unit, deploy, status, remove-unit

https://codereview.appspot.com/5970068/

To post a comment you must log in.
Revision history for this message
Benjamin Saller (bcsaller) wrote :

Reviewers: mp+100489_code.launchpad.net,

Message:
Please take a look.

Description:
juju/control should be aware of subordinates

Includes support for add-unit, deploy, status, remove-unit

https://code.launchpad.net/~bcsaller/juju/subordinate-control-status/+merge/100489

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/5970068/

Affected files:
   A [revision details]
   M juju/control/add_unit.py
   M juju/control/deploy.py
   M juju/control/remove_unit.py
   M juju/control/status.py
   M juju/control/tests/test_add_unit.py
   M juju/control/tests/test_deploy.py
   M juju/control/tests/test_remove_unit.py
   M juju/control/tests/test_status.py
   M juju/state/tests/test_charm.py
   M juju/unit/tests/test_lifecycle.py

Revision history for this message
Benjamin Saller (bcsaller) wrote :
523. By Benjamin Saller

better comments

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

This looks good. The refactoring of status isn't complete though.. it
needs some more docs, this area of the codebase is frequently touched,
and it needs more introduction to what the responsibility of the various
methods are, esp. given their use of shared instance state variables in
a functional style.

https://codereview.appspot.com/5970068/diff/2002/juju/control/status.py
File juju/control/status.py (right):

https://codereview.appspot.com/5970068/diff/2002/juju/control/status.py#newcode136
juju/control/status.py:136: s = StatusCommand(client, provider, log)
it shouldn't exist if its only purpose is for existing tests.. i'd just
update the tests to the new api.. that's part of the refactoring, else
its just dead code.

https://codereview.appspot.com/5970068/diff/2002/juju/control/status.py#newcode155
juju/control/status.py:155: self.service_manager =
ServiceStateManager(client)
these where documented in a self referential way post the diff, but
saying 'service_name' to 'state' isn't very clear, if its referring to
the actual state object (unlikely), or what structure the dictionary
termed state has.

https://codereview.appspot.com/5970068/diff/2002/juju/control/status.py#newcode252
juju/control/status.py:252: def _process_units(self, service, relations,
rel_svc_map):
this should have some docs, what are the params

https://codereview.appspot.com/5970068/diff/2002/juju/control/status.py#newcode271
juju/control/status.py:271: u = self.unit_data[unit.unit_name] = dict()
this method could use some documentation, what are the params

https://codereview.appspot.com/5970068/diff/2002/juju/control/status.py#newcode307
juju/control/status.py:307: relation_data =
self.service_data[service.service_name]["relations"]
please document the return value.

https://codereview.appspot.com/5970068/diff/2002/juju/control/status.py#newcode339
juju/control/status.py:339: relation_data =
self.service_data[service.service_name]["relations"]
please document what the method is doing..

ie. inspects all the relations of a service and...?

https://codereview.appspot.com/5970068/

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

https://codereview.appspot.com/5970068/diff/2002/juju/control/tests/test_status.py
File juju/control/tests/test_status.py (right):

https://codereview.appspot.com/5970068/diff/2002/juju/control/tests/test_status.py#newcode788
juju/control/tests/test_status.py:788: state = yield
self.build_topology()
this method is a crutch, its creating way more stuff than what is
actually tested for. its better to simplify and model an api for
creation of whats needed, instead of relying on a megamethod to build
world every test.

i'd like to see a test here of removing a primary unit with a
subordinate attached, as that broke status on my previous usage of this
feature.

https://codereview.appspot.com/5970068/

524. By Benjamin Saller

move status changes to proper branch, added docs, moved collect method to test module

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

Latest changes look good, time to merge.

525. By Benjamin Saller

wip

526. By Benjamin Saller

handle case where principal unit is removed

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'juju/control/add_unit.py'
2--- juju/control/add_unit.py 2012-03-29 13:40:45 +0000
3+++ juju/control/add_unit.py 2012-04-05 21:24:20 +0000
4@@ -4,6 +4,7 @@
5
6 from juju.control import legacy
7 from juju.control.utils import get_environment, sync_environment_state
8+from juju.errors import JujuError
9 from juju.state.placement import place_unit
10 from juju.state.service import ServiceStateManager
11
12@@ -51,6 +52,10 @@
13
14 service_manager = ServiceStateManager(client)
15 service_state = yield service_manager.get_service_state(service_name)
16+ if (yield service_state.is_subordinate()):
17+ raise JujuError("Subordinate services acquire units from "
18+ "their principal service.")
19+
20 for i in range(num_units):
21 unit_state = yield service_state.add_unit_state()
22 yield place_unit(client, placement_policy, unit_state)
23
24=== modified file 'juju/control/deploy.py'
25--- juju/control/deploy.py 2012-03-29 13:40:45 +0000
26+++ juju/control/deploy.py 2012-04-05 21:24:20 +0000
27@@ -37,7 +37,7 @@
28 sub_parser.add_argument(
29 "--repository",
30 help="Directory for charm lookup and retrieval",
31- default=os.environ.get('JUJU_REPOSITORY'),
32+ default=os.environ.get("JUJU_REPOSITORY"),
33 type=expand_path)
34
35 sub_parser.add_argument(
36@@ -170,9 +170,13 @@
37 yield state.write()
38
39 # Create desired number of service units
40- for i in xrange(num_units):
41- unit_state = yield service_state.add_unit_state()
42- yield place_unit(client, placement_policy, unit_state)
43+ if (yield service_state.is_subordinate()):
44+ log.info("Subordinate %r awaiting relationship "
45+ "to principal for deployment.", service_name)
46+ else:
47+ for i in xrange(num_units):
48+ unit_state = yield service_state.add_unit_state()
49+ yield place_unit(client, placement_policy, unit_state)
50
51 # Check if we have any peer relations to establish
52 if charm.metadata.peers:
53
54=== modified file 'juju/control/remove_unit.py'
55--- juju/control/remove_unit.py 2011-11-29 22:50:38 +0000
56+++ juju/control/remove_unit.py 2012-04-05 21:24:20 +0000
57@@ -2,6 +2,7 @@
58
59 from twisted.internet.defer import inlineCallbacks
60
61+from juju.errors import JujuError
62 from juju.state.service import ServiceStateManager, parse_service_name
63 from juju.control.utils import get_environment
64
65@@ -39,6 +40,10 @@
66 service_name = parse_service_name(unit_name)
67 service_state = yield service_manager.get_service_state(
68 service_name)
69+ if (yield service_state.is_subordinate()):
70+ raise JujuError("Subordinate services acquire units from "
71+ "their principal service.")
72+
73 unit_state = yield service_state.get_unit_state(unit_name)
74 yield service_state.remove_unit_state(unit_state)
75 log.info("Unit %r removed from service %r",
76
77=== modified file 'juju/control/status.py'
78--- juju/control/status.py 2012-03-29 02:50:29 +0000
79+++ juju/control/status.py 2012-04-05 21:24:20 +0000
80@@ -7,12 +7,11 @@
81 from twisted.internet.defer import inlineCallbacks, returnValue
82 import yaml
83
84-from juju.errors import ProviderError
85 from juju.environment.errors import EnvironmentsConfigError
86 from juju.state.errors import UnitRelationStateNotFound
87 from juju.state.charm import CharmStateManager
88 from juju.state.machine import MachineStateManager
89-from juju.state.service import ServiceStateManager
90+from juju.state.service import ServiceStateManager, parse_service_name
91 from juju.state.relation import RelationStateManager
92 from juju.unit.workflow import WorkflowStateClient
93
94@@ -96,7 +95,9 @@
95 client = yield provider.connect()
96 try:
97 # Collect status information
98- state = yield collect(scope, provider, client, log)
99+ command = StatusCommand(client, provider, log)
100+ state = yield command(scope)
101+ #state = yield collect(scope, provider, client, log)
102 finally:
103 yield client.close()
104 # Render
105@@ -125,60 +126,222 @@
106 return (services, units)
107
108
109-@inlineCallbacks
110-def collect(scope, machine_provider, client, log):
111- """Extract status information into nested dicts for rendering.
112-
113- `scope`: an optional list of name specifiers. Globbing based
114- wildcards supported. Defaults to all units, services and
115- relations.
116-
117- `machine_provider`: machine provider for the environment
118-
119- `client`: ZK client connection
120-
121- `log`: a Python stdlib logger.
122- """
123- service_manager = ServiceStateManager(client)
124- relation_manager = RelationStateManager(client)
125- machine_manager = MachineStateManager(client)
126- charm_manager = CharmStateManager(client)
127-
128- service_data = {}
129- machine_data = {}
130- state = dict(services=service_data, machines=machine_data)
131-
132- seen_machines = set()
133- filter_services, filter_units = digest_scope(scope)
134-
135- services = yield service_manager.get_all_service_states()
136- for service in services:
137- if len(filter_services):
138- found = False
139- for filter_service in filter_services:
140- if fnmatch(service.service_name, filter_service):
141- found = True
142- break
143- if not found:
144- continue
145-
146- unit_data = {}
147+class StatusCommand(object):
148+ def __init__(self, client, provider, log):
149+ """
150+ Callable status command object.
151+
152+ `client`: ZK client connection
153+ `provider`: machine provider for the environment
154+ `log`: a Python stdlib logger.
155+
156+ """
157+ self.client = client
158+ self.provider = provider
159+ self.log = log
160+
161+ self.service_manager = ServiceStateManager(client)
162+ self.relation_manager = RelationStateManager(client)
163+ self.machine_manager = MachineStateManager(client)
164+ self.charm_manager = CharmStateManager(client)
165+ self._reset()
166+
167+ def _reset(self, scope=None):
168+ # init per-run state
169+ # self.state is assembled by the various process methods
170+ # intermediate access to state is made more convenient
171+ # using these references to its internals.
172+ self.service_data = {} # service name: service info
173+ self.machine_data = {} # machine id: machine state
174+ self.unit_data = {} # unit_name :unit_info
175+
176+ # used in collecting subordinate (which are added to state in a two
177+ # phase pass)
178+ self.subordinates = {} # service : set(principal service names)
179+
180+ self.state = dict(services=self.service_data,
181+ machines=self.machine_data)
182+
183+ # Filtering info
184+ self.seen_machines = set()
185+ self.filter_services, self.filter_units = digest_scope(scope)
186+
187+ @inlineCallbacks
188+ def __call__(self, scope=None):
189+ """Extract status information into nested dicts for rendering.
190+
191+ `scope`: an optional list of name specifiers. Globbing based wildcards
192+ supported. Defaults to all units, services and relations.
193+
194+ """
195+ self._reset(scope)
196+
197+ # Pass 1 Gather Data (including principals and subordinates)
198+ # this builds unit info and container relationships
199+ # which is assembled in pass 2 below
200+ yield self._process_services()
201+
202+ # Pass 2: Nest information according to principal/subordinates
203+ # rules
204+ self._process_subordinates()
205+
206+ yield self._process_machines()
207+
208+ returnValue(self.state)
209+
210+ @inlineCallbacks
211+ def _process_services(self):
212+ """
213+ For each service gather the following information::
214+
215+ <service name>:
216+ charm: <charm name>
217+ exposed: <expose boolean>
218+ relations:
219+ <relation info -- see _process_relations>
220+ units:
221+ <unit info -- see _process_units>
222+ """
223+ services = yield self.service_manager.get_all_service_states()
224+ for service in services:
225+ if len(self.filter_services):
226+ found = False
227+ for filter_service in self.filter_services:
228+ if fnmatch(service.service_name, filter_service):
229+ found = True
230+ break
231+ if not found:
232+ continue
233+ yield self._process_service(service)
234+
235+ @inlineCallbacks
236+ def _process_service(self, service):
237+ """
238+ Gather the service info (described in _process_services).
239+
240+ `service`: ServiceState instance
241+ """
242+
243 relation_data = {}
244+ service_data = self.service_data
245
246 charm_id = yield service.get_charm_id()
247- charm = yield charm_manager.get_charm_state(charm_id)
248-
249- service_data[service.service_name] = dict(units=unit_data,
250- charm=charm.id,
251- relations=relation_data)
252- exposed = yield service.get_exposed_flag()
253- if exposed:
254- service_data[service.service_name].update(exposed=exposed)
255-
256+ charm = yield self.charm_manager.get_charm_state(charm_id)
257+
258+ service_data[service.service_name] = (
259+ dict(units={},
260+ charm=charm.id,
261+ relations=relation_data))
262+
263+ if (yield service.is_subordinate()):
264+ service_data[service.service_name]["subordinate"] = True
265+
266+ yield self._process_expose(service)
267+
268+ relations, rel_svc_map = yield self._process_relation_map(
269+ service)
270+
271+ unit_matched = yield self._process_units(service,
272+ relations,
273+ rel_svc_map)
274+
275+ # after filtering units check if any matched or remove the
276+ # service from the output
277+ if self.filter_units and not unit_matched:
278+ del service_data[service.service_name]
279+ return
280+
281+ yield self._process_relations(service, relations, rel_svc_map)
282+
283+ @inlineCallbacks
284+ def _process_units(self, service, relations, rel_svc_map):
285+ """
286+ Gather unit information for a service::
287+
288+ <unit name>:
289+ agent-state: <started|pendding|etc>
290+ machine: <machine id>
291+ open-ports: ["port/protocol", ...]
292+ public-address: <public dns name or ip>
293+ subordinates:
294+ <optional nested units of subordinate services>
295+
296+
297+ `service`: ServiceState intance
298+ `relations`: list of ServiceRelationState instance for this service
299+ `rel_svc_map`: maps relation internal ids to the remote endpoint
300+ service name. This references the name of the remote
301+ endpoint and so is generated per service.
302+ """
303 units = yield service.get_all_unit_states()
304 unit_matched = False
305
306- relations = yield relation_manager.get_relations_for_service(service)
307+ for unit in units:
308+ if len(self.filter_units):
309+ found = False
310+ for filter_unit in self.filter_units:
311+ if fnmatch(unit.unit_name, filter_unit):
312+ found = True
313+ break
314+ if not found:
315+ continue
316+ yield self._process_unit(service, unit, relations, rel_svc_map)
317+ unit_matched = True
318+ returnValue(unit_matched)
319+
320+ @inlineCallbacks
321+ def _process_unit(self, service, unit, relations, rel_svc_map):
322+ """ Generate unit info for a single unit of a single service.
323+
324+ `unit`: ServiceUnitState
325+ see `_process_units` for an explanation of other arguments.
326+
327+ """
328+ u = self.unit_data[unit.unit_name] = dict()
329+ container = yield unit.get_container()
330+
331+ if container:
332+ u["container"] = container.unit_name
333+ self.subordinates.setdefault(unit.service_name,
334+ set()).add(container.service_name)
335+
336+ machine_id = yield unit.get_assigned_machine_id()
337+ u["machine"] = machine_id
338+ unit_workflow_client = WorkflowStateClient(self.client, unit)
339+ unit_state = yield unit_workflow_client.get_state()
340+ if not unit_state:
341+ u["agent-state"] = "pending"
342+ else:
343+ unit_connected = yield unit.has_agent()
344+ u["agent-state"] = unit_state.replace("_", "-") \
345+ if unit_connected else "down"
346+
347+ exposed = self.service_data[service.service_name].get("exposed")
348+ if exposed:
349+ open_ports = yield unit.get_open_ports()
350+ u["open-ports"] = ["{port}/{proto}".format(**port_info)
351+ for port_info in open_ports]
352+
353+ u["public-address"] = yield unit.get_public_address()
354+
355+ # indicate we should include information about this
356+ # machine later
357+ self.seen_machines.add(machine_id)
358+
359+ # collect info on each relation for the service unit
360+ yield self._process_unit_relations(service, unit,
361+ relations, rel_svc_map)
362+
363+ @inlineCallbacks
364+ def _process_relation_map(self, service):
365+ """Generate a mapping from a services relations to the service name of
366+ the remote endpoints.
367+
368+ returns: ([ServiceRelationState, ...], mapping)
369+ """
370+ relation_data = self.service_data[service.service_name]["relations"]
371+ relation_mgr = self.relation_manager
372+ relations = yield relation_mgr.get_relations_for_service(service)
373 rel_svc_map = {}
374
375 for relation in relations:
376@@ -199,107 +362,182 @@
377 "than 2 endpoints")
378
379 rel_service = rel_services[0]
380+ relation_data.setdefault(relation.relation_name, set()).add(
381+ rel_service.service_name)
382+ rel_svc_map[relation.internal_relation_id] = (
383+ rel_service.service_name)
384+
385+ returnValue((relations, rel_svc_map))
386+
387+ @inlineCallbacks
388+ def _process_relations(self, service, relations, rel_svc_map):
389+ """Generate relation information for a given service
390+
391+ Each service with relations will have a relations dict
392+ nested under it with one or more relations described::
393+
394+ relations:
395+ <relation name>:
396+ - <remote service name>
397+
398+ """
399+ relation_data = self.service_data[service.service_name]["relations"]
400+
401+ for relation in relations:
402+ rel_services = yield relation.get_service_states()
403+
404+ # A single related service implies a peer relation. More
405+ # imply a bi-directional provides/requires relationship.
406+ # In the later case we omit the local side of the relation
407+ # when reporting.
408+ if len(rel_services) > 1:
409+ # Filter out self from multi-service relations.
410+ rel_services = [
411+ rsn for rsn in rel_services if rsn.service_name !=
412+ service.service_name]
413+
414+ if len(rel_services) > 1:
415+ raise ValueError("Unexpected relationship with more "
416+ "than 2 endpoints")
417+
418+ rel_service = rel_services[0]
419 relation_data.setdefault(
420- relation.relation_name, set()).add(rel_service.service_name)
421- rel_svc_map[relation.internal_relation_id] = \
422- rel_service.service_name
423+ relation.relation_name, set()).add(
424+ rel_service.service_name)
425+ rel_svc_map[relation.internal_relation_id] = (
426+ rel_service.service_name)
427
428 # Normalize the sets back to lists
429 for r in relation_data:
430- relation_data[r] = list(relation_data[r])
431-
432- for unit in units:
433- if len(filter_units):
434- found = False
435- for filter_unit in filter_units:
436- if fnmatch(unit.unit_name, filter_unit):
437- found = True
438- break
439- if not found:
440- continue
441-
442- u = unit_data[unit.unit_name] = dict()
443- machine_id = yield unit.get_assigned_machine_id()
444- u["machine"] = machine_id
445- unit_workflow_client = WorkflowStateClient(client, unit)
446- unit_state = yield unit_workflow_client.get_state()
447- if not unit_state:
448- u["agent-state"] = "pending"
449+ relation_data[r] = sorted(relation_data[r])
450+
451+ @inlineCallbacks
452+ def _process_unit_relations(self, service, unit, relations, rel_svc_map):
453+ """Collect UnitRelationState information per relation and per unit.
454+
455+ Includes information under each unit for its relations including
456+ its relation state and information about any possible errors.
457+
458+ see `_process_relations` for argument information
459+ """
460+ u = self.unit_data[unit.unit_name]
461+ relation_errors = {}
462+
463+ for relation in relations:
464+ try:
465+ relation_unit = yield relation.get_unit_state(unit)
466+ except UnitRelationStateNotFound:
467+ # This exception will occur when relations are
468+ # established between services without service
469+ # units, and therefore never have any
470+ # corresponding service relation units.
471+ # UPDATE: common with subordinate services, and
472+ # some testing scenarios.
473+ continue
474+ relation_workflow_client = WorkflowStateClient(
475+ self.client, relation_unit)
476+ workflow_state = yield relation_workflow_client.get_state()
477+
478+ rel_svc_name = rel_svc_map.get(relation.internal_relation_id)
479+ if rel_svc_name and workflow_state not in ("up", None):
480+ relation_errors.setdefault(
481+ relation.relation_name, set()).add(rel_svc_name)
482+
483+ if relation_errors:
484+ # Normalize sets and store.
485+ u["relation-errors"] = dict(
486+ [(r, sorted(relation_errors[r])) for r in relation_errors])
487+
488+ def _process_subordinates(self):
489+ """Properly nest subordinate units under their principal service's
490+ unit nodes. Services and units are generated in one pass, then
491+ iterated by this method to structure the output data to reflect
492+ actual unit containment.
493+
494+ Subordinate units will include the follow::
495+ subordinate: true
496+ subordinate-to:
497+ - <principal service names>
498+
499+ Principal services that have subordinates will include::
500+
501+ subordinates:
502+ <subordinate unit name>:
503+ agent-state: <agent state>
504+ """
505+ service_data = self.service_data
506+
507+ for unit_name, u in self.unit_data.iteritems():
508+ container = u.get("container")
509+ if container:
510+ d = self.unit_data[container].setdefault("subordinates", {})
511+ d[unit_name] = u
512+
513+ # remove keys that don't appear in output or come from container
514+ for key in ("container", "machine", "public-address"):
515+ u.pop(key, None)
516 else:
517- unit_connected = yield unit.has_agent()
518- u["agent-state"] = unit_state.replace("_", "-") \
519- if unit_connected else "down"
520- if exposed:
521- open_ports = yield unit.get_open_ports()
522- u["open-ports"] = ["{port}/{proto}".format(**port_info)
523- for port_info in open_ports]
524-
525- u["public-address"] = yield unit.get_public_address()
526-
527- # indicate we should include information about this
528- # machine later
529- seen_machines.add(machine_id)
530- unit_matched = True
531-
532- # collect info on each relation for the service unit
533- relation_errors = {}
534- for relation in relations:
535- try:
536- relation_unit = yield relation.get_unit_state(unit)
537- except UnitRelationStateNotFound:
538- # This exception will occur when relations are
539- # established between services without service
540- # units, and therefore never have any
541- # corresponding service relation units.
542- # UPDATE: common with subordinate services, and
543- # some testing scenarios.
544- continue
545- relation_workflow_client = WorkflowStateClient(
546- client, relation_unit)
547- workflow_state = yield relation_workflow_client.get_state()
548-
549- rel_svc_name = rel_svc_map.get(relation.internal_relation_id)
550- if rel_svc_name and workflow_state not in ("up", None):
551- relation_errors.setdefault(
552- relation.relation_name, set()).add(rel_svc_name)
553-
554- if relation_errors:
555- # Normalize sets and store.
556- u["relation-errors"] = dict(
557- [(r, list(relation_errors[r])) for r in relation_errors])
558-
559- # after filtering units check if any matched or remove the
560- # service from the output
561- if filter_units and not unit_matched:
562- del service_data[service.service_name]
563- continue
564-
565- machines = yield machine_manager.get_all_machine_states()
566- for machine_state in machines:
567- if (filter_services or filter_units) and \
568- machine_state.id not in seen_machines:
569- continue
570-
571+ service_name = parse_service_name(unit_name)
572+ service_data[service_name]["units"][unit_name] = u
573+
574+ for sub_service, principal_services in self.subordinates.iteritems():
575+ service_data[sub_service]["subordinate-to"] = sorted(principal_services)
576+ service_data[sub_service].pop("units", None)
577+
578+ @inlineCallbacks
579+ def _process_expose(self, service):
580+ """Indicate if a service is exposed or not."""
581+ exposed = yield service.get_exposed_flag()
582+ if exposed:
583+ self.service_data[service.service_name].update(exposed=exposed)
584+ returnValue(exposed)
585+
586+ @inlineCallbacks
587+ def _process_machines(self):
588+ """Gather machine information.
589+
590+ machines:
591+ <machine id>:
592+ agent-state: <agent state>
593+ dns-name: <dns name>
594+ instance-id: <provider specific instance id>
595+ instance-state: <instance state>
596+ """
597+
598+ machines = yield self.machine_manager.get_all_machine_states()
599+ for machine_state in machines:
600+ if (self.filter_services or self.filter_units) and \
601+ machine_state.id not in self.seen_machines:
602+ continue
603+ yield self._process_machine(machine_state)
604+
605+ @inlineCallbacks
606+ def _process_machine(self, machine_state):
607+ """
608+ `machine_state`: MachineState instance
609+ """
610 instance_id = yield machine_state.get_instance_id()
611 m = {"instance-id": instance_id \
612 if instance_id is not None else "pending"}
613 if instance_id is not None:
614 try:
615- pm = yield machine_provider.get_machine(instance_id)
616+ pm = yield self.provider.get_machine(instance_id)
617 m["dns-name"] = pm.dns_name
618 m["instance-state"] = pm.state
619 if (yield machine_state.has_agent()):
620 # if the agent's connected, we're fine
621 m["agent-state"] = "running"
622 else:
623- units = (yield machine_state.get_all_service_unit_states())
624+ units = (
625+ yield machine_state.get_all_service_unit_states())
626 for unit in units:
627 unit_workflow_client = WorkflowStateClient(
628- client, unit)
629+ self.client, unit)
630 if (yield unit_workflow_client.get_state()):
631- # for unit to have a state, its agent must have
632- # run, which implies the machine agent must have
633- # been running correctly at some point in the past
634+ # for unit to have a state, its agent must
635+ # have run, which implies the machine agent
636+ # must have been running correctly at some
637+ # point in the past
638 m["agent-state"] = "down"
639 break
640 else:
641@@ -307,13 +545,11 @@
642 m["agent-state"] = "not-started"
643 except ProviderError:
644 # The provider doesn't have machine information
645- log.error(
646+ self.log.error(
647 "Machine provider information missing: machine %s" % (
648 machine_state.id))
649
650- machine_data[machine_state.id] = m
651-
652- returnValue(state)
653+ self.machine_data[machine_state.id] = m
654
655
656 def render_yaml(data, filelike, environment):
657@@ -356,6 +592,15 @@
658 "shape": "box",
659 "style": "filled",
660 },
661+
662+ "subunit": {
663+ "color": "#c9c9c9",
664+ "fontcolor": "#ffffff",
665+ "shape": "box",
666+ "style": "filled",
667+ "rank": "same"
668+ },
669+
670 "relation": {
671 "dir": "none"}
672 }
673@@ -364,9 +609,10 @@
674 def safe_dot_label(name):
675 """Convert a name to a label safe for use in DOT.
676
677- Works around an issue where service names like wiki-db will
678- produce DOT items with names like cluster_wiki-db where the
679- trailing '-' invalidates the name.
680+ Works around an issue where service names like wiki-db will produce DOT
681+ items with names like cluster_wiki-db where the trailing '-' invalidates
682+ the name.
683+
684 """
685 return name.replace("-", "_")
686
687@@ -399,34 +645,54 @@
688 **style["service"])
689 cluster.add_node(snode)
690
691- for unit_name, unit in service["units"].iteritems():
692- un = pydot.Node(safe_dot_label(unit_name),
693- label="<%s<br/><i>%s</i>>" % (
694- unit_name,
695- unit.get("public-address")),
696- **style["unit"])
697- cluster.add_node(un)
698+ for unit_name, unit in service.get("units", {}).iteritems():
699+ subordinates = unit.get("subordinates")
700+ if subordinates:
701+ container = pydot.Subgraph()
702+ un = pydot.Node(safe_dot_label(unit_name),
703+ label="<%s<br/><i>%s</i>>" % (
704+ unit_name,
705+ unit.get("public-address")),
706+ **style["unit"])
707+ container.add_node(un)
708+ for sub in subordinates:
709+ s = pydot.Node(safe_dot_label(sub),
710+ label="<%s<br/>>" % (sub),
711+ **style["subunit"])
712+ container.add_node(s)
713+ container.add_edge(pydot.Edge(un, s, **style["relation"]))
714+ cluster.add_subgraph(container)
715+ else:
716+ un = pydot.Node(safe_dot_label(unit_name),
717+ label="<%s<br/><i>%s</i>>" % (
718+ unit_name,
719+ unit.get("public-address")),
720+ **style["unit"])
721+ cluster.add_node(un)
722
723 cluster.add_edge(pydot.Edge(snode, un))
724
725 dot.add_subgraph(cluster)
726
727 # now map the relationships
728- for kind, relations in service["relations"].iteritems():
729- for relation in relations:
730- src = safe_dot_label(relation)
731+ for kind, relation in service["relations"].iteritems():
732+ if not isinstance(relation, list):
733+ relation = (relation,)
734+ for rel in relation:
735+ src = safe_dot_label(rel)
736 dest = safe_dot_label(service_name)
737- descriptor = tuple(sorted((src, dest)))
738+ descriptor = ":".join(tuple(sorted((src, dest))))
739+ kind = safe_dot_label("%s/%s" % (descriptor, kind))
740 if descriptor not in seen_relations:
741 seen_relations.add(descriptor)
742 dot.add_edge(pydot.Edge(
743- src,
744- dest,
745- label=kind,
746- **style["relation"]
747- ))
748+ src,
749+ dest,
750+ label=kind,
751+ **style["relation"]
752+ ))
753
754- if format == 'dot':
755+ if format == "dot":
756 filelike.write(dot.to_string())
757 else:
758 filelike.write(dot.create(format=format))
759
760=== modified file 'juju/control/tests/test_add_unit.py'
761--- juju/control/tests/test_add_unit.py 2012-03-29 13:40:45 +0000
762+++ juju/control/tests/test_add_unit.py 2012-04-05 21:24:20 +0000
763@@ -97,6 +97,19 @@
764 "Service 'volcano' was not found", self.stderr.getvalue())
765
766 @inlineCallbacks
767+ def test_add_unit_subordinate_service(self):
768+ yield self.add_service_from_charm("logging")
769+ finished = self.setup_cli_reactor()
770+ self.setup_exit(0)
771+ self.mocker.replay()
772+ main(["add-unit", "logging"])
773+ yield finished
774+ self.assertIn(
775+ "Subordinate services acquire units from "
776+ "their principal service.",
777+ self.stderr.getvalue())
778+
779+ @inlineCallbacks
780 def test_add_unit_reuses_machines(self):
781 """Verify that if machines are not in use, add-unit uses them."""
782 # add machine to wordpress, then destroy and reallocate later
783@@ -107,6 +120,7 @@
784 wordpress_machine_state = yield self.add_machine_state()
785 yield wordpress_unit_state.assign_to_machine(wordpress_machine_state)
786 yield wordpress_unit_state.unassign_from_machine()
787+
788 finished = self.setup_cli_reactor()
789 self.setup_exit(0)
790 self.mocker.replay()
791
792=== modified file 'juju/control/tests/test_deploy.py'
793--- juju/control/tests/test_deploy.py 2012-03-29 03:20:51 +0000
794+++ juju/control/tests/test_deploy.py 2012-04-05 21:24:20 +0000
795@@ -506,6 +506,33 @@
796 self.assertEqual(machine_id, 0)
797
798 @inlineCallbacks
799+
800+ def test_deploy_informs_with_subordinate(self):
801+ """Verify subordinate charm doesn't deploy.
802+
803+ And that it properly notifies the user.
804+ """
805+ log = self.capture_logging()
806+ finished = self.setup_cli_reactor()
807+ self.setup_exit(0)
808+ self.mocker.replay()
809+
810+ # missing config file
811+ main(["deploy",
812+ "--repository", self.unbundled_repo_path, "local:logging"])
813+ yield finished
814+ self.assertIn(
815+ "Subordinate 'logging' awaiting relationship to "
816+ "principal for deployment.\n",
817+ log.getvalue())
818+
819+ # and verify no units assigned to service
820+ service_state = yield self.service_state_manager.get_service_state("logging")
821+ self.assertEqual(service_state.service_name, "logging")
822+
823+ units = yield service_state.get_unit_names()
824+ self.assertEqual(units, [])
825+
826 def test_deploy_legacy_keys_in_legacy_env(self):
827 yield self.client.delete("/constraints")
828
829
830=== modified file 'juju/control/tests/test_remove_unit.py'
831--- juju/control/tests/test_remove_unit.py 2012-03-27 22:46:44 +0000
832+++ juju/control/tests/test_remove_unit.py 2012-04-05 21:24:20 +0000
833@@ -139,6 +139,18 @@
834 "Service 'volcano' was not found", self.stderr.getvalue())
835
836 @inlineCallbacks
837+ def test_remove_unit_with_subordinate(self):
838+ yield self.add_service_from_charm("logging")
839+ finished = self.setup_cli_reactor()
840+ self.setup_exit(0)
841+ self.mocker.replay()
842+ main(["remove-unit", "logging/0"])
843+ yield finished
844+ self.assertIn(
845+ "Subordinate services acquire units from their principal service.",
846+ self.stderr.getvalue())
847+
848+ @inlineCallbacks
849 def test_remove_unit_bad_parse(self):
850 """Verify that a bad service unit name results in an appropriate error.
851 """
852
853=== modified file 'juju/control/tests/test_status.py'
854--- juju/control/tests/test_status.py 2012-03-30 10:13:09 +0000
855+++ juju/control/tests/test_status.py 2012-04-05 21:24:20 +0000
856@@ -33,6 +33,17 @@
857 fp.close()
858
859
860+@inlineCallbacks
861+def collect(scope, provider, client, log):
862+ """Collect and return status info as dict"""
863+ # provided for backwards compatibility with
864+ # original API
865+ # used only in testing
866+ s = status.StatusCommand(client, provider, log)
867+ state = yield s(scope)
868+ returnValue(state)
869+
870+
871 class StatusTestBase(ServiceStateManagerTestBase, ControlToolTest):
872
873 # Status tests setup a large tree every time, make allowances for it.
874@@ -64,8 +75,7 @@
875 @inlineCallbacks
876 def add_relation_unit_states(self, relation_state, unit_states, states):
877 for unit_state, state in zip(unit_states, states):
878- relation_unit_state = yield relation_state.add_unit_state(
879- unit_state)
880+ relation_unit_state = yield relation_state.add_unit_state(unit_state)
881 workflow_client = ZookeeperWorkflowState(
882 self.client, relation_unit_state)
883 with (yield workflow_client.lock()):
884@@ -76,6 +86,7 @@
885 self,
886 source_endpoint, source_units, source_states,
887 dest_endpoint, dest_units, dest_states):
888+
889 relation_state, service_relation_states = \
890 yield self.relation_state_manager.add_relation_state(
891 *[source_endpoint, dest_endpoint])
892@@ -86,11 +97,17 @@
893 dest_relation_state, dest_units, dest_states)
894
895 @inlineCallbacks
896- def add_unit(self, service, machine, with_agent=lambda _: True):
897- unit = yield service.add_unit_state()
898- yield unit.assign_to_machine(machine)
899+ def add_unit(self, service, machine, with_agent=lambda _: True,
900+ units=None, container=None):
901+ unit = yield service.add_unit_state(container=container)
902+ self.assertTrue(machine or container)
903+ if machine is not None:
904+ yield unit.assign_to_machine(machine)
905 if with_agent(unit.unit_name):
906 yield unit.connect_agent()
907+ if units is not None:
908+ units.setdefault(service.service_name, []).append(unit)
909+
910 returnValue(unit)
911
912 @inlineCallbacks
913@@ -155,15 +172,19 @@
914 if fnmatch(name, pattern):
915 return False
916 return True
917- wpu = yield self.add_unit(wordpress, m1, with_unit)
918- myu1 = yield self.add_unit(mysql, m2, with_unit)
919- myu2 = yield self.add_unit(mysql, m3, with_unit)
920- vu1 = yield self.add_unit(varnish, m4, with_unit)
921- vu2 = yield self.add_unit(varnish, m5, with_unit)
922- mc1 = yield self.add_unit(memcache, m6, with_unit)
923- mc2 = yield self.add_unit(memcache, m7, with_unit)
924-
925- # add unit states to services and assign to machines
926+
927+
928+ units = {}
929+ wpu = yield self.add_unit(wordpress, m1, with_unit, units)
930+ myu1 = yield self.add_unit(mysql, m2, with_unit, units)
931+ myu2 = yield self.add_unit(mysql, m3, with_unit, units)
932+ vu1 = yield self.add_unit(varnish, m4, with_unit, units)
933+ vu2 = yield self.add_unit(varnish, m5, with_unit, units)
934+ mc1 = yield self.add_unit(memcache, m6, with_unit, units)
935+ mc2 = yield self.add_unit(memcache, m7, with_unit, units)
936+ state["units"] = units
937+
938+ # add unit states to services and assign to machines
939 # Set the lifecycle state and open ports, if any, for each unit state.
940 yield self.set_unit_state(wpu, "started", [(80, "tcp"), (443, "tcp")])
941 yield self.set_unit_state(myu1, "started")
942@@ -250,7 +271,7 @@
943 yield riak_u1_workflow.set_state("up")
944 yield peer_rel.add_unit_state(riak_u2)
945
946- state = yield status.collect(
947+ state = yield collect(
948 ["riak"], self.provider, self.client, None)
949 self.assertEqual(
950 state["services"]["riak"],
951@@ -295,15 +316,15 @@
952 mysql_ep, [mysql_1], ["up"],
953 teamblog_db_ep, [teamblog_1], ["up"])
954
955- state = yield status.collect(None, self.provider, self.client, None)
956- self.assertEqual(
957- state['services']['mysql']['units']['mysql/0'],
958- {'agent-state': 'pending',
959- 'machine': 0,
960- 'public-address': None})
961- self.assertEqual(
962- state['services']['mysql']['relations'],
963- {'db': ['myblog', 'teamblog']})
964+ state = yield collect(None, self.provider, self.client, None)
965+ self.assertEqual(
966+ state["services"]["mysql"]["units"]["mysql/0"],
967+ {"agent-state": "pending",
968+ "machine": 0,
969+ "public-address": None})
970+ self.assertEqual(
971+ state["services"]["mysql"]["relations"],
972+ {"db": ["myblog", "teamblog"]})
973
974 @inlineCallbacks
975 def test_service_with_multiple_rels_to_same_endpoint(self):
976@@ -330,18 +351,18 @@
977 mysql_ep, [mysql_1], ["down"],
978 read_db_ep, [myblog_1], ["up"])
979
980- state = yield status.collect(None, self.provider, self.client, None)
981+ state = yield collect(None, self.provider, self.client, None)
982 # Even though there are two relations to this service we
983 # collapse to one the additional displays are redundant.
984 self.assertEqual(
985- state['services']['mysql']['relations'],
986- {'db': ['myblog']})
987+ state["services"]["mysql"]["relations"],
988+ {"db": ["myblog"]})
989 self.assertEqual(
990- state['services']['mysql']['units']['mysql/0'],
991- {'agent-state': 'pending',
992- 'machine': 0,
993- 'relation-errors': {'db': ['myblog']},
994- 'public-address': None})
995+ state["services"]["mysql"]["units"]["mysql/0"],
996+ {"agent-state": "pending",
997+ "machine": 0,
998+ "relation-errors": {"db": ["myblog"]},
999+ "public-address": None})
1000
1001 @inlineCallbacks
1002 def test_collect(self):
1003@@ -359,7 +380,7 @@
1004 yield agent.start()
1005
1006 # collect everything
1007- state = yield status.collect(None, self.provider, self.client, None)
1008+ state = yield collect(None, self.provider, self.client, None)
1009 services = state["services"]
1010 self.assertIn("wordpress", services)
1011 self.assertIn("varnish", services)
1012@@ -404,7 +425,7 @@
1013 self.assertEqual(
1014 services["wordpress"],
1015 {"charm": "local:series/wordpress-3",
1016- "relations": {
1017+ "relations": {
1018 "cache": ["memcache"],
1019 "db": ["mysql"],
1020 "proxy": ["varnish"]},
1021@@ -436,7 +457,7 @@
1022 yield self.build_topology()
1023
1024 # collect by service name
1025- state = yield status.collect(
1026+ state = yield collect(
1027 ["wordpress"], self.provider, self.client, None)
1028 # Validate that only the expected service is present
1029 # in the state
1030@@ -444,13 +465,13 @@
1031 self.assertEqual(state["services"].keys(), ["wordpress"])
1032
1033 # collect by unit name
1034- state = yield status.collect(["*/0"], self.provider, self.client, None)
1035+ state = yield collect(["*/0"], self.provider, self.client, None)
1036 self.assertEqual(set(state["machines"].keys()), set([0, 1, 3, 5]))
1037 self.assertEqual(set(state["services"].keys()),
1038 set(["memcache", "varnish", "mysql", "wordpress"]))
1039
1040 # collect by unit name
1041- state = yield status.collect(["*/1"], self.provider, self.client, None)
1042+ state = yield collect(["*/1"], self.provider, self.client, None)
1043 self.assertEqual(set(state["machines"].keys()), set([2, 4, 6]))
1044
1045 # verify that only the proper units and services are present
1046@@ -489,13 +510,13 @@
1047 }}}})
1048
1049 # filter a missing service
1050- state = yield status.collect(
1051+ state = yield collect(
1052 ["cluehammer"], self.provider, self.client, None)
1053 self.assertEqual(set(state["machines"].keys()), set([]))
1054 self.assertEqual(set(state["services"].keys()), set([]))
1055
1056 # filter a missing unit
1057- state = yield status.collect(["*/7"], self.provider, self.client, None)
1058+ state = yield collect(["*/7"], self.provider, self.client, None)
1059 self.assertEqual(set(state["machines"].keys()), set([]))
1060 self.assertEqual(set(state["services"].keys()), set([]))
1061
1062@@ -512,7 +533,7 @@
1063 yield unit.unassign_from_machine()
1064 yield unit.set_public_address(None)
1065 # test that the machine is in state information w/o assignment
1066- state = yield status.collect(None, self.provider, self.client, None)
1067+ state = yield collect(None, self.provider, self.client, None)
1068 # verify that the unassigned machine appears in the state
1069 self.assertEqual(state["machines"][machine_id],
1070 {"dns-name": "steamcloud-1.com",
1071@@ -544,7 +565,7 @@
1072 yield wordpress.remove_unit_state(unit)
1073
1074 # test that wordpress has no assigned service units
1075- state = yield status.collect(None, self.provider, self.client, None)
1076+ state = yield collect(None, self.provider, self.client, None)
1077 self.assertEqual(
1078 state["services"]["wordpress"],
1079 {"charm": "local:series/wordpress-3",
1080@@ -576,7 +597,7 @@
1081 yield wpu.assign_to_machine(m8)
1082
1083 # test that we identify we don't have machine state
1084- state = yield status.collect(
1085+ state = yield collect(
1086 None, self.provider, self.client, logging.getLogger())
1087 self.assertEqual(state["machines"][7]["instance-id"],
1088 "pending")
1089@@ -715,13 +736,16 @@
1090 self.assertIn("mysql -> wordpress", result)
1091
1092 # assert that properties were applied to a relationship
1093- self.assertIn("wordpress -> varnish [dir=none, label=proxy]",
1094+ self.assertIn("wordpress -> varnish [dir=none, "
1095+ "label=\"varnish:wordpress/proxy\"]",
1096 result)
1097
1098 # verify that the renderer picked up the DNS name of the
1099 # machines (and they are associated with the proper machine)
1100 self.assertIn(
1101- '"mysql/0" [color="#DD4814", fontcolor="#ffffff", shape=box, style=filled, label=<mysql/0<br/><i>mysql-0.example.com</i>>]',
1102+ '"mysql/0" [color="#DD4814", fontcolor="#ffffff", '
1103+ "shape=box, style=filled, label=<mysql/0<br/><i>mysql-0."
1104+ "example.com</i>>]",
1105 result)
1106 self.assertIn(
1107 '"mysql/1" [color="#DD4814", fontcolor="#ffffff", shape=box, style=filled, label=<mysql/1<br/><i>mysql-1.example.com</i>>]',
1108@@ -771,3 +795,100 @@
1109
1110 # look for a hint the process completed.
1111 self.assertIn("</svg>", self.output.getvalue())
1112+
1113+ @inlineCallbacks
1114+ def test_subordinate_status_output(self):
1115+ state = yield self.build_topology()
1116+ # supplement status with additional subordinates
1117+ # add logging to mysql and wordpress
1118+ logging = yield self.add_service_from_charm("logging")
1119+
1120+ mysql_ep = RelationEndpoint("mysql", "client-server",
1121+ "juju-info", "server")
1122+ wordpress_db_ep = RelationEndpoint("wordpress", "client-server",
1123+ "juju-info", "server")
1124+ logging_ep = RelationEndpoint("logging", "client-server",
1125+ "juju-info", "client", "container")
1126+
1127+ my_log_rel, my_log_services = (
1128+ yield self.relation_state_manager.add_relation_state(
1129+ mysql_ep, logging_ep))
1130+ wp_log_rel, wp_log_services = (
1131+ yield self.relation_state_manager.add_relation_state(
1132+ wordpress_db_ep, logging_ep))
1133+
1134+ units = state["units"]
1135+ log_units = units.setdefault("logging", {})
1136+ wp1 = iter(units["wordpress"]).next()
1137+ mu1, mu2 = list(units["mysql"])
1138+
1139+ yield self.add_unit(logging, None, container=mu1, units=log_units)
1140+ yield self.add_unit(logging, None, container=wp1, units=log_units)
1141+ yield self.add_unit(logging, None, container=mu2, units=log_units)
1142+
1143+ self.mock_environment()
1144+ self.mocker.replay()
1145+
1146+ yield status.status(self.environment, [],
1147+ status.render_yaml, self.output, None)
1148+
1149+ state = yaml.load(self.output.getvalue())
1150+
1151+ # verify our changes
1152+ log_state = state["services"]["logging"]
1153+
1154+ self.assertEqual(set(log_state["relations"]["juju-info"]),
1155+ set(["mysql", "wordpress"]))
1156+ self.assertEqual(set(log_state["subordinate-to"]),
1157+ set(["mysql", "wordpress"]))
1158+
1159+ wp_state = state["services"]["wordpress"]
1160+ self.assertEqual(wp_state["relations"]["juju-info"], ["logging"])
1161+ wp_subs = wp_state["units"]["wordpress/0"]["subordinates"]
1162+ logging_sub = wp_subs["logging/1"]
1163+ # this assertion verifies that we don't see keys we don't
1164+ # expect as well
1165+ self.assertEqual(logging_sub, {"agent-state": "pending"})
1166+
1167+ @inlineCallbacks
1168+ def test_subordinate_status_output_no_container(self):
1169+ state = yield self.build_topology()
1170+ # supplement status with additional subordinates
1171+ # add logging to mysql and wordpress
1172+ logging = yield self.add_service_from_charm("logging")
1173+
1174+ mysql_ep = RelationEndpoint("mysql", "client-server",
1175+ "juju-info", "server")
1176+ wordpress_db_ep = RelationEndpoint("wordpress", "client-server",
1177+ "juju-info", "server")
1178+ logging_ep = RelationEndpoint("logging", "client-server",
1179+ "juju-info", "client", "container")
1180+
1181+ my_log_rel, my_log_services = (
1182+ yield self.relation_state_manager.add_relation_state(
1183+ mysql_ep, logging_ep))
1184+ wp_log_rel, wp_log_services = (
1185+ yield self.relation_state_manager.add_relation_state(
1186+ wordpress_db_ep, logging_ep))
1187+
1188+ units = state["units"]
1189+ log_units = units.setdefault("logging", {})
1190+ wp1 = iter(units["wordpress"]).next()
1191+ mu1, mu2 = list(units["mysql"])
1192+
1193+ yield self.add_unit(logging, None, container=mu1, units=log_units)
1194+ yield self.add_unit(logging, None, container=wp1, units=log_units)
1195+ yield self.add_unit(logging, None, container=mu2, units=log_units)
1196+
1197+ # remove mysql/0
1198+ yield state["services"]["mysql"].remove_unit_state(mu1)
1199+
1200+ self.mock_environment()
1201+ self.mocker.replay()
1202+
1203+ yield status.status(self.environment, [],
1204+ status.render_yaml, self.output, None)
1205+
1206+ output = yaml.load(self.output.getvalue())
1207+ self.assertNotIn(mu1.unit_name, output["services"]["mysql"]["units"])
1208+ self.assertIn(mu2.unit_name, output["services"]["mysql"]["units"])
1209
1210=== modified file 'juju/state/service.py'
1211--- juju/state/service.py 2012-03-30 10:13:09 +0000
1212+++ juju/state/service.py 2012-04-05 21:24:20 +0000
1213@@ -23,6 +23,7 @@
1214 from juju.state.charm import CharmStateManager
1215 from juju.state.relation import ServiceRelationState, RelationStateManager
1216 from juju.state.machine import _public_machine_id, MachineState
1217+from juju.state.topology import InternalTopologyError
1218 from juju.state.utils import (
1219 remove_tree, dict_merge, YAMLState, YAMLStateNodeMixin)
1220
1221@@ -866,7 +867,12 @@
1222
1223 """
1224 topology = yield self._read_topology()
1225- container_info = topology.get_service_unit_container(self.internal_id)
1226+ try:
1227+ container_info = topology.get_service_unit_container(
1228+ self.internal_id)
1229+ except InternalTopologyError:
1230+ returnValue(None)
1231+
1232 if container_info is None:
1233 returnValue(None)
1234
1235
1236=== modified file 'juju/state/tests/test_charm.py'
1237--- juju/state/tests/test_charm.py 2012-03-16 10:11:36 +0000
1238+++ juju/state/tests/test_charm.py 2012-04-05 21:24:20 +0000
1239@@ -1,17 +1,19 @@
1240+import os
1241+import shutil
1242+
1243 import yaml
1244
1245 from twisted.internet.defer import inlineCallbacks
1246
1247 from juju.charm.directory import CharmDirectory
1248 from juju.charm.tests import local_charm_id
1249+from juju.charm.tests.test_directory import sample_directory
1250 from juju.charm.tests.test_repository import unbundled_repository
1251+
1252 from juju.state.charm import CharmStateManager
1253 from juju.state.errors import CharmStateNotFound
1254 from juju.state.tests.common import StateTestBase
1255
1256-import os
1257-import shutil
1258-
1259
1260 class CharmStateManagerTest(StateTestBase):
1261
1262@@ -107,6 +109,20 @@
1263 "local:series/dummy-1")
1264 metadata = yield charm_state.get_metadata()
1265 self.assertEquals(metadata.name, "dummy")
1266+ self.assertFalse(metadata.is_subordinate)
1267+ self.assertFalse(charm_state.is_subordinate())
1268+
1269+ @inlineCallbacks
1270+ def test_charm_state_is_subordinate(self):
1271+ log_dir = os.path.join(os.path.dirname(sample_directory), "logging")
1272+ charm = CharmDirectory(log_dir)
1273+ yield self.charm_state_manager.add_charm_state(
1274+ "local:series/logging-1", charm, "")
1275+ charm_state = yield self.charm_state_manager.get_charm_state(
1276+ "local:series/logging-1")
1277+
1278+ self.assertTrue(charm_state.is_subordinate)
1279+
1280
1281 @inlineCallbacks
1282 def test_charm_state_config_options(self):
1283
1284=== modified file 'juju/state/tests/test_service.py'
1285--- juju/state/tests/test_service.py 2012-03-30 10:13:09 +0000
1286+++ juju/state/tests/test_service.py 2012-04-05 21:24:20 +0000
1287@@ -11,2880 +11,2921 @@
1288 from juju.charm.tests import local_charm_id
1289 from juju.charm.tests.test_metadata import test_repository_path
1290 from juju.machine.tests.test_constraints import (
1291- dummy_constraints, dummy_cs, series_constraints)
1292+ dummy_constraints, dummy_cs, series_constraints)
1293 from juju.lib.pick import pick_attr
1294 from juju.state.charm import CharmStateManager
1295 from juju.charm.tests.test_repository import unbundled_repository
1296 from juju.state.endpoint import RelationEndpoint
1297 from juju.state.service import (
1298- ServiceStateManager, NO_HOOKS, RETRY_HOOKS, parse_service_name)
1299+ ServiceStateManager, NO_HOOKS, RETRY_HOOKS, parse_service_name)
1300 from juju.state.machine import MachineStateManager
1301 from juju.state.relation import RelationStateManager
1302 from juju.state.utils import YAMLState
1303 from juju.state.errors import (
1304- StateChanged, ServiceStateNotFound, ServiceUnitStateNotFound,
1305- ServiceUnitStateMachineAlreadyAssigned, ServiceStateNameInUse,
1306- BadDescriptor, BadServiceStateName, ServiceUnitDebugAlreadyEnabled,
1307- MachineStateNotFound, NoUnusedMachines, ServiceUnitResolvedAlreadyEnabled,
1308- ServiceUnitRelationResolvedAlreadyEnabled, StopWatcher,
1309- ServiceUnitUpgradeAlreadyEnabled)
1310+ StateChanged, ServiceStateNotFound, ServiceUnitStateNotFound,
1311+ ServiceUnitStateMachineAlreadyAssigned, ServiceStateNameInUse,
1312+ BadDescriptor, BadServiceStateName, ServiceUnitDebugAlreadyEnabled,
1313+ MachineStateNotFound, NoUnusedMachines, ServiceUnitResolvedAlreadyEnabled,
1314+ ServiceUnitRelationResolvedAlreadyEnabled, StopWatcher,
1315+ ServiceUnitUpgradeAlreadyEnabled)
1316
1317 from juju.state.tests.common import StateTestBase
1318
1319
1320 class ServiceStateManagerTestBase(StateTestBase):
1321
1322- @inlineCallbacks
1323- def setUp(self):
1324- yield super(ServiceStateManagerTestBase, self).setUp()
1325- yield self.push_default_config()
1326- self.charm_state_manager = CharmStateManager(self.client)
1327- self.service_state_manager = ServiceStateManager(self.client)
1328- self.machine_state_manager = MachineStateManager(self.client)
1329- self.charm_state = yield self.charm_state_manager.add_charm_state(
1330- local_charm_id(self.charm), self.charm, "")
1331- self.relation_state_manager = RelationStateManager(self.client)
1332-
1333- @inlineCallbacks
1334- def add_service(self, name):
1335- service_state = yield self.service_state_manager.add_service_state(
1336- name, self.charm_state, dummy_constraints)
1337- returnValue(service_state)
1338-
1339- @inlineCallbacks
1340- def add_service_from_charm(
1341- self, service_name, charm_id=None, constraints=None,
1342- charm_dir=None, charm_name=None):
1343- """Add a service from a charm.
1344- """
1345- if not charm_id and charm_dir is None:
1346- charm_name = charm_name or service_name
1347- charm_dir = CharmDirectory(os.path.join(
1348- test_repository_path, "series", charm_name))
1349- if charm_id is None:
1350- charm_state = yield self.charm_state_manager.add_charm_state(
1351- local_charm_id(charm_dir), charm_dir, "")
1352- else:
1353- charm_state = yield self.charm_state_manager.get_charm_state(
1354- charm_id)
1355- service_state = yield self.service_state_manager.add_service_state(
1356- service_name, charm_state, constraints or dummy_constraints)
1357- returnValue(service_state)
1358-
1359- @inlineCallbacks
1360- def get_subordinate_charm(self):
1361- """Return charm state for a subordinate charm.
1362-
1363- Many tests rely on adding relationships to a proper subordinate.
1364- This return the charm state of a testing subordinate charm.
1365- """
1366- unbundled_repo_path = self.makeDir()
1367- os.rmdir(unbundled_repo_path)
1368- shutil.copytree(unbundled_repository, unbundled_repo_path)
1369-
1370- sub_charm = CharmDirectory(
1371- os.path.join(unbundled_repo_path, "series", "logging"))
1372- self.charm_state_manager.add_charm_state("local:series/logging-1",
1373- sub_charm, "")
1374- logging_charm_state = yield self.charm_state_manager.get_charm_state(
1375- "local:series/logging-1")
1376- returnValue(logging_charm_state)
1377-
1378- @inlineCallbacks
1379- def add_relation(self, relation_type, relation_scope, *services):
1380- endpoints = []
1381- for service_meta in services:
1382- service_state, relation_name, relation_role = service_meta
1383- endpoints.append(RelationEndpoint(
1384- service_state.service_name,
1385- relation_type,
1386- relation_name,
1387- relation_role,
1388- relation_scope))
1389- relation_state = yield self.relation_state_manager.add_relation_state(
1390- *endpoints)
1391- returnValue(relation_state[0])
1392-
1393- @inlineCallbacks
1394- def remove_service(self, internal_service_id):
1395- topology = yield self.get_topology()
1396- topology.remove_service(internal_service_id)
1397- yield self.set_topology(topology)
1398-
1399- @inlineCallbacks
1400- def remove_service_unit(self, internal_service_id, internal_unit_id):
1401- topology = yield self.get_topology()
1402- topology.remove_service_unit(internal_service_id, internal_unit_id)
1403- yield self.set_topology(topology)
1404-
1405- def add_machine_state(self, constraints=None):
1406- return self.machine_state_manager.add_machine_state(
1407- constraints or series_constraints)
1408-
1409- @inlineCallbacks
1410- def assert_machine_states(self, present, absent):
1411- """Assert that machine IDs are either `present` or `absent` in topo."""
1412- for machine_id in present:
1413- self.assertTrue(
1414- (yield self.machine_state_manager.get_machine_state(
1415- machine_id)))
1416- for machine_id in absent:
1417- ex = yield self.assertFailure(
1418- self.machine_state_manager.get_machine_state(machine_id),
1419- MachineStateNotFound)
1420- self.assertEqual(ex.machine_id, machine_id)
1421-
1422- @inlineCallbacks
1423- def assert_machine_assignments(self, service_name, machine_ids):
1424- """Assert that `service_name` is deployed on `machine_ids`."""
1425- topology = yield self.get_topology()
1426- service_id = topology.find_service_with_name(service_name)
1427- assigned_machine_ids = [
1428- topology.get_service_unit_machine(service_id, unit_id)
1429- for unit_id in topology.get_service_units(service_id)]
1430- internal_machine_ids = []
1431- for machine_id in machine_ids:
1432- if machine_id is None:
1433- # corresponds to get_service_unit_machine API in this case
1434- internal_machine_ids.append(None)
1435- else:
1436- internal_machine_ids.append("machine-%010d" % machine_id)
1437- self.assertEqual(
1438- set(assigned_machine_ids), set(internal_machine_ids))
1439-
1440- @inlineCallbacks
1441- def get_unit_state(self):
1442- """Simple test helper to get a unit state."""
1443- service_state = yield self.service_state_manager.add_service_state(
1444- "wordpress", self.charm_state, dummy_constraints)
1445- unit_state = yield service_state.add_unit_state()
1446- returnValue(unit_state)
1447+ @inlineCallbacks
1448+ def setUp(self):
1449+ yield super(ServiceStateManagerTestBase, self).setUp()
1450+ yield self.push_default_config()
1451+ self.charm_state_manager = CharmStateManager(self.client)
1452+ self.service_state_manager = ServiceStateManager(self.client)
1453+ self.machine_state_manager = MachineStateManager(self.client)
1454+ self.charm_state = yield self.charm_state_manager.add_charm_state(
1455+ local_charm_id(self.charm), self.charm, "")
1456+ self.relation_state_manager = RelationStateManager(self.client)
1457+
1458+ @inlineCallbacks
1459+ def add_service(self, name):
1460+ service_state = yield self.service_state_manager.add_service_state(
1461+ name, self.charm_state, dummy_constraints)
1462+ returnValue(service_state)
1463+
1464+ @inlineCallbacks
1465+ def add_service_from_charm(
1466+ self, service_name, charm_id=None, constraints=None,
1467+ charm_dir=None, charm_name=None):
1468+ """Add a service from a charm.
1469+ """
1470+ if not charm_id and charm_dir is None:
1471+ charm_name = charm_name or service_name
1472+ charm_dir = CharmDirectory(os.path.join(
1473+ test_repository_path, "series", charm_name))
1474+ if charm_id is None:
1475+ charm_state = yield self.charm_state_manager.add_charm_state(
1476+ local_charm_id(charm_dir), charm_dir, "")
1477+ else:
1478+ charm_state = yield self.charm_state_manager.get_charm_state(
1479+ charm_id)
1480+ service_state = yield self.service_state_manager.add_service_state(
1481+ service_name, charm_state, constraints or dummy_constraints)
1482+ returnValue(service_state)
1483+
1484+ @inlineCallbacks
1485+ def get_subordinate_charm(self):
1486+ """Return charm state for a subordinate charm.
1487+
1488+ Many tests rely on adding relationships to a proper subordinate.
1489+ This return the charm state of a testing subordinate charm.
1490+ """
1491+ unbundled_repo_path = self.makeDir()
1492+ os.rmdir(unbundled_repo_path)
1493+ shutil.copytree(unbundled_repository, unbundled_repo_path)
1494+
1495+ sub_charm = CharmDirectory(
1496+ os.path.join(unbundled_repo_path, "series", "logging"))
1497+ self.charm_state_manager.add_charm_state("local:series/logging-1",
1498+ sub_charm, "")
1499+ logging_charm_state = yield self.charm_state_manager.get_charm_state(
1500+ "local:series/logging-1")
1501+ returnValue(logging_charm_state)
1502+
1503+ @inlineCallbacks
1504+ def add_relation(self, relation_type, relation_scope, *services):
1505+ endpoints = []
1506+ for service_meta in services:
1507+ service_state, relation_name, relation_role = service_meta
1508+ endpoints.append(RelationEndpoint(
1509+ service_state.service_name,
1510+ relation_type,
1511+ relation_name,
1512+ relation_role,
1513+ relation_scope))
1514+ relation_state = yield self.relation_state_manager.add_relation_state(
1515+ *endpoints)
1516+ returnValue(relation_state[0])
1517+
1518+ @inlineCallbacks
1519+ def remove_service(self, internal_service_id):
1520+ topology = yield self.get_topology()
1521+ topology.remove_service(internal_service_id)
1522+ yield self.set_topology(topology)
1523+
1524+ @inlineCallbacks
1525+ def remove_service_unit(self, internal_service_id, internal_unit_id):
1526+ topology = yield self.get_topology()
1527+ topology.remove_service_unit(internal_service_id, internal_unit_id)
1528+ yield self.set_topology(topology)
1529+
1530+ def add_machine_state(self, constraints=None):
1531+ return self.machine_state_manager.add_machine_state(
1532+ constraints or series_constraints)
1533+
1534+ @inlineCallbacks
1535+ def assert_machine_states(self, present, absent):
1536+ """Assert that machine IDs are either `present` or `absent` in topo."""
1537+ for machine_id in present:
1538+ self.assertTrue(
1539+ (yield self.machine_state_manager.get_machine_state(
1540+ machine_id)))
1541+ for machine_id in absent:
1542+ ex = yield self.assertFailure(
1543+ self.machine_state_manager.get_machine_state(machine_id),
1544+ MachineStateNotFound)
1545+ self.assertEqual(ex.machine_id, machine_id)
1546+
1547+ @inlineCallbacks
1548+ def assert_machine_assignments(self, service_name, machine_ids):
1549+ """Assert that `service_name` is deployed on `machine_ids`."""
1550+ topology = yield self.get_topology()
1551+ service_id = topology.find_service_with_name(service_name)
1552+ assigned_machine_ids = [
1553+ topology.get_service_unit_machine(service_id, unit_id)
1554+ for unit_id in topology.get_service_units(service_id)]
1555+ internal_machine_ids = []
1556+ for machine_id in machine_ids:
1557+ if machine_id is None:
1558+ # corresponds to get_service_unit_machine API in this case
1559+ internal_machine_ids.append(None)
1560+ else:
1561+ internal_machine_ids.append("machine-%010d" % machine_id)
1562+ self.assertEqual(
1563+ set(assigned_machine_ids), set(internal_machine_ids))
1564+
1565+ @inlineCallbacks
1566+ def get_unit_state(self):
1567+ """Simple test helper to get a unit state."""
1568+ service_state = yield self.service_state_manager.add_service_state(
1569+ "wordpress", self.charm_state, dummy_constraints)
1570+ unit_state = yield service_state.add_unit_state()
1571+ returnValue(unit_state)
1572
1573
1574 class ServiceStateManagerTest(ServiceStateManagerTestBase):
1575
1576- @inlineCallbacks
1577- def test_add_service(self):
1578- """
1579- Adding a service state should register it in zookeeper,
1580- including the requested charm id.
1581- """
1582- yield self.service_state_manager.add_service_state(
1583- "wordpress", self.charm_state, dummy_constraints)
1584- yield self.service_state_manager.add_service_state(
1585- "mysql", self.charm_state, dummy_constraints)
1586- children = yield self.client.get_children("/services")
1587-
1588- self.assertEquals(sorted(children),
1589- ["service-0000000000", "service-0000000001"])
1590-
1591- content, stat = yield self.client.get("/services/service-0000000000")
1592- details = yaml.load(content)
1593- self.assertTrue(details)
1594- self.assertEquals(details.get("charm"), "local:series/dummy-1")
1595- self.assertFalse(isinstance(details.get("charm"), unicode))
1596- self.assertEquals(details.get("constraints"), series_constraints.data)
1597-
1598- topology = yield self.get_topology()
1599- self.assertEquals(topology.find_service_with_name("wordpress"),
1600- "service-0000000000")
1601- self.assertEquals(topology.find_service_with_name("mysql"),
1602- "service-0000000001")
1603-
1604- @inlineCallbacks
1605- def test_add_service_with_duplicated_name(self):
1606- """
1607- If a service is added with a duplicated name, a meaningful
1608- error should be raised.
1609- """
1610- yield self.service_state_manager.add_service_state(
1611- "wordpress", self.charm_state, dummy_constraints)
1612-
1613- try:
1614- yield self.service_state_manager.add_service_state(
1615- "wordpress", self.charm_state, dummy_constraints)
1616- except ServiceStateNameInUse, e:
1617- self.assertEquals(e.service_name, "wordpress")
1618- else:
1619- self.fail("Error not raised")
1620-
1621- @inlineCallbacks
1622- def test_get_service_and_check_attributes(self):
1623- """
1624- Getting a service state should be possible, and the service
1625- state identification should be available through its
1626- attributes.
1627- """
1628- yield self.service_state_manager.add_service_state(
1629- "wordpress", self.charm_state, dummy_constraints)
1630- service_state = yield self.service_state_manager.get_service_state(
1631- "wordpress")
1632- self.assertEquals(service_state.service_name, "wordpress")
1633- self.assertEquals(service_state.internal_id, "service-0000000000")
1634-
1635- @inlineCallbacks
1636- def test_get_service_not_found(self):
1637- """
1638- Getting a service state which is not available should errback
1639- a meaningful error.
1640- """
1641- try:
1642- yield self.service_state_manager.get_service_state("wordpress")
1643- except ServiceStateNotFound, e:
1644- self.assertEquals(e.service_name, "wordpress")
1645- else:
1646- self.fail("Error not raised")
1647-
1648- def test_get_unit_state(self):
1649- """A unit state can be retrieved by name from the service manager."""
1650- self.assertFailure(self.service_state_manager.get_unit_state(
1651- "wordpress/1"), ServiceStateNotFound)
1652-
1653- self.assertFailure(self.service_state_manager.get_unit_state(
1654- "wordpress1"), ServiceUnitStateNotFound)
1655-
1656- wordpress_state = yield self.service_state_manager.add_service_state(
1657- "wordpress", self.charm_state, dummy_constraints)
1658-
1659- self.assertFailure(self.service_state_manager.get_unit_state(
1660- "wordpress/1"), ServiceUnitStateNotFound)
1661-
1662- wordpress_unit = wordpress_state.add_unit_state()
1663-
1664- unit_state = yield self.service_state_manager.get_unit_state(
1665- "wordpress/1")
1666-
1667- self.assertEqual(unit_state.internal_id, wordpress_unit.internal_id)
1668-
1669- @inlineCallbacks
1670- def test_get_service_charm_id(self):
1671- """
1672- The service state should make its respective charm id available.
1673- """
1674- yield self.service_state_manager.add_service_state(
1675- "wordpress", self.charm_state, dummy_constraints)
1676- service_state = yield self.service_state_manager.get_service_state(
1677- "wordpress")
1678- charm_id = yield service_state.get_charm_id()
1679- self.assertEquals(charm_id, "local:series/dummy-1")
1680-
1681- @inlineCallbacks
1682- def test_set_service_charm_id(self):
1683- """
1684- The service state should allow its charm id to be set.
1685- """
1686- yield self.service_state_manager.add_service_state(
1687- "wordpress", self.charm_state, dummy_constraints)
1688- service_state = yield self.service_state_manager.get_service_state(
1689- "wordpress")
1690- yield service_state.set_charm_id("local:series/dummy-2")
1691- charm_id = yield service_state.get_charm_id()
1692- self.assertEquals(charm_id, "local:series/dummy-2")
1693-
1694- @inlineCallbacks
1695- def test_get_service_constraints(self):
1696- """The service state should make constraints available"""
1697- initial_constraints = dummy_cs.parse(["cpu=256", "arch=amd64"])
1698- yield self.service_state_manager.add_service_state(
1699- "wordpress", self.charm_state, initial_constraints)
1700- service_state = yield self.service_state_manager.get_service_state(
1701- "wordpress")
1702- constraints = yield service_state.get_constraints()
1703- self.assertEquals(
1704- constraints, initial_constraints.with_series("series"))
1705-
1706- @inlineCallbacks
1707- def test_get_service_constraints_inherits(self):
1708- """The service constraints should be combined with the environment's"""
1709- yield self.push_env_constraints("arch=arm", "cpu=32")
1710- service_constraints = dummy_cs.parse(["cpu=256"])
1711- yield self.service_state_manager.add_service_state(
1712- "wordpress", self.charm_state, service_constraints)
1713- service_state = yield self.service_state_manager.get_service_state(
1714- "wordpress")
1715- constraints = yield service_state.get_constraints()
1716- expected_base = dummy_cs.parse(["arch=arm", "cpu=256"])
1717- self.assertEquals(constraints, expected_base.with_series("series"))
1718-
1719- @inlineCallbacks
1720- def test_get_missing_service_constraints(self):
1721- """
1722- Nodes created before the constraints mechanism was added should have
1723- empty constraints.
1724- """
1725- yield self.client.delete("/constraints")
1726- yield self.service_state_manager.add_service_state(
1727- "wordpress", self.charm_state, dummy_constraints)
1728- service_state = yield self.service_state_manager.get_service_state(
1729- "wordpress")
1730- path = "/services/" + service_state.internal_id
1731- node = YAMLState(self.client, path)
1732- yield node.read()
1733- del node["constraints"]
1734- yield node.write()
1735- constraints = yield service_state.get_constraints()
1736- self.assertEquals(constraints.data, {})
1737-
1738- @inlineCallbacks
1739- def test_get_missing_unit_constraints(self):
1740- """
1741- Nodes created before the constraints mechanism was added should have
1742- empty constraints.
1743- """
1744- yield self.service_state_manager.add_service_state(
1745- "wordpress", self.charm_state, dummy_constraints)
1746- service_state = yield self.service_state_manager.get_service_state(
1747- "wordpress")
1748- unit_state = yield service_state.add_unit_state()
1749- path = "/units/" + unit_state.internal_id
1750- node = YAMLState(self.client, path)
1751- yield node.read()
1752- del node["constraints"]
1753- yield node.write()
1754- constraints = yield unit_state.get_constraints()
1755- self.assertEquals(constraints.data, {})
1756-
1757- @inlineCallbacks
1758- def test_set_service_constraints(self):
1759- """The service state should make constraints available for change"""
1760- initial_constraints = dummy_cs.parse(["cpu=256", "arch=amd64"])
1761- yield self.service_state_manager.add_service_state(
1762- "wordpress", self.charm_state, initial_constraints)
1763- service_state = yield self.service_state_manager.get_service_state(
1764- "wordpress")
1765- new_constraints = dummy_cs.parse(["mem=2G", "arch=arm"])
1766- yield service_state.set_constraints(new_constraints)
1767- retrieved_constraints = yield service_state.get_constraints()
1768- self.assertEquals(
1769- retrieved_constraints, new_constraints.with_series("series"))
1770-
1771- @inlineCallbacks
1772- def test_remove_service_state(self):
1773- """
1774- A service state can be removed along with its relations, units,
1775- and zookeeper state.
1776- """
1777- service_state = yield self.service_state_manager.add_service_state(
1778- "wordpress", self.charm_state, dummy_constraints)
1779-
1780- relation_state = yield self.add_relation(
1781- "rel-type2", "global", [service_state, "app", "server"])
1782-
1783- unit_state = yield service_state.add_unit_state()
1784- machine_state = yield self.machine_state_manager.add_machine_state(
1785- series_constraints)
1786- yield unit_state.assign_to_machine(machine_state)
1787-
1788- yield self.service_state_manager.remove_service_state(service_state)
1789-
1790- topology = yield self.get_topology()
1791- self.assertFalse(topology.has_relation(relation_state.internal_id))
1792- self.assertFalse(topology.has_service(service_state.internal_id))
1793- self.assertFalse(
1794- topology.get_service_units_in_machine(machine_state.internal_id))
1795-
1796- exists = yield self.client.exists(
1797- "/services/%s" % service_state.internal_id)
1798- self.assertFalse(exists)
1799-
1800- @inlineCallbacks
1801- def test_add_service_unit_and_check_attributes(self):
1802- """
1803- A service state should enable adding a new service unit
1804- under it, and again the unit should offer attributes allowing
1805- its identification.
1806- """
1807- service_state0 = yield self.service_state_manager.add_service_state(
1808- "wordpress", self.charm_state, dummy_constraints)
1809- service_state1 = yield self.service_state_manager.add_service_state(
1810- "mysql", self.charm_state, dummy_constraints)
1811-
1812- unit_state0 = yield service_state0.add_unit_state()
1813- unit_state1 = yield service_state1.add_unit_state()
1814- unit_state2 = yield service_state0.add_unit_state()
1815- unit_state3 = yield service_state1.add_unit_state()
1816-
1817- children = yield self.client.get_children("/units")
1818- self.assertEquals(sorted(children),
1819- ["unit-0000000000", "unit-0000000001",
1820- "unit-0000000002", "unit-0000000003"])
1821-
1822- self.assertEquals(unit_state0.service_name, "wordpress")
1823- self.assertEquals(unit_state0.internal_id, "unit-0000000000")
1824- self.assertEquals(unit_state0.unit_name, "wordpress/0")
1825-
1826- self.assertEquals(unit_state1.service_name, "mysql")
1827- self.assertEquals(unit_state1.internal_id, "unit-0000000001")
1828- self.assertEquals(unit_state1.unit_name, "mysql/0")
1829-
1830- self.assertEquals(unit_state2.service_name, "wordpress")
1831- self.assertEquals(unit_state2.internal_id, "unit-0000000002")
1832- self.assertEquals(unit_state2.unit_name, "wordpress/1")
1833-
1834- self.assertEquals(unit_state3.service_name, "mysql")
1835- self.assertEquals(unit_state3.internal_id, "unit-0000000003")
1836- self.assertEquals(unit_state3.unit_name, "mysql/1")
1837-
1838- topology = yield self.get_topology()
1839-
1840- self.assertTrue(
1841- topology.has_service_unit("service-0000000000",
1842- "unit-0000000000"))
1843- self.assertTrue(
1844- topology.has_service_unit("service-0000000001",
1845- "unit-0000000001"))
1846- self.assertTrue(
1847- topology.has_service_unit("service-0000000000",
1848- "unit-0000000002"))
1849- self.assertTrue(
1850- topology.has_service_unit("service-0000000001",
1851- "unit-0000000003"))
1852-
1853- def get_presence_path(
1854- self, relation_state, relation_role, unit_state, container=None):
1855- container = container.internal_id if container else None
1856- presence_path = "/".join(filter(None, [
1857- "/relations",
1858- relation_state.internal_id,
1859- container,
1860- relation_role,
1861- unit_state.internal_id]))
1862- return presence_path
1863-
1864- @inlineCallbacks
1865- def test_add_service_unit_with_container(self):
1866- """
1867- Validate adding units with containers specified and recovering that.
1868- """
1869- mysql_ep = RelationEndpoint("mysql", "juju-info", "juju-info",
1870- "server", "global")
1871- logging_ep = RelationEndpoint("logging", "juju-info", "juju-info",
1872- "client", "container")
1873-
1874- logging_charm_state = yield self.get_subordinate_charm()
1875- self.assertTrue(logging_charm_state.is_subordinate())
1876- log_state = yield self.service_state_manager.add_service_state(
1877- "logging", logging_charm_state, dummy_constraints)
1878- mysql_state = yield self.service_state_manager.add_service_state(
1879- "mysql", self.charm_state, dummy_constraints)
1880-
1881- relation_state, service_states = (yield
1882- self.relation_state_manager.add_relation_state(
1883- mysql_ep, logging_ep))
1884-
1885- unit_state1 = yield mysql_state.add_unit_state()
1886- unit_state0 = yield log_state.add_unit_state(container=unit_state1)
1887-
1888- unit_state3 = yield mysql_state.add_unit_state()
1889- unit_state2 = yield log_state.add_unit_state(container=unit_state3)
1890-
1891- self.assertEquals((yield unit_state1.get_container()), None)
1892- self.assertEquals((yield unit_state0.get_container()), unit_state1)
1893- self.assertEquals((yield unit_state2.get_container()), unit_state3)
1894- self.assertEquals((yield unit_state3.get_container()), None)
1895-
1896- for unit_state in (unit_state1, unit_state3):
1897- yield unit_state.set_private_address(
1898- "%s.example.com" % (
1899- unit_state.unit_name.replace("/", "-")))
1900-
1901- # construct the proper relation state
1902- mystate = pick_attr(service_states, relation_role="server")
1903- logstate = pick_attr(service_states, relation_role="client")
1904- yield logstate.add_unit_state(unit_state0)
1905- yield logstate.add_unit_state(unit_state2)
1906-
1907- yield mystate.add_unit_state(unit_state1)
1908- yield mystate.add_unit_state(unit_state3)
1909-
1910- @inlineCallbacks
1911- def verify_container(relation_state, service_relation_state,
1912- unit_state, container):
1913- presence_path = self.get_presence_path(
1914- relation_state,
1915- service_relation_state.relation_role,
1916- unit_state,
1917- container)
1918-
1919- content, stat = yield self.client.get(presence_path)
1920- self.assertTrue(stat)
1921- self.assertEqual(content, '')
1922- # verify the node data on the relation role nodes
1923- role_path = os.path.dirname(presence_path)
1924- # role path
1925- content, stat = yield self.client.get(role_path)
1926- self.assertTrue(stat)
1927- node_info = yaml.load(content)
1928-
1929- self.assertEqual(
1930- node_info["name"],
1931- service_relation_state.relation_name)
1932- self.assertEqual(
1933- node_info["role"],
1934- service_relation_state.relation_role)
1935-
1936- settings_path = os.path.dirname(
1937- os.path.dirname(presence_path)) + "/settings/" + \
1938- unit_state.internal_id
1939- content, stat = yield self.client.get(settings_path)
1940- self.assertTrue(stat)
1941- settings_info = yaml.load(content)
1942-
1943- # Verify that private address was set
1944- # we verify the content elsewhere
1945- self.assertTrue(settings_info["private-address"])
1946-
1947- # verify all the units are constructed as expected
1948- # first the client roles with another container
1949- yield verify_container(relation_state, logstate,
1950- unit_state0, unit_state1)
1951-
1952- yield verify_container(relation_state, logstate,
1953- unit_state2, unit_state3)
1954-
1955- # and now the principals (which are their own relation containers)
1956- yield verify_container(relation_state, logstate,
1957- unit_state0, unit_state1)
1958-
1959- yield verify_container(relation_state, mystate,
1960- unit_state1, unit_state1)
1961-
1962- @inlineCallbacks
1963- def test_add_service_unit_with_changing_state(self):
1964- """
1965- When adding a service unit, there's a chance that the
1966- service will go away mid-way through. Rather than blowing
1967- up randomly, a nice error should be raised.
1968- """
1969- service_state = yield self.service_state_manager.add_service_state(
1970- "wordpress", self.charm_state, dummy_constraints)
1971-
1972- yield self.remove_service(service_state.internal_id)
1973-
1974- d = service_state.add_unit_state()
1975- yield self.assertFailure(d, StateChanged)
1976-
1977- @inlineCallbacks
1978- def test_get_unit_names(self):
1979- """A service's units names are retrievable."""
1980- service_state = yield self.service_state_manager.add_service_state(
1981- "wordpress", self.charm_state, dummy_constraints)
1982-
1983- expected_names = []
1984- for i in range(3):
1985- unit_state = yield service_state.add_unit_state()
1986- expected_names.append(unit_state.unit_name)
1987-
1988- unit_names = yield service_state.get_unit_names()
1989- self.assertEqual(unit_names, expected_names)
1990-
1991- @inlineCallbacks
1992- def test_remove_service_unit(self):
1993- """Removing a service unit removes all state associated.
1994- """
1995- service_state = yield self.service_state_manager.add_service_state(
1996- "wordpress", self.charm_state, dummy_constraints)
1997-
1998- unit_state = yield service_state.add_unit_state()
1999-
2000- # Assign to a machine
2001- machine_state = yield self.machine_state_manager.add_machine_state(
2002- series_constraints)
2003- yield unit_state.assign_to_machine(machine_state)
2004- # Connect a unit agent
2005- yield unit_state.connect_agent()
2006-
2007- # Now try and destroy it.
2008- yield service_state.remove_unit_state(unit_state)
2009-
2010- # Verify destruction.
2011- topology = yield self.get_topology()
2012- self.assertTrue(topology.has_service(service_state.internal_id))
2013- self.assertFalse(
2014- topology.has_service_unit(
2015- service_state.internal_id, unit_state.internal_id))
2016-
2017- exists = yield self.client.exists("/units/%s" % unit_state.internal_id)
2018- self.assertFalse(exists)
2019-
2020- def test_remove_service_unit_nonexistant(self):
2021- """Removing a non existant service unit, is fine."""
2022-
2023- service_state = yield self.service_state_manager.add_service_state(
2024- "wordpress", self.charm_state, dummy_constraints)
2025- unit_state = yield service_state.add_unit_state()
2026- yield service_state.remove_unit_state(unit_state)
2027- yield service_state.remove_unit_state(unit_state)
2028-
2029- @inlineCallbacks
2030- def test_get_all_service_states(self):
2031- services = yield self.service_state_manager.get_all_service_states()
2032- self.assertFalse(services)
2033-
2034- yield self.service_state_manager.add_service_state(
2035- "wordpress", self.charm_state, dummy_constraints)
2036- services = yield self.service_state_manager.get_all_service_states()
2037- self.assertEquals(len(services), 1)
2038-
2039- yield self.service_state_manager.add_service_state(
2040- "mysql", self.charm_state, dummy_constraints)
2041- services = yield self.service_state_manager.get_all_service_states()
2042- self.assertEquals(len(services), 2)
2043-
2044- @inlineCallbacks
2045- def test_get_service_unit(self):
2046- """
2047- Getting back service units should be possible using the
2048- user-oriented id.
2049- """
2050- service_state0 = yield self.service_state_manager.add_service_state(
2051- "wordpress", self.charm_state, dummy_constraints)
2052- service_state1 = yield self.service_state_manager.add_service_state(
2053- "mysql", self.charm_state, dummy_constraints)
2054-
2055- yield service_state0.add_unit_state()
2056- yield service_state1.add_unit_state()
2057- yield service_state0.add_unit_state()
2058- yield service_state1.add_unit_state()
2059-
2060- unit_state0 = yield service_state0.get_unit_state("wordpress/0")
2061- unit_state1 = yield service_state1.get_unit_state("mysql/0")
2062- unit_state2 = yield service_state0.get_unit_state("wordpress/1")
2063- unit_state3 = yield service_state1.get_unit_state("mysql/1")
2064-
2065- self.assertEquals(unit_state0.internal_id, "unit-0000000000")
2066- self.assertEquals(unit_state1.internal_id, "unit-0000000001")
2067- self.assertEquals(unit_state2.internal_id, "unit-0000000002")
2068- self.assertEquals(unit_state3.internal_id, "unit-0000000003")
2069-
2070- self.assertEquals(unit_state0.unit_name, "wordpress/0")
2071- self.assertEquals(unit_state1.unit_name, "mysql/0")
2072- self.assertEquals(unit_state2.unit_name, "wordpress/1")
2073- self.assertEquals(unit_state3.unit_name, "mysql/1")
2074-
2075- @inlineCallbacks
2076- def test_get_all_unit_states(self):
2077- service_state0 = yield self.service_state_manager.add_service_state(
2078- "wordpress", self.charm_state, dummy_constraints)
2079- service_state1 = yield self.service_state_manager.add_service_state(
2080- "mysql", self.charm_state, dummy_constraints)
2081-
2082- yield service_state0.add_unit_state()
2083- yield service_state1.add_unit_state()
2084- yield service_state0.add_unit_state()
2085- yield service_state1.add_unit_state()
2086-
2087- unit_state0 = yield service_state0.get_unit_state("wordpress/0")
2088- unit_state1 = yield service_state1.get_unit_state("mysql/0")
2089- unit_state2 = yield service_state0.get_unit_state("wordpress/1")
2090- unit_state3 = yield service_state1.get_unit_state("mysql/1")
2091-
2092- wordpress_units = yield service_state0.get_all_unit_states()
2093- self.assertEquals(
2094- set(wordpress_units), set((unit_state0, unit_state2)))
2095-
2096- mysql_units = yield service_state1.get_all_unit_states()
2097- self.assertEquals(set(mysql_units), set((unit_state1, unit_state3)))
2098-
2099- @inlineCallbacks
2100- def test_get_all_unit_states_with_changing_state(self):
2101- """
2102- When getting the service unit states, there's a chance that
2103- the service will go away mid-way through. Rather than blowing
2104- up randomly, a nice error should be raised.
2105- """
2106- service_state = yield self.service_state_manager.add_service_state(
2107- "wordpress", self.charm_state, dummy_constraints)
2108- yield service_state.add_unit_state()
2109- unit_state = (yield service_state.get_all_unit_states())[0]
2110- self.assertEqual(unit_state.unit_name, "wordpress/0")
2111- yield self.remove_service(service_state.internal_id)
2112- yield self.assertFailure(
2113- service_state.get_all_unit_states(), StateChanged)
2114-
2115- @inlineCallbacks
2116- def test_set_functions(self):
2117- wordpress = yield self.service_state_manager.add_service_state(
2118- "wordpress", self.charm_state, dummy_constraints)
2119- mysql = yield self.service_state_manager.add_service_state(
2120- "mysql", self.charm_state, dummy_constraints)
2121-
2122- s1 = yield self.service_state_manager.get_service_state(
2123- "wordpress")
2124- s2 = yield self.service_state_manager.get_service_state(
2125- "mysql")
2126- self.assertEquals(hash(s1), hash(wordpress))
2127- self.assertEquals(hash(s2), hash(mysql))
2128-
2129- self.assertNotEqual(s1, object())
2130- self.assertNotEqual(s1, s2)
2131-
2132- self.assertEquals(s1, wordpress)
2133- self.assertEquals(s2, mysql)
2134-
2135- us0 = yield wordpress.add_unit_state()
2136- us1 = yield wordpress.add_unit_state()
2137-
2138- unit_state0 = yield wordpress.get_unit_state("wordpress/0")
2139- unit_state1 = yield wordpress.get_unit_state("wordpress/1")
2140-
2141- self.assertEquals(us0, unit_state0)
2142- self.assertEquals(us1, unit_state1)
2143- self.assertEquals(hash(us1), hash(unit_state1))
2144-
2145- self.assertNotEqual(us0, object())
2146- self.assertNotEqual(us0, us1)
2147-
2148- @inlineCallbacks
2149- def test_get_service_unit_not_found(self):
2150- """
2151- Attempting to retrieve a non-existent service unit should
2152- result in an errback.
2153- """
2154- service_state0 = yield self.service_state_manager.add_service_state(
2155- "wordpress", self.charm_state, dummy_constraints)
2156- service_state1 = yield self.service_state_manager.add_service_state(
2157- "mysql", self.charm_state, dummy_constraints)
2158-
2159- # Add some state in a different service to make it a little
2160- # bit more prone to breaking in case of errors.
2161- yield service_state1.add_unit_state()
2162-
2163- try:
2164- yield service_state0.get_unit_state("wordpress/0")
2165- except ServiceUnitStateNotFound, e:
2166- self.assertEquals(e.unit_name, "wordpress/0")
2167- else:
2168- self.fail("Error not raised")
2169-
2170- @inlineCallbacks
2171- def test_get_set_public_address(self):
2172- service_state = yield self.service_state_manager.add_service_state(
2173- "wordpress", self.charm_state, dummy_constraints)
2174- unit_state = yield service_state.add_unit_state()
2175- self.assertEqual((yield unit_state.get_public_address()), None)
2176- yield unit_state.set_public_address("example.foobar.com")
2177- yield self.assertEqual(
2178- (yield unit_state.get_public_address()),
2179- "example.foobar.com")
2180-
2181- @inlineCallbacks
2182- def test_get_set_private_address(self):
2183- service_state = yield self.service_state_manager.add_service_state(
2184- "wordpress", self.charm_state, dummy_constraints)
2185- unit_state = yield service_state.add_unit_state()
2186- self.assertEqual((yield unit_state.get_private_address()), None)
2187- yield unit_state.set_private_address("example.local")
2188- yield self.assertEqual(
2189- (yield unit_state.get_private_address()),
2190- "example.local")
2191-
2192- @inlineCallbacks
2193- def test_get_service_unit_with_changing_state(self):
2194- """
2195- If a service is removed during operation, get_service_unit()
2196- should raise a nice error.
2197- """
2198- service_state = yield self.service_state_manager.add_service_state(
2199- "wordpress", self.charm_state, dummy_constraints)
2200-
2201- yield self.remove_service(service_state.internal_id)
2202-
2203- d = service_state.get_unit_state("wordpress/0")
2204- yield self.assertFailure(d, StateChanged)
2205-
2206- @inlineCallbacks
2207- def test_get_service_unit_with_bad_service_name(self):
2208- """
2209- Service unit names contain a service name embedded into
2210- them. The service name requested when calling get_unit_state()
2211- must match that of the object being used.
2212- """
2213- service_state0 = yield self.service_state_manager.add_service_state(
2214- "wordpress", self.charm_state, dummy_constraints)
2215- service_state1 = yield self.service_state_manager.add_service_state(
2216- "mysql", self.charm_state, dummy_constraints)
2217-
2218- # Add some state in a different service to make it a little
2219- # bit more prone to breaking in case of errors.
2220- yield service_state1.add_unit_state()
2221-
2222- try:
2223- yield service_state0.get_unit_state("mysql/0")
2224- except BadServiceStateName, e:
2225- self.assertEquals(e.expected_name, "wordpress")
2226- self.assertEquals(e.obtained_name, "mysql")
2227- else:
2228- self.fail("Error not raised")
2229-
2230- @inlineCallbacks
2231- def test_assign_unit_to_machine(self):
2232- service_state = yield self.service_state_manager.add_service_state(
2233- "wordpress", self.charm_state, dummy_constraints)
2234- unit_state = yield service_state.add_unit_state()
2235- machine_state = yield self.machine_state_manager.add_machine_state(
2236- series_constraints)
2237-
2238- yield unit_state.assign_to_machine(machine_state)
2239-
2240- topology = yield self.get_topology()
2241-
2242- self.assertEquals(
2243- topology.get_service_unit_machine(service_state.internal_id,
2244- unit_state.internal_id),
2245- machine_state.internal_id)
2246-
2247- @inlineCallbacks
2248- def test_assign_unit_to_machine_with_changing_state(self):
2249- service_state = yield self.service_state_manager.add_service_state(
2250- "wordpress", self.charm_state, dummy_constraints)
2251- unit_state = yield service_state.add_unit_state()
2252- machine_state = yield self.machine_state_manager.add_machine_state(
2253- series_constraints)
2254-
2255- yield self.remove_service_unit(service_state.internal_id,
2256- unit_state.internal_id)
2257-
2258- d = unit_state.assign_to_machine(machine_state)
2259- yield self.assertFailure(d, StateChanged)
2260-
2261- yield self.remove_service(service_state.internal_id)
2262-
2263- d = unit_state.assign_to_machine(machine_state)
2264- yield self.assertFailure(d, StateChanged)
2265-
2266- @inlineCallbacks
2267- def test_unassign_unit_from_machine(self):
2268- service_state = yield self.service_state_manager.add_service_state(
2269- "wordpress", self.charm_state, dummy_constraints)
2270- unit_state = yield service_state.add_unit_state()
2271- machine_state = yield self.machine_state_manager.add_machine_state(
2272- series_constraints)
2273-
2274- yield unit_state.assign_to_machine(machine_state)
2275- yield unit_state.unassign_from_machine()
2276-
2277- topology = yield self.get_topology()
2278-
2279- self.assertEquals(
2280- topology.get_service_unit_machine(service_state.internal_id,
2281- unit_state.internal_id),
2282- None)
2283-
2284- @inlineCallbacks
2285- def test_get_set_clear_resolved(self):
2286- """The a unit can be set to resolved to mark a future transition, with
2287- an optional retry flag."""
2288-
2289- unit_state = yield self.get_unit_state()
2290-
2291- self.assertIdentical((yield unit_state.get_resolved()), None)
2292- yield unit_state.set_resolved(NO_HOOKS)
2293-
2294- yield self.assertFailure(
2295- unit_state.set_resolved(NO_HOOKS),
2296- ServiceUnitResolvedAlreadyEnabled)
2297- yield self.assertEqual(
2298-
2299- (yield unit_state.get_resolved()), {"retry": NO_HOOKS})
2300-
2301- yield unit_state.clear_resolved()
2302- self.assertIdentical((yield unit_state.get_resolved()), None)
2303- yield unit_state.clear_resolved()
2304-
2305- yield self.assertFailure(unit_state.set_resolved(None), ValueError)
2306-
2307- @inlineCallbacks
2308- def test_watch_resolved(self):
2309- """A unit resolved watch can be instituted on a permanent basis."""
2310- unit_state = yield self.get_unit_state()
2311-
2312- results = []
2313-
2314- def callback(value):
2315- results.append(value)
2316-
2317- unit_state.watch_resolved(callback)
2318- yield unit_state.set_resolved(RETRY_HOOKS)
2319- yield unit_state.clear_resolved()
2320- yield unit_state.set_resolved(NO_HOOKS)
2321-
2322- yield self.poke_zk()
2323-
2324- self.assertEqual(len(results), 4)
2325- self.assertIdentical(results.pop(0), False)
2326- self.assertEqual(results.pop(0).type_name, "created")
2327- self.assertEqual(results.pop(0).type_name, "deleted")
2328- self.assertEqual(results.pop(0).type_name, "created")
2329-
2330- self.assertEqual(
2331- (yield unit_state.get_resolved()),
2332- {"retry": NO_HOOKS})
2333-
2334- @inlineCallbacks
2335- def test_watch_resolved_processes_current_state(self):
2336- """The watch method processes the current state before returning."""
2337- unit_state = yield self.get_unit_state()
2338-
2339- results = []
2340-
2341- @inlineCallbacks
2342- def callback(value):
2343- results.append((yield unit_state.get_resolved()))
2344-
2345- yield unit_state.watch_resolved(callback)
2346- self.assertTrue(results)
2347-
2348- @inlineCallbacks
2349- def test_stop_watch_resolved(self):
2350- """A unit resolved watch can be instituted on a permanent basis.
2351-
2352- However the callback can raise StopWatcher at anytime to stop the watch
2353- """
2354- unit_state = yield self.get_unit_state()
2355-
2356- results = []
2357-
2358- def callback(value):
2359- results.append(value)
2360- if len(results) == 1:
2361- raise StopWatcher()
2362- if len(results) == 3:
2363- raise StopWatcher()
2364-
2365- unit_state.watch_resolved(callback)
2366- yield unit_state.set_resolved(RETRY_HOOKS)
2367- yield unit_state.clear_resolved()
2368- yield self.poke_zk()
2369-
2370- unit_state.watch_resolved(callback)
2371- yield unit_state.set_resolved(NO_HOOKS)
2372- yield unit_state.clear_resolved()
2373-
2374- yield self.poke_zk()
2375-
2376- self.assertEqual(len(results), 3)
2377- self.assertIdentical(results.pop(0), False)
2378- self.assertIdentical(results.pop(0), False)
2379- self.assertEqual(results.pop(0).type_name, "created")
2380-
2381- self.assertEqual(
2382- (yield unit_state.get_resolved()), None)
2383-
2384- @inlineCallbacks
2385- def test_get_set_clear_relation_resolved(self):
2386- """The a unit's realtions can be set to resolved to mark a
2387- future transition, with an optional retry flag."""
2388-
2389- unit_state = yield self.get_unit_state()
2390-
2391- self.assertIdentical((yield unit_state.get_relation_resolved()), None)
2392- yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
2393-
2394- # Trying to set a conflicting raises an error
2395- yield self.assertFailure(
2396- unit_state.set_relation_resolved({"0": NO_HOOKS}),
2397- ServiceUnitRelationResolvedAlreadyEnabled)
2398-
2399- # Doing the same thing is fine
2400- yield unit_state.set_relation_resolved({"0": RETRY_HOOKS}),
2401-
2402- # Its fine to put in new values
2403- yield unit_state.set_relation_resolved({"21": RETRY_HOOKS})
2404- yield self.assertEqual(
2405- (yield unit_state.get_relation_resolved()),
2406- {"0": RETRY_HOOKS, "21": RETRY_HOOKS})
2407-
2408- yield unit_state.clear_relation_resolved()
2409- self.assertIdentical((yield unit_state.get_relation_resolved()), None)
2410- yield unit_state.clear_relation_resolved()
2411-
2412- yield self.assertFailure(
2413- unit_state.set_relation_resolved(True), ValueError)
2414- yield self.assertFailure(
2415- unit_state.set_relation_resolved(None), ValueError)
2416-
2417- @inlineCallbacks
2418- def test_watch_relation_resolved(self):
2419- """A unit resolved watch can be instituted on a permanent basis."""
2420- unit_state = yield self.get_unit_state()
2421-
2422- results = []
2423-
2424- def callback(value):
2425- results.append(value)
2426-
2427- unit_state.watch_relation_resolved(callback)
2428- yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
2429- yield unit_state.clear_relation_resolved()
2430- yield unit_state.set_relation_resolved({"0": NO_HOOKS})
2431-
2432- yield self.poke_zk()
2433-
2434- self.assertEqual(len(results), 4)
2435- self.assertIdentical(results.pop(0), False)
2436- self.assertEqual(results.pop(0).type_name, "created")
2437- self.assertEqual(results.pop(0).type_name, "deleted")
2438- self.assertEqual(results.pop(0).type_name, "created")
2439-
2440- self.assertEqual(
2441- (yield unit_state.get_relation_resolved()),
2442- {"0": NO_HOOKS})
2443-
2444- @inlineCallbacks
2445- def test_watch_relation_resolved_processes_current_state(self):
2446- """The watch method returns only after processing the current state."""
2447- unit_state = yield self.get_unit_state()
2448-
2449- results = []
2450-
2451- @inlineCallbacks
2452- def callback(value):
2453- results.append((yield unit_state.get_relation_resolved()))
2454- yield unit_state.watch_relation_resolved(callback)
2455- self.assertTrue(results)
2456-
2457- @inlineCallbacks
2458- def test_stop_watch_relation_resolved(self):
2459- """A unit resolved watch can be instituted on a permanent basis."""
2460- unit_state = yield self.get_unit_state()
2461-
2462- results = []
2463-
2464- def callback(value):
2465- results.append(value)
2466-
2467- if len(results) == 1:
2468- raise StopWatcher()
2469-
2470- if len(results) == 3:
2471- raise StopWatcher()
2472-
2473- unit_state.watch_relation_resolved(callback)
2474- yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
2475- yield unit_state.clear_relation_resolved()
2476- yield self.poke_zk()
2477- self.assertEqual(len(results), 1)
2478-
2479- unit_state.watch_relation_resolved(callback)
2480- yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
2481- yield unit_state.clear_relation_resolved()
2482- yield self.poke_zk()
2483- self.assertEqual(len(results), 3)
2484- self.assertIdentical(results.pop(0), False)
2485- self.assertIdentical(results.pop(0), False)
2486- self.assertEqual(results.pop(0).type_name, "created")
2487-
2488- self.assertEqual(
2489- (yield unit_state.get_relation_resolved()), None)
2490-
2491- @inlineCallbacks
2492- def test_watch_resolved_slow_callback(self):
2493- """A slow watch callback is still invoked serially."""
2494- unit_state = yield self.get_unit_state()
2495-
2496- callbacks = [Deferred() for i in range(5)]
2497- results = []
2498- contents = []
2499-
2500- @inlineCallbacks
2501- def watch(value):
2502- results.append(value)
2503- yield callbacks[len(results) - 1]
2504- contents.append((yield unit_state.get_resolved()))
2505-
2506- callbacks[0].callback(True)
2507- yield unit_state.watch_resolved(watch)
2508-
2509- # These get collapsed into a single event
2510- yield unit_state.set_resolved(RETRY_HOOKS)
2511- yield unit_state.clear_resolved()
2512- yield self.poke_zk()
2513-
2514- # Verify the callback hasn't completed
2515- self.assertEqual(len(results), 2)
2516- self.assertEqual(len(contents), 1)
2517-
2518- # Let it finish
2519- callbacks[1].callback(True)
2520- yield self.poke_zk()
2521-
2522- # Verify result counts
2523- self.assertEqual(len(results), 3)
2524- self.assertEqual(len(contents), 2)
2525-
2526- # Verify result values. Even though we have created event, the
2527- # setting retrieved shows the hook is not enabled.
2528- self.assertEqual(results[-1].type_name, "deleted")
2529- self.assertEqual(contents[-1], None)
2530-
2531- yield unit_state.set_resolved(NO_HOOKS)
2532- callbacks[2].callback(True)
2533- yield self.poke_zk()
2534-
2535- self.assertEqual(len(results), 4)
2536- self.assertEqual(contents[-1], {"retry": NO_HOOKS})
2537-
2538- # Clear out any pending activity.
2539- yield self.poke_zk()
2540-
2541- @inlineCallbacks
2542- def test_watch_relation_resolved_slow_callback(self):
2543- """A slow watch callback is still invoked serially."""
2544- unit_state = yield self.get_unit_state()
2545-
2546- callbacks = [Deferred() for i in range(5)]
2547- results = []
2548- contents = []
2549-
2550- @inlineCallbacks
2551- def watch(value):
2552- results.append(value)
2553- yield callbacks[len(results) - 1]
2554- contents.append((yield unit_state.get_relation_resolved()))
2555-
2556- callbacks[0].callback(True)
2557- yield unit_state.watch_relation_resolved(watch)
2558-
2559- # These get collapsed into a single event
2560- yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
2561- yield unit_state.clear_relation_resolved()
2562- yield self.poke_zk()
2563-
2564- # Verify the callback hasn't completed
2565- self.assertEqual(len(results), 2)
2566- self.assertEqual(len(contents), 1)
2567-
2568- # Let it finish
2569- callbacks[1].callback(True)
2570- yield self.poke_zk()
2571-
2572- # Verify result counts
2573- self.assertEqual(len(results), 3)
2574- self.assertEqual(len(contents), 2)
2575-
2576- # Verify result values. Even though we have created event, the
2577- # setting retrieved shows the hook is not enabled.
2578- self.assertEqual(results[-1].type_name, "deleted")
2579- self.assertEqual(contents[-1], None)
2580-
2581- yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
2582- callbacks[2].callback(True)
2583- yield self.poke_zk()
2584-
2585- self.assertEqual(len(results), 4)
2586- self.assertEqual(contents[-1], {"0": RETRY_HOOKS})
2587-
2588- # Clear out any pending activity.
2589- yield self.poke_zk()
2590-
2591- @inlineCallbacks
2592- def test_set_and_clear_upgrade_flag(self):
2593- """An upgrade flag can be set on a unit."""
2594-
2595- # Defaults to false
2596- unit_state = yield self.get_unit_state()
2597- upgrade_flag = yield unit_state.get_upgrade_flag()
2598- self.assertEqual(upgrade_flag, False)
2599-
2600- # Can be set
2601- yield unit_state.set_upgrade_flag()
2602- upgrade_flag = yield unit_state.get_upgrade_flag()
2603- self.assertEqual(upgrade_flag, {'force': False})
2604-
2605- # Attempting to set multiple times is an error if the values
2606- # differ.
2607- yield self.assertFailure(
2608- unit_state.set_upgrade_flag(force=True),
2609- ServiceUnitUpgradeAlreadyEnabled)
2610- self.assertEqual(upgrade_flag, {'force': False})
2611-
2612- # Can be cleared
2613- yield unit_state.clear_upgrade_flag()
2614- upgrade_flag = yield unit_state.get_upgrade_flag()
2615- self.assertEqual(upgrade_flag, False)
2616-
2617- # Can be cleared multiple times
2618- yield unit_state.clear_upgrade_flag()
2619- upgrade_flag = yield unit_state.get_upgrade_flag()
2620- self.assertEqual(upgrade_flag, False)
2621-
2622- # A empty node present is not problematic
2623- yield self.client.create(unit_state._upgrade_flag_path, "")
2624- upgrade_flag = yield unit_state.get_upgrade_flag()
2625- self.assertEqual(upgrade_flag, False)
2626-
2627- yield unit_state.set_upgrade_flag(force=True)
2628- upgrade_flag = yield unit_state.get_upgrade_flag()
2629- self.assertEqual(upgrade_flag, {"force": True})
2630-
2631- @inlineCallbacks
2632- def test_watch_upgrade_flag_once(self):
2633- """An upgrade watch can be set to notified of presence and changes."""
2634- unit_state = yield self.get_unit_state()
2635- yield unit_state.set_upgrade_flag()
2636-
2637- results = []
2638-
2639- def callback(value):
2640- results.append(value)
2641-
2642- unit_state.watch_upgrade_flag(callback, permanent=False)
2643- yield unit_state.clear_upgrade_flag()
2644- yield unit_state.set_upgrade_flag(force=True)
2645- yield self.sleep(0.1)
2646- yield self.poke_zk()
2647- self.assertEqual(len(results), 2)
2648- self.assertIdentical(results.pop(0), True)
2649- self.assertIdentical(results.pop().type_name, "deleted")
2650-
2651- self.assertEqual(
2652- (yield unit_state.get_upgrade_flag()),
2653- {'force': True})
2654-
2655- @inlineCallbacks
2656- def test_watch_upgrade_processes_current_state(self):
2657- unit_state = yield self.get_unit_state()
2658- results = []
2659-
2660- @inlineCallbacks
2661- def callback(value):
2662- results.append((yield unit_state.get_upgrade_flag()))
2663-
2664- yield unit_state.watch_upgrade_flag(callback)
2665- self.assertTrue(results)
2666-
2667- @inlineCallbacks
2668- def test_watch_upgrade_flag_permanent(self):
2669- """An upgrade watch can be instituted on a permanent basis."""
2670- unit_state = yield self.get_unit_state()
2671-
2672- results = []
2673-
2674- def callback(value):
2675- results.append(value)
2676-
2677- yield unit_state.watch_upgrade_flag(callback)
2678- self.assertTrue(results)
2679- yield unit_state.set_upgrade_flag()
2680- yield unit_state.clear_upgrade_flag()
2681- yield unit_state.set_upgrade_flag()
2682-
2683- yield self.poke_zk()
2684-
2685- self.assertEqual(len(results), 4)
2686- self.assertIdentical(results.pop(0), False)
2687- self.assertIdentical(results.pop(0).type_name, "created")
2688- self.assertIdentical(results.pop(0).type_name, "deleted")
2689- self.assertIdentical(results.pop(0).type_name, "created")
2690-
2691- self.assertEqual(
2692- (yield unit_state.get_upgrade_flag()),
2693- {'force': False})
2694-
2695- @inlineCallbacks
2696- def test_watch_upgrade_flag_waits_on_slow_callbacks(self):
2697- """A slow watch callback is still invoked serially."""
2698- unit_state = yield self.get_unit_state()
2699-
2700- callbacks = [Deferred() for i in range(5)]
2701- results = []
2702- contents = []
2703-
2704- @inlineCallbacks
2705- def watch(value):
2706- results.append(value)
2707- yield callbacks[len(results) - 1]
2708- contents.append((yield unit_state.get_upgrade_flag()))
2709-
2710- yield callbacks[0].callback(True)
2711- yield unit_state.watch_upgrade_flag(watch)
2712-
2713- # These get collapsed into a single event
2714- yield unit_state.set_upgrade_flag()
2715- yield unit_state.clear_upgrade_flag()
2716- yield self.poke_zk()
2717-
2718- # Verify the callback hasn't completed
2719- self.assertEqual(len(results), 2)
2720- self.assertEqual(len(contents), 1)
2721-
2722- # Let it finish
2723- callbacks[1].callback(True)
2724- yield self.poke_zk()
2725-
2726- # Verify result counts
2727- self.assertEqual(len(results), 3)
2728- self.assertEqual(len(contents), 2)
2729-
2730- # Verify result values. Even though we have created event, the
2731- # setting retrieved shows the hook is not enabled.
2732- self.assertEqual(results[-1].type_name, "deleted")
2733- self.assertEqual(contents[-1], False)
2734-
2735- yield unit_state.set_upgrade_flag()
2736- yield self.poke_zk()
2737-
2738- # Verify the callback hasn't completed
2739- self.assertEqual(len(contents), 2)
2740-
2741- callbacks[2].callback(True)
2742- yield self.poke_zk()
2743-
2744- # Verify values.
2745- self.assertEqual(len(contents), 3)
2746- self.assertEqual(results[-1].type_name, "created")
2747- self.assertEqual(contents[-1], {'force': False})
2748-
2749- # Clear out any pending activity.
2750- yield self.poke_zk()
2751-
2752- @inlineCallbacks
2753- def test_enable_debug_hook(self):
2754- """Unit hook debugging can be enabled on the unit state."""
2755- unit_state = yield self.get_unit_state()
2756- enabled = yield unit_state.enable_hook_debug(["*"])
2757- self.assertIdentical(enabled, True)
2758- content, stat = yield self.client.get(
2759- "/units/%s/debug" % unit_state.internal_id)
2760- data = yaml.load(content)
2761- self.assertEqual(data, {"debug_hooks": ["*"]})
2762-
2763- @inlineCallbacks
2764- def test_enable_debug_multiple_named_hooks(self):
2765- """Unit hook debugging can be enabled for multiple hooks."""
2766- unit_state = yield self.get_unit_state()
2767- enabled = yield unit_state.enable_hook_debug(
2768- ["db-relation-broken", "db-relation-changed"])
2769-
2770- self.assertIdentical(enabled, True)
2771- content, stat = yield self.client.get(
2772- "/units/%s/debug" % unit_state.internal_id)
2773- data = yaml.load(content)
2774- self.assertEqual(
2775- data,
2776- {"debug_hooks": ["db-relation-broken", "db-relation-changed"]})
2777-
2778- @inlineCallbacks
2779- def test_enable_debug_all_and_named_is_error(self):
2780- """Unit hook debugging can be enabled for multiple hooks,
2781- but only if they are all named hooks."""
2782- unit_state = yield self.get_unit_state()
2783-
2784- error = yield self.assertFailure(
2785- unit_state.enable_hook_debug(["*", "db-relation-changed"]),
2786- ValueError)
2787- self.assertEquals(
2788- str(error),
2789- "Ambigious to debug all hooks and named hooks "
2790- "['*', 'db-relation-changed']")
2791-
2792- @inlineCallbacks
2793- def test_enable_debug_requires_sequence(self):
2794- """The enable hook debug only accepts a sequences of names.
2795- """
2796- unit_state = yield self.get_unit_state()
2797-
2798- error = yield self.assertFailure(
2799- unit_state.enable_hook_debug(None),
2800- AssertionError)
2801- self.assertEquals(str(error), "Hook names must be a list: got None")
2802-
2803- @inlineCallbacks
2804- def test_enable_named_debug_hook(self):
2805- """Unit hook debugging can be enabled on for a named hook."""
2806- unit_state = yield self.get_unit_state()
2807- enabled = yield unit_state.enable_hook_debug(
2808- ["db-relation-changed"])
2809- self.assertIdentical(enabled, True)
2810- content, stat = yield self.client.get(
2811- "/units/%s/debug" % unit_state.internal_id)
2812- data = yaml.load(content)
2813- self.assertEqual(data, {"debug_hooks": ["db-relation-changed"]})
2814-
2815- @inlineCallbacks
2816- def test_enable_debug_hook_pre_existing(self):
2817- """Attempting to enable debug on a unit state already being debugged
2818- raises an exception.
2819- """
2820- unit_state = yield self.get_unit_state()
2821- yield unit_state.enable_hook_debug(["*"])
2822- error = yield self.assertFailure(unit_state.enable_hook_debug(["*"]),
2823- ServiceUnitDebugAlreadyEnabled)
2824- self.assertEquals(
2825- str(error), "Service unit 'wordpress/0' is already in debug mode.")
2826-
2827- @inlineCallbacks
2828- def test_enable_debug_hook_lifetime(self):
2829- """A debug hook setting is only active for the lifetime of the client
2830- that created it.
2831- """
2832- unit_state = yield self.get_unit_state()
2833- yield unit_state.enable_hook_debug(["*"])
2834- exists = yield self.client.exists(
2835- "/units/%s/debug" % unit_state.internal_id)
2836- self.assertTrue(exists)
2837- yield self.client.close()
2838- self.client = self.get_zookeeper_client()
2839- yield self.client.connect()
2840- exists = yield self.client.exists(
2841- "/units/%s/debug" % unit_state.internal_id)
2842- self.assertFalse(exists)
2843-
2844- @inlineCallbacks
2845- def test_watch_debug_hook_once(self):
2846- """A watch can be set to notified of presence and changes."""
2847- unit_state = yield self.get_unit_state()
2848- yield unit_state.enable_hook_debug(["*"])
2849-
2850- results = []
2851-
2852- def callback(value):
2853- results.append(value)
2854-
2855- yield unit_state.watch_hook_debug(callback, permanent=False)
2856- yield unit_state.clear_hook_debug()
2857- yield unit_state.enable_hook_debug(["*"])
2858- yield self.poke_zk()
2859- self.assertEqual(len(results), 2)
2860- self.assertIdentical(results.pop(0), True)
2861- self.assertIdentical(results.pop().type_name, "deleted")
2862-
2863- self.assertEqual(
2864- (yield unit_state.get_hook_debug()),
2865- {"debug_hooks": ["*"]})
2866-
2867- @inlineCallbacks
2868- def test_watch_debug_hook_processes_current_state(self):
2869- """A hook debug watch can be instituted on a permanent basis."""
2870- unit_state = yield self.get_unit_state()
2871-
2872- results = []
2873-
2874- @inlineCallbacks
2875- def callback(value):
2876- results.append((yield unit_state.get_hook_debug()))
2877-
2878- yield unit_state.watch_hook_debug(callback)
2879- self.assertTrue(results)
2880-
2881- @inlineCallbacks
2882- def test_watch_debug_hook_permanent(self):
2883- """A hook debug watch can be instituted on a permanent basis."""
2884- unit_state = yield self.get_unit_state()
2885-
2886- results = []
2887-
2888- def callback(value):
2889- results.append(value)
2890-
2891- yield unit_state.watch_hook_debug(callback)
2892- yield unit_state.enable_hook_debug(["*"])
2893- yield unit_state.clear_hook_debug()
2894- yield unit_state.enable_hook_debug(["*"])
2895-
2896- yield self.poke_zk()
2897-
2898- self.assertEqual(len(results), 4)
2899- self.assertIdentical(results.pop(0), False)
2900- self.assertIdentical(results.pop(0).type_name, "created")
2901- self.assertIdentical(results.pop(0).type_name, "deleted")
2902- self.assertIdentical(results.pop(0).type_name, "created")
2903-
2904- self.assertEqual(
2905- (yield unit_state.get_hook_debug()),
2906- {"debug_hooks": ["*"]})
2907-
2908- @inlineCallbacks
2909- def test_watch_debug_hook_waits_on_slow_callbacks(self):
2910- """A slow watch callback is still invoked serially."""
2911-
2912- unit_state = yield self.get_unit_state()
2913-
2914- callbacks = [Deferred() for i in range(5)]
2915- results = []
2916- contents = []
2917-
2918- @inlineCallbacks
2919- def watch(value):
2920- results.append(value)
2921- yield callbacks[len(results) - 1]
2922- contents.append((yield unit_state.get_hook_debug()))
2923-
2924- callbacks[0].callback(True) # Finish the current state processing
2925- yield unit_state.watch_hook_debug(watch)
2926-
2927- # These get collapsed into a single event
2928- yield unit_state.enable_hook_debug(["*"])
2929- yield unit_state.clear_hook_debug()
2930- yield self.poke_zk()
2931-
2932- # Verify the callback hasn't completed
2933- self.assertEqual(len(results), 2)
2934- self.assertEqual(len(contents), 1)
2935-
2936- # Let it finish
2937- callbacks[1].callback(True)
2938- yield self.poke_zk()
2939-
2940- # Verify result counts
2941- self.assertEqual(len(results), 3)
2942- self.assertEqual(len(contents), 2)
2943-
2944- # Verify result values. Even though we have created event, the
2945- # setting retrieved shows the hook is not enabled.
2946- self.assertEqual(results[-1].type_name, "deleted")
2947- self.assertEqual(contents[-1], None)
2948-
2949- yield unit_state.enable_hook_debug(["*"])
2950- yield self.poke_zk()
2951-
2952- # Verify the callback hasn't completed
2953- self.assertEqual(len(contents), 2)
2954-
2955- callbacks[2].callback(True)
2956- yield self.poke_zk()
2957-
2958- # Verify values.
2959- self.assertEqual(len(contents), 3)
2960- self.assertEqual(results[-1].type_name, "created")
2961- self.assertEqual(contents[-1], {"debug_hooks": ["*"]})
2962-
2963- # Clear out any pending activity.
2964- yield self.poke_zk()
2965-
2966- @inlineCallbacks
2967- def test_service_unit_agent(self):
2968- """A service unit state has an associated unit agent."""
2969- service_state = yield self.service_state_manager.add_service_state(
2970- "wordpress", self.charm_state, dummy_constraints)
2971- unit_state = yield service_state.add_unit_state()
2972- exists_d, watch_d = unit_state.watch_agent()
2973- exists = yield exists_d
2974- self.assertFalse(exists)
2975- yield unit_state.connect_agent()
2976- event = yield watch_d
2977- self.assertEqual(event.type_name, "created")
2978- self.assertEqual(event.path,
2979- "/units/%s/agent" % unit_state.internal_id)
2980-
2981- @inlineCallbacks
2982- def test_get_charm_id(self):
2983- """A service unit knows its charm id"""
2984- service_state = yield self.service_state_manager.add_service_state(
2985- "wordpress", self.charm_state, dummy_constraints)
2986- unit_state = yield service_state.add_unit_state()
2987- unit_charm = yield unit_state.get_charm_id()
2988- service_charm = yield service_state.get_charm_id()
2989- self.assertTrue(
2990- (self.charm_state.id == unit_charm == service_charm))
2991-
2992- @inlineCallbacks
2993- def test_set_charm_id(self):
2994- """A service unit charm can be set and is validated when set."""
2995- service_state = yield self.service_state_manager.add_service_state(
2996- "wordpress", self.charm_state, dummy_constraints)
2997- unit_state = yield service_state.add_unit_state()
2998- yield self.assertFailure(
2999- unit_state.set_charm_id("abc"), CharmURLError)
3000- yield self.assertFailure(
3001- unit_state.set_charm_id("abc:foobar-a"), CharmURLError)
3002- yield self.assertFailure(
3003- unit_state.set_charm_id(None), CharmURLError)
3004- charm_id = "local:series/name-1"
3005- yield unit_state.set_charm_id(charm_id)
3006- value = yield unit_state.get_charm_id()
3007- self.assertEqual(charm_id, value)
3008-
3009- @inlineCallbacks
3010- def test_add_unit_state_combines_constraints(self):
3011- """Constraints are inherited both from juju defaults and service"""
3012- service_state = yield self.service_state_manager.add_service_state(
3013- "wordpress", self.charm_state,
3014- dummy_cs.parse(["arch=arm", "mem=1G"]))
3015- unit_state = yield service_state.add_unit_state()
3016- constraints = yield unit_state.get_constraints()
3017- expected = {
3018- "arch": "arm", "cpu": 1, "mem": 1024,
3019- "provider-type": "dummy", "ubuntu-series": "series"}
3020- self.assertEquals(constraints, expected)
3021-
3022- @inlineCallbacks
3023- def test_unassign_unit_from_machine_without_being_assigned(self):
3024- """
3025- When unassigning a machine from a unit, it is possible that
3026- the machine has not been previously assigned, or that it
3027- was assigned but the state changed beneath us. In either
3028- case, the end state is the intended state, so we simply
3029- move forward without any errors here, to avoid having to
3030- handle the extra complexity of dealing with the concurrency
3031- problems.
3032- """
3033- service_state = yield self.service_state_manager.add_service_state(
3034- "wordpress", self.charm_state, dummy_constraints)
3035- unit_state = yield service_state.add_unit_state()
3036-
3037- yield unit_state.unassign_from_machine()
3038-
3039- topology = yield self.get_topology()
3040- self.assertEquals(
3041- topology.get_service_unit_machine(service_state.internal_id,
3042- unit_state.internal_id),
3043- None)
3044-
3045- machine_id = yield unit_state.get_assigned_machine_id()
3046- self.assertEqual(machine_id, None)
3047-
3048- @inlineCallbacks
3049- def test_assign_unit_to_machine_again_fails(self):
3050- """
3051- Trying to assign a machine to an already assigned unit
3052- should fail, unless we're assigning to precisely the same
3053- machine, in which case it's no big deal.
3054- """
3055- service_state = yield self.service_state_manager.add_service_state(
3056- "wordpress", self.charm_state, dummy_constraints)
3057- unit_state = yield service_state.add_unit_state()
3058- machine_state0 = yield self.machine_state_manager.add_machine_state(
3059- series_constraints)
3060- machine_state1 = yield self.machine_state_manager.add_machine_state(
3061- series_constraints)
3062-
3063- yield unit_state.assign_to_machine(machine_state0)
3064-
3065- # Assigning again to the same machine is a NOOP, so nothing
3066- # terrible should happen if we let it go through.
3067- yield unit_state.assign_to_machine(machine_state0)
3068-
3069- try:
3070- yield unit_state.assign_to_machine(machine_state1)
3071- except ServiceUnitStateMachineAlreadyAssigned, e:
3072- self.assertEquals(e.unit_name, "wordpress/0")
3073- else:
3074- self.fail("Error not raised")
3075-
3076- machine_id = yield unit_state.get_assigned_machine_id()
3077- self.assertEqual(machine_id, 0)
3078-
3079- @inlineCallbacks
3080- def test_unassign_unit_from_machine_with_changing_state(self):
3081- service_state = yield self.service_state_manager.add_service_state(
3082- "wordpress", self.charm_state, dummy_constraints)
3083- unit_state = yield service_state.add_unit_state()
3084-
3085- yield self.remove_service_unit(service_state.internal_id,
3086- unit_state.internal_id)
3087-
3088- d = unit_state.unassign_from_machine()
3089- yield self.assertFailure(d, StateChanged)
3090-
3091- d = unit_state.get_assigned_machine_id()
3092- yield self.assertFailure(d, StateChanged)
3093-
3094- yield self.remove_service(service_state.internal_id)
3095-
3096- d = unit_state.unassign_from_machine()
3097- yield self.assertFailure(d, StateChanged)
3098-
3099- d = unit_state.get_assigned_machine_id()
3100- yield self.assertFailure(d, StateChanged)
3101-
3102- @inlineCallbacks
3103- def test_assign_unit_to_unused_machine(self):
3104- """Verify that unused machines can be assigned to when their machine
3105- constraints match the service unit's."""
3106- yield self.machine_state_manager.add_machine_state(
3107- series_constraints)
3108- mysql_service_state = yield self.add_service_from_charm("mysql")
3109- mysql_unit_state = yield mysql_service_state.add_unit_state()
3110- mysql_machine_state = yield \
3111- self.machine_state_manager.add_machine_state(series_constraints)
3112- yield mysql_unit_state.assign_to_machine(mysql_machine_state)
3113- yield self.service_state_manager.remove_service_state(
3114- mysql_service_state)
3115- wordpress_service_state = yield self.add_service_from_charm(
3116- "wordpress")
3117- wordpress_unit_state = yield wordpress_service_state.add_unit_state()
3118- yield wordpress_unit_state.assign_to_unused_machine()
3119- self.assertEqual(
3120- (yield self.get_topology()).get_machines(),
3121- ["machine-0000000000", "machine-0000000001"])
3122- yield self.assert_machine_assignments("wordpress", [1])
3123-
3124- @inlineCallbacks
3125- def test_assign_unit_to_unused_machine_bad_constraints(self):
3126- """Verify that unused machines do not get allocated service units with
3127- non-matching constraints."""
3128- yield self.machine_state_manager.add_machine_state(
3129- series_constraints)
3130- mysql_service_state = yield self.add_service_from_charm("mysql")
3131- mysql_unit_state = yield mysql_service_state.add_unit_state()
3132- mysql_machine_state = yield \
3133- self.machine_state_manager.add_machine_state(
3134- series_constraints)
3135- yield mysql_unit_state.assign_to_machine(mysql_machine_state)
3136- yield self.service_state_manager.remove_service_state(
3137- mysql_service_state)
3138- other_constraints = dummy_cs.parse(["arch=arm"])
3139- wordpress_service_state = yield self.add_service_from_charm(
3140- "wordpress", constraints=other_constraints.with_series("series"))
3141- wordpress_unit_state = yield wordpress_service_state.add_unit_state()
3142- yield self.assertFailure(
3143- wordpress_unit_state.assign_to_unused_machine(),
3144- NoUnusedMachines)
3145- self.assertEqual(
3146- (yield self.get_topology()).get_machines(),
3147- ["machine-0000000000", "machine-0000000001"])
3148- yield self.assert_machine_assignments("wordpress", [None])
3149-
3150- @inlineCallbacks
3151- def test_assign_unit_to_unused_machine_with_changing_state_service(self):
3152- """Verify `StateChanged` raised if service is manipulated during reuse.
3153- """
3154- yield self.machine_state_manager.add_machine_state(
3155- series_constraints)
3156- mysql_service_state = yield self.add_service_from_charm("mysql")
3157- mysql_unit_state = yield mysql_service_state.add_unit_state()
3158- mysql_machine_state = yield self.machine_state_manager.add_machine_state(
3159- series_constraints)
3160- yield mysql_unit_state.assign_to_machine(mysql_machine_state)
3161- yield self.service_state_manager.remove_service_state(
3162- mysql_service_state)
3163- wordpress_service_state = yield self.add_service_from_charm(
3164- "wordpress")
3165- wordpress_unit_state = yield wordpress_service_state.add_unit_state()
3166- yield self.remove_service(wordpress_service_state.internal_id)
3167- yield self.assertFailure(
3168- wordpress_unit_state.assign_to_unused_machine(), StateChanged)
3169-
3170- @inlineCallbacks
3171- def test_assign_unit_to_unused_machine_with_changing_state_service_unit(self):
3172- "Verify `StateChanged` raised if unit is manipulated during reuse."""
3173- yield self.machine_state_manager.add_machine_state(
3174- series_constraints)
3175- mysql_service_state = yield self.add_service_from_charm("mysql")
3176- mysql_unit_state = yield mysql_service_state.add_unit_state()
3177- mysql_machine_state = yield self.machine_state_manager.add_machine_state(
3178- series_constraints)
3179- yield mysql_unit_state.assign_to_machine(mysql_machine_state)
3180- yield self.service_state_manager.remove_service_state(
3181- mysql_service_state)
3182- wordpress_service_state = yield self.add_service_from_charm(
3183- "wordpress")
3184- wordpress_unit_state = yield wordpress_service_state.add_unit_state()
3185- yield self.remove_service_unit(
3186- wordpress_service_state.internal_id,
3187- wordpress_unit_state.internal_id)
3188- yield self.assertFailure(
3189- wordpress_unit_state.assign_to_unused_machine(), StateChanged)
3190-
3191- @inlineCallbacks
3192- def test_assign_unit_to_unused_machine_only_machine_zero(self):
3193- """Verify when the only available machine is machine 0"""
3194- yield self.machine_state_manager.add_machine_state(
3195- series_constraints)
3196- wordpress_service_state = yield self.add_service_from_charm(
3197- "wordpress")
3198- wordpress_unit_state = yield wordpress_service_state.add_unit_state()
3199- yield self.assertFailure(
3200- wordpress_unit_state.assign_to_unused_machine(),
3201- NoUnusedMachines)
3202-
3203- @inlineCallbacks
3204- def test_assign_unit_to_unused_machine_none_available(self):
3205- """Verify when there are no unused machines"""
3206- yield self.machine_state_manager.add_machine_state(
3207- series_constraints)
3208- mysql_service_state = yield self.add_service_from_charm("mysql")
3209- mysql_unit_state = yield mysql_service_state.add_unit_state()
3210- mysql_machine_state = yield self.machine_state_manager.add_machine_state(
3211- series_constraints)
3212- yield mysql_unit_state.assign_to_machine(mysql_machine_state)
3213- yield self.assert_machine_assignments("mysql", [1])
3214- wordpress_service_state = yield self.add_service_from_charm(
3215- "wordpress")
3216- wordpress_unit_state = yield wordpress_service_state.add_unit_state()
3217- yield self.assertFailure(
3218- wordpress_unit_state.assign_to_unused_machine(),
3219- NoUnusedMachines)
3220-
3221- @inlineCallbacks
3222- def test_watch_relations_processes_current_state(self):
3223- """
3224- The watch method returns only after processing initial state.
3225-
3226- Note the callback is only invoked if there are changes
3227- requiring processing.
3228- """
3229- service_state = yield self.add_service("wordpress")
3230- yield self.add_relation(
3231- "rel-type", "global", [service_state, "name", "role"])
3232-
3233- results = []
3234-
3235- def callback(*args):
3236- results.append(True)
3237-
3238- yield service_state.watch_relation_states(callback)
3239- self.assertTrue(results)
3240-
3241- @inlineCallbacks
3242- def test_watch_relations_when_being_created(self):
3243- """
3244- We can watch relations before we have any.
3245- """
3246- service_state = yield self.add_service("wordpress")
3247-
3248- wait_callback = [Deferred() for i in range(5)]
3249- calls = []
3250-
3251- def watch_relations(old_relations, new_relations):
3252- calls.append((old_relations, new_relations))
3253- wait_callback[len(calls) - 1].callback(True)
3254-
3255- # Start watching
3256- service_state.watch_relation_states(watch_relations)
3257-
3258- # Callback is still untouched
3259- self.assertEquals(calls, [])
3260-
3261- # add a service relation and wait for the callback
3262- relation_state = yield self.add_relation(
3263- "rel-type", "global", [service_state, "name", "role"])
3264- yield wait_callback[1]
3265-
3266- # verify the result
3267- self.assertEquals(len(calls), 2)
3268- old_relations, new_relations = calls[1]
3269- self.assertFalse(old_relations)
3270- self.assertEquals(new_relations[0].relation_role, "role")
3271-
3272- # add a new relation with the service assigned to it.
3273- relation_state2 = yield self.add_relation(
3274- "rel-type2", "global", [service_state, "app", "server"])
3275- yield wait_callback[2]
3276-
3277- self.assertEquals(len(calls), 3)
3278- old_relations, new_relations = calls[2]
3279- self.assertEquals([r.internal_relation_id for r in old_relations],
3280- [relation_state.internal_id])
3281- self.assertEquals([r.internal_relation_id for r in new_relations],
3282- [relation_state.internal_id,
3283- relation_state2.internal_id])
3284-
3285- @inlineCallbacks
3286- def test_watch_relations_may_defer(self):
3287- """
3288- The watch relations callback may return a deferred so that
3289- it performs some its logic asynchronously. In this case, it must
3290- not be called a second time before its postponed logic is finished
3291- completely.
3292- """
3293- wait_callback = [Deferred() for i in range(5)]
3294- finish_callback = [Deferred() for i in range(5)]
3295-
3296- calls = []
3297-
3298- def watch_relations(old_relations, new_relations):
3299- calls.append((old_relations, new_relations))
3300- wait_callback[len(calls) - 1].callback(True)
3301- return finish_callback[len(calls) - 1]
3302-
3303- service_state = yield self.add_service("s-1")
3304- service_state.watch_relation_states(watch_relations)
3305-
3306- # Shouldn't have any callbacks yet.
3307- self.assertEquals(calls, [])
3308-
3309- # Assign to a relation.
3310- yield self.add_relation("rel-type", "global",
3311- (service_state, "name", "role"))
3312-
3313- # Hold off until callback is started.
3314- yield wait_callback[0]
3315-
3316- # Assign to another relation.
3317- yield self.add_relation("rel-type", "global",
3318- (service_state, "name2", "role"))
3319-
3320- # Give a chance for something bad to happen.
3321- yield self.sleep(0.3)
3322-
3323- # Ensure we still have a single call.
3324- self.assertEquals(len(calls), 1)
3325-
3326- # Allow the first call to be completed, and wait on the
3327- # next one.
3328- finish_callback[0].callback(None)
3329- yield wait_callback[1]
3330- finish_callback[1].callback(None)
3331-
3332- # We should have the second change now.
3333- self.assertEquals(len(calls), 2)
3334-
3335- @inlineCallbacks
3336- def test_get_relation_endpoints_service_name(self):
3337- """Test getting endpoints with descriptor ``<service name>``"""
3338- yield self.add_service_from_charm("wordpress")
3339- self.assertEqual(
3340- (yield self.service_state_manager.get_relation_endpoints(
3341- "wordpress")),
3342- [RelationEndpoint("wordpress", "varnish", "cache", "client"),
3343- RelationEndpoint("wordpress", "mysql", "db", "client"),
3344- RelationEndpoint("wordpress", "http", "url", "server"),
3345- RelationEndpoint("wordpress", "juju-info", "juju-info", "server")])
3346- yield self.add_service_from_charm("riak")
3347- self.assertEqual(
3348- (yield self.service_state_manager.get_relation_endpoints(
3349- "riak")),
3350- [RelationEndpoint("riak", "http", "admin", "server"),
3351- RelationEndpoint("riak", "http", "endpoint", "server"),
3352- RelationEndpoint("riak", "riak", "ring", "peer"),
3353- RelationEndpoint("riak", "juju-info", "juju-info", "server")])
3354-
3355- @inlineCallbacks
3356- def test_get_relation_endpoints_service_name_relation_name(self):
3357- """Test getting endpoints with ``<service name:relation name>``"""
3358- yield self.add_service_from_charm("wordpress")
3359- self.assertEqual(
3360- (yield self.service_state_manager.get_relation_endpoints(
3361- "wordpress:url")),
3362- [RelationEndpoint("wordpress", "http", "url", "server"),
3363- RelationEndpoint("wordpress", "juju-info", "juju-info", "server")])
3364- self.assertEqual(
3365- (yield self.service_state_manager.get_relation_endpoints(
3366- "wordpress:db")),
3367- [RelationEndpoint("wordpress", "mysql", "db", "client"),
3368- RelationEndpoint("wordpress", "juju-info", "juju-info", "server")])
3369- self.assertEqual(
3370- (yield self.service_state_manager.get_relation_endpoints(
3371- "wordpress:cache")),
3372- [RelationEndpoint("wordpress", "varnish", "cache", "client"),
3373- RelationEndpoint("wordpress", "juju-info", "juju-info", "server")])
3374- yield self.add_service_from_charm("riak")
3375- self.assertEqual(
3376- (yield self.service_state_manager.get_relation_endpoints(
3377- "riak:ring")),
3378- [RelationEndpoint("riak", "riak", "ring", "peer"),
3379- RelationEndpoint("riak", "juju-info", "juju-info", "server")])
3380-
3381- @inlineCallbacks
3382- def test_descriptor_for_services_without_charms(self):
3383- """Test with services that have no corresponding charms defined"""
3384- yield self.add_service("nocharm")
3385- # verify we get the implicit interface
3386- self.assertEqual(
3387- (yield self.service_state_manager.get_relation_endpoints(
3388- "nocharm")),
3389- [RelationEndpoint("nocharm",
3390- "juju-info", "juju-info", "server")])
3391-
3392- self.assertEqual(
3393- (yield self.service_state_manager.get_relation_endpoints(
3394- "nocharm:nonsense")),
3395- [RelationEndpoint("nocharm",
3396- "juju-info", "juju-info", "server")])
3397-
3398- @inlineCallbacks
3399- def test_descriptor_for_missing_service(self):
3400- """Test with a service that is not in the topology"""
3401- yield self.assertFailure(
3402- self.service_state_manager.get_relation_endpoints(
3403- "notadded"),
3404- ServiceStateNotFound)
3405-
3406- @inlineCallbacks
3407- def test_bad_descriptors(self):
3408- """Test that the descriptors meet the minimum naming standards"""
3409- yield self.assertFailure(
3410- self.service_state_manager.get_relation_endpoints("a:b:c"),
3411- BadDescriptor)
3412- yield self.assertFailure(
3413- self.service_state_manager.get_relation_endpoints(""),
3414- BadDescriptor)
3415-
3416- @inlineCallbacks
3417- def test_join_descriptors_service_name(self):
3418- """Test descriptor of the form ``<service name>`"""
3419- yield self.add_service_from_charm("wordpress")
3420- yield self.add_service_from_charm("mysql")
3421- self.assertEqual(
3422- (yield self.service_state_manager.join_descriptors(
3423- "wordpress", "mysql")),
3424- [(RelationEndpoint("wordpress", "mysql", "db", "client"),
3425- RelationEndpoint("mysql", "mysql", "server", "server"))])
3426- # symmetric - note the pair has rotated
3427- self.assertEqual(
3428- (yield self.service_state_manager.join_descriptors(
3429- "mysql", "wordpress")),
3430- [(RelationEndpoint("mysql", "mysql", "server", "server"),
3431- RelationEndpoint("wordpress", "mysql", "db", "client"))])
3432- yield self.add_service_from_charm("varnish")
3433- self.assertEqual(
3434- (yield self.service_state_manager.join_descriptors(
3435- "wordpress", "varnish")),
3436- [(RelationEndpoint("wordpress", "varnish", "cache", "client"),
3437- RelationEndpoint("varnish", "varnish", "webcache", "server"))])
3438-
3439- @inlineCallbacks
3440- def test_join_descriptors_service_name_relation_name(self):
3441- """Test joining descriptors ``<service name:relation name>``"""
3442- yield self.add_service_from_charm("wordpress")
3443- yield self.add_service_from_charm("mysql")
3444- self.assertEqual(
3445- (yield self.service_state_manager.join_descriptors(
3446- "wordpress:db", "mysql")),
3447- [(RelationEndpoint("wordpress", "mysql", "db", "client"),
3448- RelationEndpoint("mysql", "mysql", "server", "server"))])
3449- self.assertEqual(
3450- (yield self.service_state_manager.join_descriptors(
3451- "mysql:server", "wordpress")),
3452- [(RelationEndpoint("mysql", "mysql", "server", "server"),
3453- RelationEndpoint("wordpress", "mysql", "db", "client"))])
3454- self.assertEqual(
3455- (yield self.service_state_manager.join_descriptors(
3456- "mysql:server", "wordpress:db")),
3457- [(RelationEndpoint("mysql", "mysql", "server", "server"),
3458- RelationEndpoint("wordpress", "mysql", "db", "client"))])
3459-
3460- yield self.add_service_from_charm("varnish")
3461- self.assertEqual(
3462- (yield self.service_state_manager.join_descriptors(
3463- "wordpress:cache", "varnish")),
3464- [(RelationEndpoint("wordpress", "varnish", "cache", "client"),
3465- RelationEndpoint("varnish", "varnish", "webcache", "server"))])
3466- self.assertEqual(
3467- (yield self.service_state_manager.join_descriptors(
3468- "wordpress:cache", "varnish:webcache")),
3469- [(RelationEndpoint("wordpress", "varnish", "cache", "client"),
3470- RelationEndpoint("varnish", "varnish", "webcache", "server"))])
3471-
3472- @inlineCallbacks
3473- def test_join_peer_descriptors(self):
3474- """Test joining of peer relation descriptors"""
3475- yield self.add_service_from_charm("riak")
3476- self.assertEqual(
3477- (yield self.service_state_manager.join_descriptors(
3478- "riak", "riak")),
3479- [(RelationEndpoint("riak", "riak", "ring", "peer"),
3480- RelationEndpoint("riak", "riak", "ring", "peer"))])
3481- self.assertEqual(
3482- (yield self.service_state_manager.join_descriptors(
3483- "riak:ring", "riak")),
3484- [(RelationEndpoint("riak", "riak", "ring", "peer"),
3485- RelationEndpoint("riak", "riak", "ring", "peer"))])
3486- self.assertEqual(
3487- (yield self.service_state_manager.join_descriptors(
3488- "riak:ring", "riak:ring")),
3489- [(RelationEndpoint("riak", "riak", "ring", "peer"),
3490- RelationEndpoint("riak", "riak", "ring", "peer"))])
3491- self.assertEqual(
3492- (yield self.service_state_manager.join_descriptors(
3493- "riak:no-ring", "riak:ring")),
3494- [])
3495-
3496- @inlineCallbacks
3497- def test_join_descriptors_no_common_relation(self):
3498- """Test joining of descriptors that do not share a relation"""
3499- yield self.add_service_from_charm("mysql")
3500- yield self.add_service_from_charm("riak")
3501- yield self.add_service_from_charm("wordpress")
3502- yield self.add_service_from_charm("varnish")
3503- self.assertEqual(
3504- (yield self.service_state_manager.join_descriptors(
3505- "mysql", "riak")),
3506- [])
3507- self.assertEqual(
3508- (yield self.service_state_manager.join_descriptors(
3509- "mysql:server", "riak:ring")),
3510- [])
3511- self.assertEqual(
3512- (yield self.service_state_manager.join_descriptors(
3513- "varnish", "mysql")),
3514- [])
3515- self.assertEqual(
3516- (yield self.service_state_manager.join_descriptors(
3517- "riak:ring", "riak:admin")),
3518- [])
3519- self.assertEqual(
3520- (yield self.service_state_manager.join_descriptors(
3521- "riak", "wordpress")),
3522- [])
3523-
3524- @inlineCallbacks
3525- def test_join_descriptors_no_service_state(self):
3526- """Test joining of nonexistent services"""
3527- yield self.add_service_from_charm("wordpress")
3528- yield self.assertFailure(
3529- self.service_state_manager.join_descriptors("wordpress", "nosuch"),
3530- ServiceStateNotFound)
3531- yield self.assertFailure(
3532- self.service_state_manager.join_descriptors("notyet", "nosuch"),
3533- ServiceStateNotFound)
3534-
3535- @inlineCallbacks
3536- def test_watch_services_initial_callback(self):
3537- """Watch service processes initial state before returning.
3538-
3539- Note the callback is only executed if there is some meaningful state
3540- change.
3541- """
3542- results = []
3543-
3544- def callback(*args):
3545- results.append(True)
3546-
3547- yield self.service_state_manager.watch_service_states(callback)
3548- yield self.add_service("wordpress")
3549- yield self.poke_zk()
3550- self.assertTrue(results)
3551-
3552- @inlineCallbacks
3553- def test_watch_services_when_being_created(self):
3554- """
3555- It should be possible to start watching services even
3556- before they are created. In this case, the callback will
3557- be made when it's actually introduced.
3558- """
3559- wait_callback = [Deferred() for i in range(10)]
3560-
3561- calls = []
3562-
3563- def watch_services(old_services, new_services):
3564- calls.append((old_services, new_services))
3565- wait_callback[len(calls) - 1].callback(True)
3566-
3567- # Start watching.
3568- self.service_state_manager.watch_service_states(watch_services)
3569-
3570- # Callback is still untouched.
3571- self.assertEquals(calls, [])
3572-
3573- # Add a service, and wait for callback.
3574- yield self.add_service("wordpress")
3575- yield wait_callback[0]
3576-
3577- # The first callback must have been fired, and it must have None
3578- # as the first argument because that's the first service seen.
3579- self.assertEquals(len(calls), 1)
3580- old_services, new_services = calls[0]
3581- self.assertEquals(old_services, set())
3582- self.assertEquals(new_services, set(["wordpress"]))
3583-
3584- # Add a service again.
3585- yield self.add_service("mysql")
3586- yield wait_callback[1]
3587-
3588- # Now the watch callback must have been fired with two
3589- # different service sets. The old one, and the new one.
3590- self.assertEquals(len(calls), 2)
3591- old_services, new_services = calls[1]
3592- self.assertEquals(old_services, set(["wordpress"]))
3593- self.assertEquals(new_services, set(["mysql", "wordpress"]))
3594-
3595- @inlineCallbacks
3596- def test_watch_services_may_defer(self):
3597- """
3598- The watch services callback may return a deferred so that it
3599- performs some of its logic asynchronously. In this case, it
3600- must not be called a second time before its postponed logic
3601- is finished completely.
3602- """
3603- wait_callback = [Deferred() for i in range(10)]
3604- finish_callback = [Deferred() for i in range(10)]
3605-
3606- calls = []
3607-
3608- def watch_services(old_services, new_services):
3609- calls.append((old_services, new_services))
3610- wait_callback[len(calls) - 1].callback(True)
3611- return finish_callback[len(calls) - 1]
3612-
3613- # Start watching.
3614- self.service_state_manager.watch_service_states(watch_services)
3615-
3616- # Create the service.
3617- yield self.add_service("wordpress")
3618-
3619- # Hold off until callback is started.
3620- yield wait_callback[0]
3621-
3622- # Add another service.
3623- yield self.add_service("mysql")
3624-
3625- # Ensure we still have a single call.
3626- self.assertEquals(len(calls), 1)
3627-
3628- # Allow the first call to be completed, and wait on the
3629- # next one.
3630- finish_callback[0].callback(None)
3631- yield wait_callback[1]
3632- finish_callback[1].callback(None)
3633-
3634- # We should have the second change now.
3635- self.assertEquals(len(calls), 2)
3636- old_services, new_services = calls[1]
3637- self.assertEquals(old_services, set(["wordpress"]))
3638- self.assertEquals(new_services, set(["mysql", "wordpress"]))
3639-
3640- @inlineCallbacks
3641- def test_watch_services_with_changing_topology(self):
3642- """
3643- If the topology changes in an unrelated way, the services
3644- watch callback should not be called with two equal
3645- arguments.
3646- """
3647- wait_callback = [Deferred() for i in range(10)]
3648-
3649- calls = []
3650-
3651- def watch_services(old_services, new_services):
3652- calls.append((old_services, new_services))
3653- wait_callback[len(calls) - 1].callback(True)
3654-
3655- # Start watching.
3656- self.service_state_manager.watch_service_states(watch_services)
3657-
3658- # Callback is still untouched.
3659- self.assertEquals(calls, [])
3660-
3661- # Add a service, and wait for callback.
3662- yield self.add_service("wordpress")
3663- yield wait_callback[0]
3664-
3665- # Now change the topology in an unrelated way.
3666- yield self.machine_state_manager.add_machine_state(
3667- series_constraints)
3668-
3669- # Add a service again.
3670- yield self.add_service("mysql")
3671- yield wait_callback[1]
3672-
3673- # But it *shouldn't* have happened.
3674- self.assertEquals(len(calls), 2)
3675-
3676- @inlineCallbacks
3677- def test_watch_service_units_initial_callback(self):
3678- """Watch service unit processes initial state before returning.
3679-
3680- Note the callback is only executed if there is some meaningful state
3681- change.
3682- """
3683- results = []
3684-
3685- def callback(*args):
3686- results.append(True)
3687-
3688- service_state = yield self.add_service("wordpress")
3689- yield service_state.watch_service_unit_states(callback)
3690- yield service_state.add_unit_state()
3691- yield self.poke_zk()
3692- self.assertTrue(results)
3693-
3694- @inlineCallbacks
3695- def test_watch_service_units_when_being_created(self):
3696- """
3697- It should be possible to start watching service units even
3698- before they are created. In this case, the callback will be
3699- made when it's actually introduced.
3700- """
3701- wait_callback = [Deferred() for i in range(10)]
3702-
3703- calls = []
3704-
3705- def watch_service_units(old_service_units, new_service_units):
3706- calls.append((old_service_units, new_service_units))
3707- wait_callback[len(calls) - 1].callback(True)
3708-
3709- # Start watching.
3710- service_state = yield self.add_service("wordpress")
3711- service_state.watch_service_unit_states(watch_service_units)
3712-
3713- # Callback is still untouched.
3714- self.assertEquals(calls, [])
3715-
3716- # Add a service unit, and wait for callback.
3717- yield service_state.add_unit_state()
3718-
3719- yield wait_callback[0]
3720-
3721- # The first callback must have been fired, and it must have None
3722- # as the first argument because that's the first service seen.
3723- self.assertEquals(len(calls), 1)
3724- old_service_units, new_service_units = calls[0]
3725- self.assertEquals(old_service_units, set())
3726- self.assertEquals(new_service_units, set(["wordpress/0"]))
3727-
3728- # Add another service unit.
3729- yield service_state.add_unit_state()
3730- yield wait_callback[1]
3731-
3732- # Now the watch callback must have been fired with two
3733- # different service sets. The old one, and the new one.
3734- self.assertEquals(len(calls), 2)
3735- old_service_units, new_service_units = calls[1]
3736- self.assertEquals(old_service_units, set(["wordpress/0"]))
3737- self.assertEquals(new_service_units, set(["wordpress/0", "wordpress/1"]))
3738-
3739- @inlineCallbacks
3740- def test_watch_service_units_may_defer(self):
3741- """
3742- The watch service units callback may return a deferred so that
3743- it performs some of its logic asynchronously. In this case,
3744- it must not be called a second time before its postponed logic
3745- is finished completely.
3746- """
3747- wait_callback = [Deferred() for i in range(10)]
3748- finish_callback = [Deferred() for i in range(10)]
3749-
3750- calls = []
3751-
3752- def watch_service_units(old_service_units, new_service_units):
3753- calls.append((old_service_units, new_service_units))
3754- wait_callback[len(calls) - 1].callback(True)
3755- return finish_callback[len(calls) - 1]
3756-
3757- # Start watching.
3758- service_state = yield self.add_service("wordpress")
3759- service_state.watch_service_unit_states(watch_service_units)
3760-
3761- # Create the service unit.
3762- yield service_state.add_unit_state()
3763-
3764- # Hold off until callback is started.
3765- yield wait_callback[0]
3766-
3767- # Add another service unit.
3768- yield service_state.add_unit_state()
3769-
3770- # Ensure we still have a single call.
3771- self.assertEquals(len(calls), 1)
3772-
3773- # Allow the first call to be completed, and wait on the
3774- # next one.
3775- finish_callback[0].callback(None)
3776- yield wait_callback[1]
3777- finish_callback[1].callback(None)
3778-
3779- # We should have the second change now.
3780- self.assertEquals(len(calls), 2)
3781- old_service_units, new_service_units = calls[1]
3782- self.assertEquals(old_service_units, set(["wordpress/0"]))
3783- self.assertEquals(
3784- new_service_units, set(["wordpress/0", "wordpress/1"]))
3785-
3786- @inlineCallbacks
3787- def test_watch_service_units_with_changing_topology(self):
3788- """
3789- If the topology changes in an unrelated way, the services
3790- watch callback should not be called with two equal
3791- arguments.
3792- """
3793- wait_callback = [Deferred() for i in range(10)]
3794-
3795- calls = []
3796-
3797- def watch_service_units(old_service_units, new_service_units):
3798- calls.append((old_service_units, new_service_units))
3799- wait_callback[len(calls) - 1].callback(True)
3800-
3801- # Start watching.
3802- service_state = yield self.add_service("wordpress")
3803- service_state.watch_service_unit_states(watch_service_units)
3804-
3805- # Callback is still untouched.
3806- self.assertEquals(calls, [])
3807-
3808- # Add a service, and wait for callback.
3809- yield service_state.add_unit_state()
3810- yield wait_callback[0]
3811-
3812- # Now change the topology in an unrelated way.
3813- yield self.machine_state_manager.add_machine_state(
3814- series_constraints)
3815-
3816- # Add a service again.
3817- yield service_state.add_unit_state()
3818- yield wait_callback[1]
3819-
3820- # But it *shouldn't* have happened.
3821- self.assertEquals(len(calls), 2)
3822-
3823- @inlineCallbacks
3824- def test_service_config_get_set(self):
3825- """Validate that we can set and get service config options."""
3826- wordpress = yield self.add_service_from_charm("wordpress")
3827-
3828- # attempt to get the initialized service state
3829- config = yield wordpress.get_config()
3830-
3831- # the initial state is empty
3832- self.assertEqual(config, {"blog-title": "My Title"})
3833-
3834- # behaves as a normal dict
3835- self.assertRaises(KeyError, config.__getitem__, "missing")
3836-
3837- # various ways to set state
3838- config.update(dict(alpha="beta", one="two"))
3839- config["another"] = "value"
3840-
3841- # write the values
3842- yield config.write()
3843-
3844- # we should be able to read the config and see the same values
3845- # (in this case it would be the cached object)
3846- config2 = yield wordpress.get_config()
3847- self.assertEqual(config2, {"alpha": "beta",
3848- "one": "two",
3849- "another": "value",
3850- "blog-title": "My Title"})
3851-
3852- # now set a non-string value and recover it
3853- config2["number"] = 1
3854- config2["one"] = None
3855- yield config2.write()
3856-
3857- yield config.read()
3858- self.assertEquals(config["number"], 1)
3859- self.assertEquals(config["one"], None)
3860-
3861- @inlineCallbacks
3862- def test_service_config_get_returns_new(self):
3863- """Validate that we can set and get service config options."""
3864- wordpress = yield self.add_service_from_charm("wordpress")
3865-
3866- # attempt to get the initialized service state
3867- config = yield wordpress.get_config()
3868- config.update({"foo": "bar"})
3869- # Defaults come through
3870- self.assertEqual(config, {"foo": "bar", "blog-title": "My Title"})
3871-
3872- config2 = yield wordpress.get_config()
3873- self.assertEqual(config2, {"blog-title": "My Title"})
3874-
3875- yield config.write()
3876- self.assertEqual(config, {"foo": "bar", "blog-title": "My Title"})
3877-
3878- # Config2 is still empty (a different YAML State), with charm defaults.
3879- self.assertEqual(config2, {"blog-title": "My Title"})
3880-
3881- yield config2.read()
3882- self.assertEqual(config, {"foo": "bar", "blog-title": "My Title"})
3883-
3884- # The default was never written to storage.
3885- data, stat = yield self.client.get(
3886- "/services/%s/config" % wordpress.internal_id)
3887- self.assertEqual(yaml.load(data), {"foo": "bar"})
3888-
3889- @inlineCallbacks
3890- def test_get_charm_state(self):
3891- wordpress = yield self.add_service_from_charm("wordpress")
3892- charm = yield wordpress.get_charm_state()
3893-
3894- self.assertEqual(charm.name, "wordpress")
3895- metadata = yield charm.get_metadata()
3896- self.assertEqual(metadata.summary, "Blog engine")
3897+ @inlineCallbacks
3898+ def test_add_service(self):
3899+ """
3900+ Adding a service state should register it in zookeeper,
3901+ including the requested charm id.
3902+ """
3903+ yield self.service_state_manager.add_service_state(
3904+ "wordpress", self.charm_state, dummy_constraints)
3905+ yield self.service_state_manager.add_service_state(
3906+ "mysql", self.charm_state, dummy_constraints)
3907+ children = yield self.client.get_children("/services")
3908+
3909+ self.assertEquals(sorted(children),
3910+ ["service-0000000000", "service-0000000001"])
3911+
3912+ content, stat = yield self.client.get("/services/service-0000000000")
3913+ details = yaml.load(content)
3914+ self.assertTrue(details)
3915+ self.assertEquals(details.get("charm"), "local:series/dummy-1")
3916+ self.assertFalse(isinstance(details.get("charm"), unicode))
3917+ self.assertEquals(details.get("constraints"), series_constraints.data)
3918+
3919+ topology = yield self.get_topology()
3920+ self.assertEquals(topology.find_service_with_name("wordpress"),
3921+ "service-0000000000")
3922+ self.assertEquals(topology.find_service_with_name("mysql"),
3923+ "service-0000000001")
3924+
3925+ @inlineCallbacks
3926+ def test_add_service_with_duplicated_name(self):
3927+ """
3928+ If a service is added with a duplicated name, a meaningful
3929+ error should be raised.
3930+ """
3931+ yield self.service_state_manager.add_service_state(
3932+ "wordpress", self.charm_state, dummy_constraints)
3933+
3934+ try:
3935+ yield self.service_state_manager.add_service_state(
3936+ "wordpress", self.charm_state, dummy_constraints)
3937+ except ServiceStateNameInUse, e:
3938+ self.assertEquals(e.service_name, "wordpress")
3939+ else:
3940+ self.fail("Error not raised")
3941+
3942+ @inlineCallbacks
3943+ def test_get_service_and_check_attributes(self):
3944+ """
3945+ Getting a service state should be possible, and the service
3946+ state identification should be available through its
3947+ attributes.
3948+ """
3949+ yield self.service_state_manager.add_service_state(
3950+ "wordpress", self.charm_state, dummy_constraints)
3951+ service_state = yield self.service_state_manager.get_service_state(
3952+ "wordpress")
3953+ self.assertEquals(service_state.service_name, "wordpress")
3954+ self.assertEquals(service_state.internal_id, "service-0000000000")
3955+
3956+ @inlineCallbacks
3957+ def test_get_service_not_found(self):
3958+ """
3959+ Getting a service state which is not available should errback
3960+ a meaningful error.
3961+ """
3962+ try:
3963+ yield self.service_state_manager.get_service_state("wordpress")
3964+ except ServiceStateNotFound, e:
3965+ self.assertEquals(e.service_name, "wordpress")
3966+ else:
3967+ self.fail("Error not raised")
3968+
3969+ def test_get_unit_state(self):
3970+ """A unit state can be retrieved by name from the service manager."""
3971+ self.assertFailure(self.service_state_manager.get_unit_state(
3972+ "wordpress/1"), ServiceStateNotFound)
3973+
3974+ self.assertFailure(self.service_state_manager.get_unit_state(
3975+ "wordpress1"), ServiceUnitStateNotFound)
3976+
3977+ wordpress_state = yield self.service_state_manager.add_service_state(
3978+ "wordpress", self.charm_state, dummy_constraints)
3979+
3980+ self.assertFailure(self.service_state_manager.get_unit_state(
3981+ "wordpress/1"), ServiceUnitStateNotFound)
3982+
3983+ wordpress_unit = wordpress_state.add_unit_state()
3984+
3985+ unit_state = yield self.service_state_manager.get_unit_state(
3986+ "wordpress/1")
3987+
3988+ self.assertEqual(unit_state.internal_id, wordpress_unit.internal_id)
3989+
3990+ @inlineCallbacks
3991+ def test_get_service_charm_id(self):
3992+ """
3993+ The service state should make its respective charm id available.
3994+ """
3995+ yield self.service_state_manager.add_service_state(
3996+ "wordpress", self.charm_state, dummy_constraints)
3997+ service_state = yield self.service_state_manager.get_service_state(
3998+ "wordpress")
3999+ charm_id = yield service_state.get_charm_id()
4000+ self.assertEquals(charm_id, "local:series/dummy-1")
4001+
4002+ @inlineCallbacks
4003+ def test_set_service_charm_id(self):
4004+ """
4005+ The service state should allow its charm id to be set.
4006+ """
4007+ yield self.service_state_manager.add_service_state(
4008+ "wordpress", self.charm_state, dummy_constraints)
4009+ service_state = yield self.service_state_manager.get_service_state(
4010+ "wordpress")
4011+ yield service_state.set_charm_id("local:series/dummy-2")
4012+ charm_id = yield service_state.get_charm_id()
4013+ self.assertEquals(charm_id, "local:series/dummy-2")
4014+
4015+ @inlineCallbacks
4016+ def test_get_service_constraints(self):
4017+ """The service state should make constraints available"""
4018+ initial_constraints = dummy_cs.parse(["cpu=256", "arch=amd64"])
4019+ yield self.service_state_manager.add_service_state(
4020+ "wordpress", self.charm_state, initial_constraints)
4021+ service_state = yield self.service_state_manager.get_service_state(
4022+ "wordpress")
4023+ constraints = yield service_state.get_constraints()
4024+ self.assertEquals(
4025+ constraints, initial_constraints.with_series("series"))
4026+
4027+ @inlineCallbacks
4028+ def test_get_service_constraints_inherits(self):
4029+ """The service constraints should be combined with the environment's"""
4030+ yield self.push_env_constraints("arch=arm", "cpu=32")
4031+ service_constraints = dummy_cs.parse(["cpu=256"])
4032+ yield self.service_state_manager.add_service_state(
4033+ "wordpress", self.charm_state, service_constraints)
4034+ service_state = yield self.service_state_manager.get_service_state(
4035+ "wordpress")
4036+ constraints = yield service_state.get_constraints()
4037+ expected_base = dummy_cs.parse(["arch=arm", "cpu=256"])
4038+ self.assertEquals(constraints, expected_base.with_series("series"))
4039+
4040+ @inlineCallbacks
4041+ def test_get_missing_service_constraints(self):
4042+ """
4043+ Nodes created before the constraints mechanism was added should have
4044+ empty constraints.
4045+ """
4046+ yield self.client.delete("/constraints")
4047+ yield self.service_state_manager.add_service_state(
4048+ "wordpress", self.charm_state, dummy_constraints)
4049+ service_state = yield self.service_state_manager.get_service_state(
4050+ "wordpress")
4051+ path = "/services/" + service_state.internal_id
4052+ node = YAMLState(self.client, path)
4053+ yield node.read()
4054+ del node["constraints"]
4055+ yield node.write()
4056+ constraints = yield service_state.get_constraints()
4057+ self.assertEquals(constraints.data, {})
4058+
4059+ @inlineCallbacks
4060+ def test_get_missing_unit_constraints(self):
4061+ """
4062+ Nodes created before the constraints mechanism was added should have
4063+ empty constraints.
4064+ """
4065+ yield self.service_state_manager.add_service_state(
4066+ "wordpress", self.charm_state, dummy_constraints)
4067+ service_state = yield self.service_state_manager.get_service_state(
4068+ "wordpress")
4069+ unit_state = yield service_state.add_unit_state()
4070+ path = "/units/" + unit_state.internal_id
4071+ node = YAMLState(self.client, path)
4072+ yield node.read()
4073+ del node["constraints"]
4074+ yield node.write()
4075+ constraints = yield unit_state.get_constraints()
4076+ self.assertEquals(constraints.data, {})
4077+
4078+ @inlineCallbacks
4079+ def test_set_service_constraints(self):
4080+ """The service state should make constraints available for change"""
4081+ initial_constraints = dummy_cs.parse(["cpu=256", "arch=amd64"])
4082+ yield self.service_state_manager.add_service_state(
4083+ "wordpress", self.charm_state, initial_constraints)
4084+ service_state = yield self.service_state_manager.get_service_state(
4085+ "wordpress")
4086+ new_constraints = dummy_cs.parse(["mem=2G", "arch=arm"])
4087+ yield service_state.set_constraints(new_constraints)
4088+ retrieved_constraints = yield service_state.get_constraints()
4089+ self.assertEquals(
4090+ retrieved_constraints, new_constraints.with_series("series"))
4091+
4092+ @inlineCallbacks
4093+ def test_remove_service_state(self):
4094+ """
4095+ A service state can be removed along with its relations, units,
4096+ and zookeeper state.
4097+ """
4098+ service_state = yield self.service_state_manager.add_service_state(
4099+ "wordpress", self.charm_state, dummy_constraints)
4100+
4101+ relation_state = yield self.add_relation(
4102+ "rel-type2", "global", [service_state, "app", "server"])
4103+
4104+ unit_state = yield service_state.add_unit_state()
4105+ machine_state = yield self.machine_state_manager.add_machine_state(
4106+ series_constraints)
4107+ yield unit_state.assign_to_machine(machine_state)
4108+
4109+ yield self.service_state_manager.remove_service_state(service_state)
4110+
4111+ topology = yield self.get_topology()
4112+ self.assertFalse(topology.has_relation(relation_state.internal_id))
4113+ self.assertFalse(topology.has_service(service_state.internal_id))
4114+ self.assertFalse(
4115+ topology.get_service_units_in_machine(machine_state.internal_id))
4116+
4117+ exists = yield self.client.exists(
4118+ "/services/%s" % service_state.internal_id)
4119+ self.assertFalse(exists)
4120+
4121+ @inlineCallbacks
4122+ def test_add_service_unit_and_check_attributes(self):
4123+ """
4124+ A service state should enable adding a new service unit
4125+ under it, and again the unit should offer attributes allowing
4126+ its identification.
4127+ """
4128+ service_state0 = yield self.service_state_manager.add_service_state(
4129+ "wordpress", self.charm_state, dummy_constraints)
4130+ service_state1 = yield self.service_state_manager.add_service_state(
4131+ "mysql", self.charm_state, dummy_constraints)
4132+
4133+ unit_state0 = yield service_state0.add_unit_state()
4134+ unit_state1 = yield service_state1.add_unit_state()
4135+ unit_state2 = yield service_state0.add_unit_state()
4136+ unit_state3 = yield service_state1.add_unit_state()
4137+
4138+ children = yield self.client.get_children("/units")
4139+ self.assertEquals(sorted(children),
4140+ ["unit-0000000000", "unit-0000000001",
4141+ "unit-0000000002", "unit-0000000003"])
4142+
4143+ self.assertEquals(unit_state0.service_name, "wordpress")
4144+ self.assertEquals(unit_state0.internal_id, "unit-0000000000")
4145+ self.assertEquals(unit_state0.unit_name, "wordpress/0")
4146+
4147+ self.assertEquals(unit_state1.service_name, "mysql")
4148+ self.assertEquals(unit_state1.internal_id, "unit-0000000001")
4149+ self.assertEquals(unit_state1.unit_name, "mysql/0")
4150+
4151+ self.assertEquals(unit_state2.service_name, "wordpress")
4152+ self.assertEquals(unit_state2.internal_id, "unit-0000000002")
4153+ self.assertEquals(unit_state2.unit_name, "wordpress/1")
4154+
4155+ self.assertEquals(unit_state3.service_name, "mysql")
4156+ self.assertEquals(unit_state3.internal_id, "unit-0000000003")
4157+ self.assertEquals(unit_state3.unit_name, "mysql/1")
4158+
4159+ topology = yield self.get_topology()
4160+
4161+ self.assertTrue(
4162+ topology.has_service_unit("service-0000000000",
4163+ "unit-0000000000"))
4164+ self.assertTrue(
4165+ topology.has_service_unit("service-0000000001",
4166+ "unit-0000000001"))
4167+ self.assertTrue(
4168+ topology.has_service_unit("service-0000000000",
4169+ "unit-0000000002"))
4170+ self.assertTrue(
4171+ topology.has_service_unit("service-0000000001",
4172+ "unit-0000000003"))
4173+
4174+ def get_presence_path(
4175+ self, relation_state, relation_role, unit_state, container=None):
4176+ container = container.internal_id if container else None
4177+ presence_path = "/".join(filter(None, [
4178+ "/relations",
4179+ relation_state.internal_id,
4180+ container,
4181+ relation_role,
4182+ unit_state.internal_id]))
4183+ return presence_path
4184+
4185+ @inlineCallbacks
4186+ def test_add_service_unit_with_container(self):
4187+ """
4188+ Validate adding units with containers specified and recovering that.
4189+ """
4190+ mysql_ep = RelationEndpoint("mysql", "juju-info", "juju-info",
4191+ "server", "global")
4192+ logging_ep = RelationEndpoint("logging", "juju-info", "juju-info",
4193+ "client", "container")
4194+
4195+ logging_charm_state = yield self.get_subordinate_charm()
4196+ self.assertTrue(logging_charm_state.is_subordinate())
4197+ log_state = yield self.service_state_manager.add_service_state(
4198+ "logging", logging_charm_state, dummy_constraints)
4199+ mysql_state = yield self.service_state_manager.add_service_state(
4200+ "mysql", self.charm_state, dummy_constraints)
4201+
4202+ relation_state, service_states = (yield
4203+ self.relation_state_manager.add_relation_state(
4204+ mysql_ep, logging_ep))
4205+
4206+ unit_state1 = yield mysql_state.add_unit_state()
4207+ unit_state0 = yield log_state.add_unit_state(container=unit_state1)
4208+
4209+ unit_state3 = yield mysql_state.add_unit_state()
4210+ unit_state2 = yield log_state.add_unit_state(container=unit_state3)
4211+
4212+ self.assertEquals((yield unit_state1.get_container()), None)
4213+ self.assertEquals((yield unit_state0.get_container()), unit_state1)
4214+ self.assertEquals((yield unit_state2.get_container()), unit_state3)
4215+ self.assertEquals((yield unit_state3.get_container()), None)
4216+
4217+ for unit_state in (unit_state1, unit_state3):
4218+ yield unit_state.set_private_address(
4219+ "%s.example.com" % (
4220+ unit_state.unit_name.replace("/", "-")))
4221+
4222+ # construct the proper relation state
4223+ mystate = pick_attr(service_states, relation_role="server")
4224+ logstate = pick_attr(service_states, relation_role="client")
4225+ yield logstate.add_unit_state(unit_state0)
4226+ yield logstate.add_unit_state(unit_state2)
4227+
4228+ yield mystate.add_unit_state(unit_state1)
4229+ yield mystate.add_unit_state(unit_state3)
4230+
4231+ @inlineCallbacks
4232+ def verify_container(relation_state, service_relation_state,
4233+ unit_state, container):
4234+ presence_path = self.get_presence_path(
4235+ relation_state,
4236+ service_relation_state.relation_role,
4237+ unit_state,
4238+ container)
4239+
4240+ content, stat = yield self.client.get(presence_path)
4241+ self.assertTrue(stat)
4242+ self.assertEqual(content, '')
4243+ # verify the node data on the relation role nodes
4244+ role_path = os.path.dirname(presence_path)
4245+ # role path
4246+ content, stat = yield self.client.get(role_path)
4247+ self.assertTrue(stat)
4248+ node_info = yaml.load(content)
4249+
4250+ self.assertEqual(
4251+ node_info["name"],
4252+ service_relation_state.relation_name)
4253+ self.assertEqual(
4254+ node_info["role"],
4255+ service_relation_state.relation_role)
4256+
4257+ settings_path = os.path.dirname(
4258+ os.path.dirname(presence_path)) + "/settings/" + \
4259+ unit_state.internal_id
4260+ content, stat = yield self.client.get(settings_path)
4261+ self.assertTrue(stat)
4262+ settings_info = yaml.load(content)
4263+
4264+ # Verify that private address was set
4265+ # we verify the content elsewhere
4266+ self.assertTrue(settings_info["private-address"])
4267+
4268+ # verify all the units are constructed as expected
4269+ # first the client roles with another container
4270+ yield verify_container(relation_state, logstate,
4271+ unit_state0, unit_state1)
4272+
4273+ yield verify_container(relation_state, logstate,
4274+ unit_state2, unit_state3)
4275+
4276+ # and now the principals (which are their own relation containers)
4277+ yield verify_container(relation_state, logstate,
4278+ unit_state0, unit_state1)
4279+
4280+ yield verify_container(relation_state, mystate,
4281+ unit_state1, unit_state1)
4282+
4283+ @inlineCallbacks
4284+ def test_get_container_no_principal(self):
4285+ """Get container should handle no principal."""
4286+ mysql_ep = RelationEndpoint("mysql", "juju-info", "juju-info",
4287+ "server", "global")
4288+ logging_ep = RelationEndpoint("logging", "juju-info", "juju-info",
4289+ "client", "container")
4290+
4291+ logging_charm_state = yield self.get_subordinate_charm()
4292+ self.assertTrue(logging_charm_state.is_subordinate())
4293+ log_state = yield self.service_state_manager.add_service_state(
4294+ "logging", logging_charm_state, dummy_constraints)
4295+ mysql_state = yield self.service_state_manager.add_service_state(
4296+ "mysql", self.charm_state, dummy_constraints)
4297+
4298+ relation_state, service_states = (yield
4299+ self.relation_state_manager.add_relation_state(
4300+ mysql_ep, logging_ep))
4301+
4302+ unit_state1 = yield mysql_state.add_unit_state()
4303+ unit_state0 = yield log_state.add_unit_state(container=unit_state1)
4304+
4305+ unit_state3 = yield mysql_state.add_unit_state()
4306+ unit_state2 = yield log_state.add_unit_state(container=unit_state3)
4307+
4308+ self.assertEquals((yield unit_state1.get_container()), None)
4309+ self.assertEquals((yield unit_state0.get_container()), unit_state1)
4310+ self.assertEquals((yield unit_state2.get_container()), unit_state3)
4311+ self.assertEquals((yield unit_state3.get_container()), None)
4312+
4313+ # now remove a principal node and test again
4314+
4315+ yield mysql_state.remove_unit_state(unit_state1)
4316+ container = yield unit_state0.get_container()
4317+ self.assertEquals(container, None)
4318+
4319+ # the other pair is still fine
4320+ self.assertEquals((yield unit_state2.get_container()), unit_state3)
4321+ self.assertEquals((yield unit_state3.get_container()), None)
4322+
4323+
4324+ @inlineCallbacks
4325+ def test_add_service_unit_with_changing_state(self):
4326+ """
4327+ When adding a service unit, there's a chance that the
4328+ service will go away mid-way through. Rather than blowing
4329+ up randomly, a nice error should be raised.
4330+ """
4331+ service_state = yield self.service_state_manager.add_service_state(
4332+ "wordpress", self.charm_state, dummy_constraints)
4333+
4334+ yield self.remove_service(service_state.internal_id)
4335+
4336+ d = service_state.add_unit_state()
4337+ yield self.assertFailure(d, StateChanged)
4338+
4339+ @inlineCallbacks
4340+ def test_get_unit_names(self):
4341+ """A service's units names are retrievable."""
4342+ service_state = yield self.service_state_manager.add_service_state(
4343+ "wordpress", self.charm_state, dummy_constraints)
4344+
4345+ expected_names = []
4346+ for i in range(3):
4347+ unit_state = yield service_state.add_unit_state()
4348+ expected_names.append(unit_state.unit_name)
4349+
4350+ unit_names = yield service_state.get_unit_names()
4351+ self.assertEqual(unit_names, expected_names)
4352+
4353+ @inlineCallbacks
4354+ def test_remove_service_unit(self):
4355+ """Removing a service unit removes all state associated.
4356+ """
4357+ service_state = yield self.service_state_manager.add_service_state(
4358+ "wordpress", self.charm_state, dummy_constraints)
4359+
4360+ unit_state = yield service_state.add_unit_state()
4361+
4362+ # Assign to a machine
4363+ machine_state = yield self.machine_state_manager.add_machine_state(
4364+ series_constraints)
4365+ yield unit_state.assign_to_machine(machine_state)
4366+ # Connect a unit agent
4367+ yield unit_state.connect_agent()
4368+
4369+ # Now try and destroy it.
4370+ yield service_state.remove_unit_state(unit_state)
4371+
4372+ # Verify destruction.
4373+ topology = yield self.get_topology()
4374+ self.assertTrue(topology.has_service(service_state.internal_id))
4375+ self.assertFalse(
4376+ topology.has_service_unit(
4377+ service_state.internal_id, unit_state.internal_id))
4378+
4379+ exists = yield self.client.exists("/units/%s" % unit_state.internal_id)
4380+ self.assertFalse(exists)
4381+
4382+ def test_remove_service_unit_nonexistant(self):
4383+ """Removing a non existant service unit, is fine."""
4384+
4385+ service_state = yield self.service_state_manager.add_service_state(
4386+ "wordpress", self.charm_state, dummy_constraints)
4387+ unit_state = yield service_state.add_unit_state()
4388+ yield service_state.remove_unit_state(unit_state)
4389+ yield service_state.remove_unit_state(unit_state)
4390+
4391+ @inlineCallbacks
4392+ def test_get_all_service_states(self):
4393+ services = yield self.service_state_manager.get_all_service_states()
4394+ self.assertFalse(services)
4395+
4396+ yield self.service_state_manager.add_service_state(
4397+ "wordpress", self.charm_state, dummy_constraints)
4398+ services = yield self.service_state_manager.get_all_service_states()
4399+ self.assertEquals(len(services), 1)
4400+
4401+ yield self.service_state_manager.add_service_state(
4402+ "mysql", self.charm_state, dummy_constraints)
4403+ services = yield self.service_state_manager.get_all_service_states()
4404+ self.assertEquals(len(services), 2)
4405+
4406+ @inlineCallbacks
4407+ def test_get_service_unit(self):
4408+ """
4409+ Getting back service units should be possible using the
4410+ user-oriented id.
4411+ """
4412+ service_state0 = yield self.service_state_manager.add_service_state(
4413+ "wordpress", self.charm_state, dummy_constraints)
4414+ service_state1 = yield self.service_state_manager.add_service_state(
4415+ "mysql", self.charm_state, dummy_constraints)
4416+
4417+ yield service_state0.add_unit_state()
4418+ yield service_state1.add_unit_state()
4419+ yield service_state0.add_unit_state()
4420+ yield service_state1.add_unit_state()
4421+
4422+ unit_state0 = yield service_state0.get_unit_state("wordpress/0")
4423+ unit_state1 = yield service_state1.get_unit_state("mysql/0")
4424+ unit_state2 = yield service_state0.get_unit_state("wordpress/1")
4425+ unit_state3 = yield service_state1.get_unit_state("mysql/1")
4426+
4427+ self.assertEquals(unit_state0.internal_id, "unit-0000000000")
4428+ self.assertEquals(unit_state1.internal_id, "unit-0000000001")
4429+ self.assertEquals(unit_state2.internal_id, "unit-0000000002")
4430+ self.assertEquals(unit_state3.internal_id, "unit-0000000003")
4431+
4432+ self.assertEquals(unit_state0.unit_name, "wordpress/0")
4433+ self.assertEquals(unit_state1.unit_name, "mysql/0")
4434+ self.assertEquals(unit_state2.unit_name, "wordpress/1")
4435+ self.assertEquals(unit_state3.unit_name, "mysql/1")
4436+
4437+ @inlineCallbacks
4438+ def test_get_all_unit_states(self):
4439+ service_state0 = yield self.service_state_manager.add_service_state(
4440+ "wordpress", self.charm_state, dummy_constraints)
4441+ service_state1 = yield self.service_state_manager.add_service_state(
4442+ "mysql", self.charm_state, dummy_constraints)
4443+
4444+ yield service_state0.add_unit_state()
4445+ yield service_state1.add_unit_state()
4446+ yield service_state0.add_unit_state()
4447+ yield service_state1.add_unit_state()
4448+
4449+ unit_state0 = yield service_state0.get_unit_state("wordpress/0")
4450+ unit_state1 = yield service_state1.get_unit_state("mysql/0")
4451+ unit_state2 = yield service_state0.get_unit_state("wordpress/1")
4452+ unit_state3 = yield service_state1.get_unit_state("mysql/1")
4453+
4454+ wordpress_units = yield service_state0.get_all_unit_states()
4455+ self.assertEquals(
4456+ set(wordpress_units), set((unit_state0, unit_state2)))
4457+
4458+ mysql_units = yield service_state1.get_all_unit_states()
4459+ self.assertEquals(set(mysql_units), set((unit_state1, unit_state3)))
4460+
4461+ @inlineCallbacks
4462+ def test_get_all_unit_states_with_changing_state(self):
4463+ """
4464+ When getting the service unit states, there's a chance that
4465+ the service will go away mid-way through. Rather than blowing
4466+ up randomly, a nice error should be raised.
4467+ """
4468+ service_state = yield self.service_state_manager.add_service_state(
4469+ "wordpress", self.charm_state, dummy_constraints)
4470+ yield service_state.add_unit_state()
4471+ unit_state = (yield service_state.get_all_unit_states())[0]
4472+ self.assertEqual(unit_state.unit_name, "wordpress/0")
4473+ yield self.remove_service(service_state.internal_id)
4474+ yield self.assertFailure(
4475+ service_state.get_all_unit_states(), StateChanged)
4476+
4477+ @inlineCallbacks
4478+ def test_set_functions(self):
4479+ wordpress = yield self.service_state_manager.add_service_state(
4480+ "wordpress", self.charm_state, dummy_constraints)
4481+ mysql = yield self.service_state_manager.add_service_state(
4482+ "mysql", self.charm_state, dummy_constraints)
4483+
4484+ s1 = yield self.service_state_manager.get_service_state(
4485+ "wordpress")
4486+ s2 = yield self.service_state_manager.get_service_state(
4487+ "mysql")
4488+ self.assertEquals(hash(s1), hash(wordpress))
4489+ self.assertEquals(hash(s2), hash(mysql))
4490+
4491+ self.assertNotEqual(s1, object())
4492+ self.assertNotEqual(s1, s2)
4493+
4494+ self.assertEquals(s1, wordpress)
4495+ self.assertEquals(s2, mysql)
4496+
4497+ us0 = yield wordpress.add_unit_state()
4498+ us1 = yield wordpress.add_unit_state()
4499+
4500+ unit_state0 = yield wordpress.get_unit_state("wordpress/0")
4501+ unit_state1 = yield wordpress.get_unit_state("wordpress/1")
4502+
4503+ self.assertEquals(us0, unit_state0)
4504+ self.assertEquals(us1, unit_state1)
4505+ self.assertEquals(hash(us1), hash(unit_state1))
4506+
4507+ self.assertNotEqual(us0, object())
4508+ self.assertNotEqual(us0, us1)
4509+
4510+ @inlineCallbacks
4511+ def test_get_service_unit_not_found(self):
4512+ """
4513+ Attempting to retrieve a non-existent service unit should
4514+ result in an errback.
4515+ """
4516+ service_state0 = yield self.service_state_manager.add_service_state(
4517+ "wordpress", self.charm_state, dummy_constraints)
4518+ service_state1 = yield self.service_state_manager.add_service_state(
4519+ "mysql", self.charm_state, dummy_constraints)
4520+
4521+ # Add some state in a different service to make it a little
4522+ # bit more prone to breaking in case of errors.
4523+ yield service_state1.add_unit_state()
4524+
4525+ try:
4526+ yield service_state0.get_unit_state("wordpress/0")
4527+ except ServiceUnitStateNotFound, e:
4528+ self.assertEquals(e.unit_name, "wordpress/0")
4529+ else:
4530+ self.fail("Error not raised")
4531+
4532+ @inlineCallbacks
4533+ def test_get_set_public_address(self):
4534+ service_state = yield self.service_state_manager.add_service_state(
4535+ "wordpress", self.charm_state, dummy_constraints)
4536+ unit_state = yield service_state.add_unit_state()
4537+ self.assertEqual((yield unit_state.get_public_address()), None)
4538+ yield unit_state.set_public_address("example.foobar.com")
4539+ yield self.assertEqual(
4540+ (yield unit_state.get_public_address()),
4541+ "example.foobar.com")
4542+
4543+ @inlineCallbacks
4544+ def test_get_set_private_address(self):
4545+ service_state = yield self.service_state_manager.add_service_state(
4546+ "wordpress", self.charm_state, dummy_constraints)
4547+ unit_state = yield service_state.add_unit_state()
4548+ self.assertEqual((yield unit_state.get_private_address()), None)
4549+ yield unit_state.set_private_address("example.local")
4550+ yield self.assertEqual(
4551+ (yield unit_state.get_private_address()),
4552+ "example.local")
4553+
4554+ @inlineCallbacks
4555+ def test_get_service_unit_with_changing_state(self):
4556+ """
4557+ If a service is removed during operation, get_service_unit()
4558+ should raise a nice error.
4559+ """
4560+ service_state = yield self.service_state_manager.add_service_state(
4561+ "wordpress", self.charm_state, dummy_constraints)
4562+
4563+ yield self.remove_service(service_state.internal_id)
4564+
4565+ d = service_state.get_unit_state("wordpress/0")
4566+ yield self.assertFailure(d, StateChanged)
4567+
4568+ @inlineCallbacks
4569+ def test_get_service_unit_with_bad_service_name(self):
4570+ """
4571+ Service unit names contain a service name embedded into
4572+ them. The service name requested when calling get_unit_state()
4573+ must match that of the object being used.
4574+ """
4575+ service_state0 = yield self.service_state_manager.add_service_state(
4576+ "wordpress", self.charm_state, dummy_constraints)
4577+ service_state1 = yield self.service_state_manager.add_service_state(
4578+ "mysql", self.charm_state, dummy_constraints)
4579+
4580+ # Add some state in a different service to make it a little
4581+ # bit more prone to breaking in case of errors.
4582+ yield service_state1.add_unit_state()
4583+
4584+ try:
4585+ yield service_state0.get_unit_state("mysql/0")
4586+ except BadServiceStateName, e:
4587+ self.assertEquals(e.expected_name, "wordpress")
4588+ self.assertEquals(e.obtained_name, "mysql")
4589+ else:
4590+ self.fail("Error not raised")
4591+
4592+ @inlineCallbacks
4593+ def test_assign_unit_to_machine(self):
4594+ service_state = yield self.service_state_manager.add_service_state(
4595+ "wordpress", self.charm_state, dummy_constraints)
4596+ unit_state = yield service_state.add_unit_state()
4597+ machine_state = yield self.machine_state_manager.add_machine_state(
4598+ series_constraints)
4599+
4600+ yield unit_state.assign_to_machine(machine_state)
4601+
4602+ topology = yield self.get_topology()
4603+
4604+ self.assertEquals(
4605+ topology.get_service_unit_machine(service_state.internal_id,
4606+ unit_state.internal_id),
4607+ machine_state.internal_id)
4608+
4609+ @inlineCallbacks
4610+ def test_assign_unit_to_machine_with_changing_state(self):
4611+ service_state = yield self.service_state_manager.add_service_state(
4612+ "wordpress", self.charm_state, dummy_constraints)
4613+ unit_state = yield service_state.add_unit_state()
4614+ machine_state = yield self.machine_state_manager.add_machine_state(
4615+ series_constraints)
4616+
4617+ yield self.remove_service_unit(service_state.internal_id,
4618+ unit_state.internal_id)
4619+
4620+ d = unit_state.assign_to_machine(machine_state)
4621+ yield self.assertFailure(d, StateChanged)
4622+
4623+ yield self.remove_service(service_state.internal_id)
4624+
4625+ d = unit_state.assign_to_machine(machine_state)
4626+ yield self.assertFailure(d, StateChanged)
4627+
4628+ @inlineCallbacks
4629+ def test_unassign_unit_from_machine(self):
4630+ service_state = yield self.service_state_manager.add_service_state(
4631+ "wordpress", self.charm_state, dummy_constraints)
4632+ unit_state = yield service_state.add_unit_state()
4633+ machine_state = yield self.machine_state_manager.add_machine_state(
4634+ series_constraints)
4635+
4636+ yield unit_state.assign_to_machine(machine_state)
4637+ yield unit_state.unassign_from_machine()
4638+
4639+ topology = yield self.get_topology()
4640+
4641+ self.assertEquals(
4642+ topology.get_service_unit_machine(service_state.internal_id,
4643+ unit_state.internal_id),
4644+ None)
4645+
4646+ @inlineCallbacks
4647+ def test_get_set_clear_resolved(self):
4648+ """The a unit can be set to resolved to mark a future transition, with
4649+ an optional retry flag."""
4650+
4651+ unit_state = yield self.get_unit_state()
4652+
4653+ self.assertIdentical((yield unit_state.get_resolved()), None)
4654+ yield unit_state.set_resolved(NO_HOOKS)
4655+
4656+ yield self.assertFailure(
4657+ unit_state.set_resolved(NO_HOOKS),
4658+ ServiceUnitResolvedAlreadyEnabled)
4659+ yield self.assertEqual(
4660+
4661+ (yield unit_state.get_resolved()), {"retry": NO_HOOKS})
4662+
4663+ yield unit_state.clear_resolved()
4664+ self.assertIdentical((yield unit_state.get_resolved()), None)
4665+ yield unit_state.clear_resolved()
4666+
4667+ yield self.assertFailure(unit_state.set_resolved(None), ValueError)
4668+
4669+ @inlineCallbacks
4670+ def test_watch_resolved(self):
4671+ """A unit resolved watch can be instituted on a permanent basis."""
4672+ unit_state = yield self.get_unit_state()
4673+
4674+ results = []
4675+
4676+ def callback(value):
4677+ results.append(value)
4678+
4679+ unit_state.watch_resolved(callback)
4680+ yield unit_state.set_resolved(RETRY_HOOKS)
4681+ yield unit_state.clear_resolved()
4682+ yield unit_state.set_resolved(NO_HOOKS)
4683+
4684+ yield self.poke_zk()
4685+
4686+ self.assertEqual(len(results), 4)
4687+ self.assertIdentical(results.pop(0), False)
4688+ self.assertEqual(results.pop(0).type_name, "created")
4689+ self.assertEqual(results.pop(0).type_name, "deleted")
4690+ self.assertEqual(results.pop(0).type_name, "created")
4691+
4692+ self.assertEqual(
4693+ (yield unit_state.get_resolved()),
4694+ {"retry": NO_HOOKS})
4695+
4696+ @inlineCallbacks
4697+ def test_watch_resolved_processes_current_state(self):
4698+ """The watch method processes the current state before returning."""
4699+ unit_state = yield self.get_unit_state()
4700+
4701+ results = []
4702+
4703+ @inlineCallbacks
4704+ def callback(value):
4705+ results.append((yield unit_state.get_resolved()))
4706+
4707+ yield unit_state.watch_resolved(callback)
4708+ self.assertTrue(results)
4709+
4710+ @inlineCallbacks
4711+ def test_stop_watch_resolved(self):
4712+ """A unit resolved watch can be instituted on a permanent basis.
4713+
4714+ However the callback can raise StopWatcher at anytime to stop the watch
4715+ """
4716+ unit_state = yield self.get_unit_state()
4717+
4718+ results = []
4719+
4720+ def callback(value):
4721+ results.append(value)
4722+ if len(results) == 1:
4723+ raise StopWatcher()
4724+ if len(results) == 3:
4725+ raise StopWatcher()
4726+
4727+ unit_state.watch_resolved(callback)
4728+ yield unit_state.set_resolved(RETRY_HOOKS)
4729+ yield unit_state.clear_resolved()
4730+ yield self.poke_zk()
4731+
4732+ unit_state.watch_resolved(callback)
4733+ yield unit_state.set_resolved(NO_HOOKS)
4734+ yield unit_state.clear_resolved()
4735+
4736+ yield self.poke_zk()
4737+
4738+ self.assertEqual(len(results), 3)
4739+ self.assertIdentical(results.pop(0), False)
4740+ self.assertIdentical(results.pop(0), False)
4741+ self.assertEqual(results.pop(0).type_name, "created")
4742+
4743+ self.assertEqual(
4744+ (yield unit_state.get_resolved()), None)
4745+
4746+ @inlineCallbacks
4747+ def test_get_set_clear_relation_resolved(self):
4748+ """The a unit's realtions can be set to resolved to mark a
4749+ future transition, with an optional retry flag."""
4750+
4751+ unit_state = yield self.get_unit_state()
4752+
4753+ self.assertIdentical((yield unit_state.get_relation_resolved()), None)
4754+ yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
4755+
4756+ # Trying to set a conflicting raises an error
4757+ yield self.assertFailure(
4758+ unit_state.set_relation_resolved({"0": NO_HOOKS}),
4759+ ServiceUnitRelationResolvedAlreadyEnabled)
4760+
4761+ # Doing the same thing is fine
4762+ yield unit_state.set_relation_resolved({"0": RETRY_HOOKS}),
4763+
4764+ # Its fine to put in new values
4765+ yield unit_state.set_relation_resolved({"21": RETRY_HOOKS})
4766+ yield self.assertEqual(
4767+ (yield unit_state.get_relation_resolved()),
4768+ {"0": RETRY_HOOKS, "21": RETRY_HOOKS})
4769+
4770+ yield unit_state.clear_relation_resolved()
4771+ self.assertIdentical((yield unit_state.get_relation_resolved()), None)
4772+ yield unit_state.clear_relation_resolved()
4773+
4774+ yield self.assertFailure(
4775+ unit_state.set_relation_resolved(True), ValueError)
4776+ yield self.assertFailure(
4777+ unit_state.set_relation_resolved(None), ValueError)
4778+
4779+ @inlineCallbacks
4780+ def test_watch_relation_resolved(self):
4781+ """A unit resolved watch can be instituted on a permanent basis."""
4782+ unit_state = yield self.get_unit_state()
4783+
4784+ results = []
4785+
4786+ def callback(value):
4787+ results.append(value)
4788+
4789+ unit_state.watch_relation_resolved(callback)
4790+ yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
4791+ yield unit_state.clear_relation_resolved()
4792+ yield unit_state.set_relation_resolved({"0": NO_HOOKS})
4793+
4794+ yield self.poke_zk()
4795+
4796+ self.assertEqual(len(results), 4)
4797+ self.assertIdentical(results.pop(0), False)
4798+ self.assertEqual(results.pop(0).type_name, "created")
4799+ self.assertEqual(results.pop(0).type_name, "deleted")
4800+ self.assertEqual(results.pop(0).type_name, "created")
4801+
4802+ self.assertEqual(
4803+ (yield unit_state.get_relation_resolved()),
4804+ {"0": NO_HOOKS})
4805+
4806+ @inlineCallbacks
4807+ def test_watch_relation_resolved_processes_current_state(self):
4808+ """The watch method returns only after processing the current state."""
4809+ unit_state = yield self.get_unit_state()
4810+
4811+ results = []
4812+
4813+ @inlineCallbacks
4814+ def callback(value):
4815+ results.append((yield unit_state.get_relation_resolved()))
4816+ yield unit_state.watch_relation_resolved(callback)
4817+ self.assertTrue(results)
4818+
4819+ @inlineCallbacks
4820+ def test_stop_watch_relation_resolved(self):
4821+ """A unit resolved watch can be instituted on a permanent basis."""
4822+ unit_state = yield self.get_unit_state()
4823+
4824+ results = []
4825+
4826+ def callback(value):
4827+ results.append(value)
4828+
4829+ if len(results) == 1:
4830+ raise StopWatcher()
4831+
4832+ if len(results) == 3:
4833+ raise StopWatcher()
4834+
4835+ unit_state.watch_relation_resolved(callback)
4836+ yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
4837+ yield unit_state.clear_relation_resolved()
4838+ yield self.poke_zk()
4839+ self.assertEqual(len(results), 1)
4840+
4841+ unit_state.watch_relation_resolved(callback)
4842+ yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
4843+ yield unit_state.clear_relation_resolved()
4844+ yield self.poke_zk()
4845+ self.assertEqual(len(results), 3)
4846+ self.assertIdentical(results.pop(0), False)
4847+ self.assertIdentical(results.pop(0), False)
4848+ self.assertEqual(results.pop(0).type_name, "created")
4849+
4850+ self.assertEqual(
4851+ (yield unit_state.get_relation_resolved()), None)
4852+
4853+ @inlineCallbacks
4854+ def test_watch_resolved_slow_callback(self):
4855+ """A slow watch callback is still invoked serially."""
4856+ unit_state = yield self.get_unit_state()
4857+
4858+ callbacks = [Deferred() for i in range(5)]
4859+ results = []
4860+ contents = []
4861+
4862+ @inlineCallbacks
4863+ def watch(value):
4864+ results.append(value)
4865+ yield callbacks[len(results) - 1]
4866+ contents.append((yield unit_state.get_resolved()))
4867+
4868+ callbacks[0].callback(True)
4869+ yield unit_state.watch_resolved(watch)
4870+
4871+ # These get collapsed into a single event
4872+ yield unit_state.set_resolved(RETRY_HOOKS)
4873+ yield unit_state.clear_resolved()
4874+ yield self.poke_zk()
4875+
4876+ # Verify the callback hasn't completed
4877+ self.assertEqual(len(results), 2)
4878+ self.assertEqual(len(contents), 1)
4879+
4880+ # Let it finish
4881+ callbacks[1].callback(True)
4882+ yield self.poke_zk()
4883+
4884+ # Verify result counts
4885+ self.assertEqual(len(results), 3)
4886+ self.assertEqual(len(contents), 2)
4887+
4888+ # Verify result values. Even though we have created event, the
4889+ # setting retrieved shows the hook is not enabled.
4890+ self.assertEqual(results[-1].type_name, "deleted")
4891+ self.assertEqual(contents[-1], None)
4892+
4893+ yield unit_state.set_resolved(NO_HOOKS)
4894+ callbacks[2].callback(True)
4895+ yield self.poke_zk()
4896+
4897+ self.assertEqual(len(results), 4)
4898+ self.assertEqual(contents[-1], {"retry": NO_HOOKS})
4899+
4900+ # Clear out any pending activity.
4901+ yield self.poke_zk()
4902+
4903+ @inlineCallbacks
4904+ def test_watch_relation_resolved_slow_callback(self):
4905+ """A slow watch callback is still invoked serially."""
4906+ unit_state = yield self.get_unit_state()
4907+
4908+ callbacks = [Deferred() for i in range(5)]
4909+ results = []
4910+ contents = []
4911+
4912+ @inlineCallbacks
4913+ def watch(value):
4914+ results.append(value)
4915+ yield callbacks[len(results) - 1]
4916+ contents.append((yield unit_state.get_relation_resolved()))
4917+
4918+ callbacks[0].callback(True)
4919+ yield unit_state.watch_relation_resolved(watch)
4920+
4921+ # These get collapsed into a single event
4922+ yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
4923+ yield unit_state.clear_relation_resolved()
4924+ yield self.poke_zk()
4925+
4926+ # Verify the callback hasn't completed
4927+ self.assertEqual(len(results), 2)
4928+ self.assertEqual(len(contents), 1)
4929+
4930+ # Let it finish
4931+ callbacks[1].callback(True)
4932+ yield self.poke_zk()
4933+
4934+ # Verify result counts
4935+ self.assertEqual(len(results), 3)
4936+ self.assertEqual(len(contents), 2)
4937+
4938+ # Verify result values. Even though we have created event, the
4939+ # setting retrieved shows the hook is not enabled.
4940+ self.assertEqual(results[-1].type_name, "deleted")
4941+ self.assertEqual(contents[-1], None)
4942+
4943+ yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
4944+ callbacks[2].callback(True)
4945+ yield self.poke_zk()
4946+
4947+ self.assertEqual(len(results), 4)
4948+ self.assertEqual(contents[-1], {"0": RETRY_HOOKS})
4949+
4950+ # Clear out any pending activity.
4951+ yield self.poke_zk()
4952+
4953+ @inlineCallbacks
4954+ def test_set_and_clear_upgrade_flag(self):
4955+ """An upgrade flag can be set on a unit."""
4956+
4957+ # Defaults to false
4958+ unit_state = yield self.get_unit_state()
4959+ upgrade_flag = yield unit_state.get_upgrade_flag()
4960+ self.assertEqual(upgrade_flag, False)
4961+
4962+ # Can be set
4963+ yield unit_state.set_upgrade_flag()
4964+ upgrade_flag = yield unit_state.get_upgrade_flag()
4965+ self.assertEqual(upgrade_flag, {'force': False})
4966+
4967+ # Attempting to set multiple times is an error if the values
4968+ # differ.
4969+ yield self.assertFailure(
4970+ unit_state.set_upgrade_flag(force=True),
4971+ ServiceUnitUpgradeAlreadyEnabled)
4972+ self.assertEqual(upgrade_flag, {'force': False})
4973+
4974+ # Can be cleared
4975+ yield unit_state.clear_upgrade_flag()
4976+ upgrade_flag = yield unit_state.get_upgrade_flag()
4977+ self.assertEqual(upgrade_flag, False)
4978+
4979+ # Can be cleared multiple times
4980+ yield unit_state.clear_upgrade_flag()
4981+ upgrade_flag = yield unit_state.get_upgrade_flag()
4982+ self.assertEqual(upgrade_flag, False)
4983+
4984+ # A empty node present is not problematic
4985+ yield self.client.create(unit_state._upgrade_flag_path, "")
4986+ upgrade_flag = yield unit_state.get_upgrade_flag()
4987+ self.assertEqual(upgrade_flag, False)
4988+
4989+ yield unit_state.set_upgrade_flag(force=True)
4990+ upgrade_flag = yield unit_state.get_upgrade_flag()
4991+ self.assertEqual(upgrade_flag, {"force": True})
4992+
4993+ @inlineCallbacks
4994+ def test_watch_upgrade_flag_once(self):
4995+ """An upgrade watch can be set to notified of presence and changes."""
4996+ unit_state = yield self.get_unit_state()
4997+ yield unit_state.set_upgrade_flag()
4998+
4999+ results = []
5000+
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to status/vote changes: