Merge lp:~bcsaller/pyjuju/subordinate-removes into lp:pyjuju

Proposed by Benjamin Saller
Status: Work in progress
Proposed branch: lp:~bcsaller/pyjuju/subordinate-removes
Merge into: lp:pyjuju
Diff against target: 550 lines (+264/-43)
11 files modified
juju/control/destroy_service.py (+2/-2)
juju/control/remove_relation.py (+5/-3)
juju/control/tests/test_destroy_service.py (+18/-4)
juju/control/tests/test_remove_relation.py (+2/-2)
juju/hooks/tests/test_invoker.py (+2/-0)
juju/lib/testing.py (+3/-3)
juju/state/hook.py (+9/-3)
juju/state/tests/test_service.py (+78/-22)
juju/unit/lifecycle.py (+64/-0)
juju/unit/tests/test_lifecycle.py (+76/-2)
juju/unit/workflow.py (+5/-2)
To merge this branch: bzr merge lp:~bcsaller/pyjuju/subordinate-removes
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+102427@code.launchpad.net

Description of the change

Destroying a subordinate shouldn't result in tracebacks

Destroying a subordinate service after destroying its primary services results in a traceback
This patch corrects this. The associated test also previously failed to exercise all the needed
behavior to demonstrate this, which is corrected as well.

https://codereview.appspot.com/6256054/

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

Please take a look.

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

Reviewers: mp+102427_code.launchpad.net,

Message:
Please take a look.

Description:
Destroying a subordinate shouldn't result in tracebacks

Destroying a subordinate service after destroying its primary services
results in a traceback
This patch corrects this. The associated test also previously failed to
exercise all the needed
behavior to demonstrate this, which is corrected as well.

https://code.launchpad.net/~bcsaller/juju/subordinate-removes/+merge/102427

(do not edit description out of merge proposal)

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

Affected files:
   A [revision details]
   M juju/control/destroy_service.py
   M juju/control/remove_relation.py
   M juju/control/tests/test_destroy_service.py
   M juju/control/tests/test_remove_relation.py
   M juju/hooks/tests/test_invoker.py
   M juju/lib/testing.py
   M juju/state/tests/test_firewall.py
   M juju/state/tests/test_service.py
   M juju/unit/lifecycle.py
   M juju/unit/tests/test_lifecycle.py
   M juju/unit/workflow.py

Revision history for this message
Kapil Thangavelu (hazmat) wrote :
Download full text (3.1 KiB)

Needs Fixing

There seem to be two separate implementations of unrelated issues here
that would ideally be in separate branches. One for open/close port. And
one for subordinate suicide on removal. I'm focusing this review on
subordinate suicide aspect.

https://codereview.appspot.com/6256054/diff/1/juju/control/remove_relation.py
File juju/control/remove_relation.py (right):

https://codereview.appspot.com/6256054/diff/1/juju/control/remove_relation.py#newcode72
juju/control/remove_relation.py:72: has_principal = True
this should default to false, right?

https://codereview.appspot.com/6256054/diff/1/juju/control/remove_relation.py#newcode79
juju/control/remove_relation.py:79: service_pair.insert(0, service)
pls document here why the subordinate is coming first in the ep pair.
its easy to gloss over this is just an error message formatting thing.

https://codereview.appspot.com/6256054/diff/1/juju/control/tests/test_destroy_service.py
File juju/control/tests/test_destroy_service.py (right):

https://codereview.appspot.com/6256054/diff/1/juju/control/tests/test_destroy_service.py#newcode138
juju/control/tests/test_destroy_service.py:138: # resulted in a
traceback
This doesn't actually test anything related to the branch afaics. The
actual problem is asynchronously triggered.

https://codereview.appspot.com/6256054/diff/1/juju/hooks/tests/test_invoker.py
File juju/hooks/tests/test_invoker.py (right):

https://codereview.appspot.com/6256054/diff/1/juju/hooks/tests/test_invoker.py#newcode1480
juju/hooks/tests/test_invoker.py:1480: """Verify that port hook commands
run and changes are immediate."""
does this change belong in this branch?

https://codereview.appspot.com/6256054/diff/1/juju/state/tests/test_firewall.py
File juju/state/tests/test_firewall.py (right):

https://codereview.appspot.com/6256054/diff/1/juju/state/tests/test_firewall.py#newcode189
juju/state/tests/test_firewall.py:189: """Verify that opening/closing
ports triggers the appropriate firewall
does this change belong in this branch?

https://codereview.appspot.com/6256054/diff/1/juju/unit/lifecycle.py
File juju/unit/lifecycle.py (right):

https://codereview.appspot.com/6256054/diff/1/juju/unit/lifecycle.py#newcode458
juju/unit/lifecycle.py:458: def _undeploy(self, service_relation):
method name is a bit ambigious its always a suicide op, ie.
_undeploy_self..

https://codereview.appspot.com/6256054/diff/1/juju/unit/lifecycle.py#newcode459
juju/unit/lifecycle.py:459: """Conditionally stop and remove _this_
agents deployment.
s/agents/subordinate agent

https://codereview.appspot.com/6256054/diff/1/juju/unit/lifecycle.py#newcode500
juju/unit/lifecycle.py:500: yield unit_deployer.start("subordinate")
are you sure about that? i don't see anything obvious there, if so its a
bug in the deployer.. start would imply deploying again.. which is
bogus.

https://codereview.appspot.com/6256054/diff/1/juju/unit/workflow.py
File juju/unit/workflow.py (right):

https://codereview.appspot.com/6256054/diff/1/juju/unit/workflow.py#newcode229
juju/unit/workflow.py:229: if not stat:
how does this help? what if the node is already gone when the
retry_change fires. also untested.

https://codereview...

Read more...

537. By Benjamin Saller

typo

538. By Benjamin Saller

merge trunk

539. By Benjamin Saller

move ports handling to another branch

540. By Benjamin Saller

verify unit state removal on subordinate suicide

541. By Benjamin Saller

catch another async failure location

542. By Benjamin Saller

missing import

543. By Benjamin Saller

missing import

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

so post discussions, i believe we've said this branch/approach is dead in favor of the stop protocol spec.

Unmerged revisions

543. By Benjamin Saller

missing import

542. By Benjamin Saller

missing import

541. By Benjamin Saller

catch another async failure location

540. By Benjamin Saller

verify unit state removal on subordinate suicide

539. By Benjamin Saller

move ports handling to another branch

538. By Benjamin Saller

merge trunk

537. By Benjamin Saller

typo

536. By Benjamin Saller

remove print

535. By Benjamin Saller

add back in subordinate removal, this will have to change to honor the normal flow control model later. When stop hooks are properly added this will also need another review

534. By Benjamin Saller

change arg. order for error message

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'juju/control/destroy_service.py'
--- juju/control/destroy_service.py 2012-04-11 17:48:26 +0000
+++ juju/control/destroy_service.py 2012-06-04 23:05:24 +0000
@@ -47,14 +47,14 @@
4747
48 if relations:48 if relations:
49 principal_service = None49 principal_service = None
50 # if we have a container we can destroy the subordinate50 # if we have a container we can't destroy the subordinate
51 # (revisit in the future)51 # (revisit in the future)
52 for relation in relations:52 for relation in relations:
53 if relation.relation_scope != "container":53 if relation.relation_scope != "container":
54 continue54 continue
55 services = yield relation.get_service_states()55 services = yield relation.get_service_states()
56 remote_service = [s for s in services if s.service_name !=56 remote_service = [s for s in services if s.service_name !=
57 service_state.service_name][0]57 service_state.service_name][0]
58 if not (yield remote_service.is_subordinate()):58 if not (yield remote_service.is_subordinate()):
59 principal_service = remote_service59 principal_service = remote_service
60 break60 break
6161
=== modified file 'juju/control/remove_relation.py'
--- juju/control/remove_relation.py 2012-04-12 09:23:50 +0000
+++ juju/control/remove_relation.py 2012-06-04 23:05:24 +0000
@@ -69,17 +69,19 @@
69 service_pair = [] # ordered such that sub, principal69 service_pair = [] # ordered such that sub, principal
7070
71 is_container = False71 is_container = False
72 has_principal = True72 has_principal = False
73 # Sort the endpoints so we know which is the sub and which is the
74 # principal in the event we need to raise an error below
73 for ep in endpoints:75 for ep in endpoints:
74 if ep.relation_scope == "container":76 if ep.relation_scope == "container":
75 is_container = True77 is_container = True
76 service = yield service_state_manager.get_service_state(78 service = yield service_state_manager.get_service_state(
77 ep.service_name)79 ep.service_name)
78 if (yield service.is_subordinate()):80 if (yield service.is_subordinate()):
79 service_pair.append(service)81 service_pair.insert(0, service)
80 has_principal = True82 has_principal = True
81 else:83 else:
82 service_pair.insert(0, service)84 service_pair.append(service)
83 if is_container and len(service_pair) == 2 and has_principal:85 if is_container and len(service_pair) == 2 and has_principal:
84 sub, principal = service_pair86 sub, principal = service_pair
85 raise UnsupportedSubordinateServiceRemoval(sub.service_name,87 raise UnsupportedSubordinateServiceRemoval(sub.service_name,
8688
=== modified file 'juju/control/tests/test_destroy_service.py'
--- juju/control/tests/test_destroy_service.py 2012-04-10 10:26:54 +0000
+++ juju/control/tests/test_destroy_service.py 2012-06-04 23:05:24 +0000
@@ -94,13 +94,15 @@
94 @inlineCallbacks94 @inlineCallbacks
95 def test_destroy_principal_with_subordinates(self):95 def test_destroy_principal_with_subordinates(self):
96 log_service = yield self.add_service_from_charm("logging")96 log_service = yield self.add_service_from_charm("logging")
97 lu1 = yield log_service.add_unit_state()
9897
99 yield self.add_relation(98 yield self.add_relation(
100 "juju-info",99 "juju-info",
100 "container",
101 (self.service_state1, "juju-info", "server"),101 (self.service_state1, "juju-info", "server"),
102 (log_service, "juju-info", "client"))102 (log_service, "juju-info", "client"))
103103
104 lu1 = yield log_service.add_unit_state(self.service1_unit)
105
104 finished = self.setup_cli_reactor()106 finished = self.setup_cli_reactor()
105 topology = yield self.get_topology()107 topology = yield self.get_topology()
106 service_id = topology.find_service_with_name("mysql")108 service_id = topology.find_service_with_name("mysql")
@@ -111,9 +113,10 @@
111113
112 self.setup_exit(0)114 self.setup_exit(0)
113 self.mocker.replay()115 self.mocker.replay()
114
115 main(["destroy-service", "mysql"])116 main(["destroy-service", "mysql"])
117
116 yield finished118 yield finished
119
117 service_id = topology.find_service_with_name("mysql")120 service_id = topology.find_service_with_name("mysql")
118121
119 topology = yield self.get_topology()122 topology = yield self.get_topology()
@@ -128,9 +131,21 @@
128 # Zookeeper. see test_destroy_subordinate_without_relations.131 # Zookeeper. see test_destroy_subordinate_without_relations.
129 exists = yield self.client.exists("/services/%s" % logging_id)132 exists = yield self.client.exists("/services/%s" % logging_id)
130 self.assertTrue(exists)133 self.assertTrue(exists)
131
132 self.assertIn("Service 'mysql' destroyed.", self.output.getvalue())134 self.assertIn("Service 'mysql' destroyed.", self.output.getvalue())
133135
136 # At this point trigger a destroy on the subordinate
137 # this deals with issue #982353 where partial state
138 # resulted in a traceback
139 # Assert that methods related to destruction fire w/o exception
140 self.assertEquals(True, (yield log_service.is_subordinate()))
141 self.assertEquals(None, (yield lu1.get_container()))
142 rels = yield self.relation_state_manager.get_relations_for_service(
143 log_service)
144 self.assertEquals([], rels)
145 yield self.service_state_manager.remove_service_state(log_service)
146 exists = yield self.client.exists("/services/%s" % logging_id)
147 self.assertFalse(exists)
148
134 @inlineCallbacks149 @inlineCallbacks
135 def test_destroy_subordinate_without_relations(self):150 def test_destroy_subordinate_without_relations(self):
136 """Verify we can remove a subordinate w/o relations."""151 """Verify we can remove a subordinate w/o relations."""
@@ -146,7 +161,6 @@
146161
147 main(["destroy-service", "logging"])162 main(["destroy-service", "logging"])
148 yield finished163 yield finished
149
150 topology = yield self.get_topology()164 topology = yield self.get_topology()
151 self.assertFalse(topology.has_service(logging_id))165 self.assertFalse(topology.has_service(logging_id))
152 exists = yield self.client.exists("/services/%s" % logging_id)166 exists = yield self.client.exists("/services/%s" % logging_id)
153167
=== modified file 'juju/control/tests/test_remove_relation.py'
--- juju/control/tests/test_remove_relation.py 2012-04-10 22:26:36 +0000
+++ juju/control/tests/test_remove_relation.py 2012-06-04 23:05:24 +0000
@@ -210,6 +210,6 @@
210 main(["remove-relation", "logging", "wordpress"])210 main(["remove-relation", "logging", "wordpress"])
211 yield wait_on_reactor_stopped211 yield wait_on_reactor_stopped
212 self.assertIn("Unsupported attempt to destroy "212 self.assertIn("Unsupported attempt to destroy "
213 "subordinate service 'wordpress' while "213 "subordinate service 'logging' while "
214 "principal service 'logging' is related.",214 "principal service 'wordpress' is related.",
215 self.output.getvalue())215 self.output.getvalue())
216216
=== modified file 'juju/hooks/tests/test_invoker.py'
--- juju/hooks/tests/test_invoker.py 2012-05-04 03:36:36 +0000
+++ juju/hooks/tests/test_invoker.py 2012-06-04 23:05:24 +0000
@@ -1467,6 +1467,8 @@
14671467
1468 mu1, mu2 = my_units1468 mu1, mu2 = my_units
1469 lu1, lu2 = log_units1469 lu1, lu2 = log_units
1470 self.mysql_units = my_units
1471 self.log_units = log_units
14701472
1471 mystate = pick_attr(service_states, relation_role="server")1473 mystate = pick_attr(service_states, relation_role="server")
1472 logstate = pick_attr(service_states, relation_role="client")1474 logstate = pick_attr(service_states, relation_role="client")
14731475
=== modified file 'juju/lib/testing.py'
--- juju/lib/testing.py 2012-04-06 14:42:06 +0000
+++ juju/lib/testing.py 2012-06-04 23:05:24 +0000
@@ -170,19 +170,19 @@
170 data, stat = yield client.get(path)170 data, stat = yield client.get(path)
171 name = path.rsplit('/', 1)[1]171 name = path.rsplit('/', 1)[1]
172172
173 d['contents'] = _decode_fmt(data, yaml.load)173 d["contents"] = _decode_fmt(data, yaml.load)
174174
175 children = yield client.get_children(path)175 children = yield client.get_children(path)
176 for name in children:176 for name in children:
177 if path == "/" and name == "zookeeper":177 if path == "/" and name == "zookeeper":
178 continue178 continue
179179
180 cd = yield export_tree(path + '/' + name, indent)180 cd = yield export_tree(path + "/" + name, indent)
181 d[name] = cd181 d[name] = cd
182182
183 returnValue(d)183 returnValue(d)
184184
185 output[path.rsplit('/', 1)[1]] = yield export_tree(path, '')185 output[path.rsplit("/", 1)[1]] = yield export_tree(path, "")
186 returnValue(output)186 returnValue(output)
187187
188 @inlineCallbacks188 @inlineCallbacks
189189
=== modified file 'juju/state/hook.py'
--- juju/state/hook.py 2012-04-09 19:40:57 +0000
+++ juju/state/hook.py 2012-06-04 23:05:24 +0000
@@ -4,8 +4,9 @@
44
5from juju.state.base import StateBase5from juju.state.base import StateBase
6from juju.state.errors import (6from juju.state.errors import (
7 UnitRelationStateNotFound, StateNotFound, RelationBrokenContextError,7 ServiceStateNotFound, UnitRelationStateNotFound, StateNotFound,
8 RelationStateNotFound, InvalidRelationIdentity, StateChanged)8 RelationBrokenContextError, RelationStateNotFound,
9 InvalidRelationIdentity, StateChanged)
9from juju.state.relation import (10from juju.state.relation import (
10 RelationStateManager, ServiceRelationState, UnitRelationState)11 RelationStateManager, ServiceRelationState, UnitRelationState)
11from juju.state.service import ServiceStateManager, parse_service_name12from juju.state.service import ServiceStateManager, parse_service_name
@@ -102,7 +103,12 @@
102 relations = []103 relations = []
103 if self._topology is None:104 if self._topology is None:
104 self._topology = yield self._read_topology()105 self._topology = yield self._read_topology()
105 service = yield self.get_local_service()106 try:
107 service = yield self.get_local_service()
108 except ServiceStateNotFound:
109 # The service has been removed
110 returnValue(relations)
111
106 internal_service_id = service.internal_id112 internal_service_id = service.internal_id
107 for info in self._topology.get_relations_for_service(113 for info in self._topology.get_relations_for_service(
108 internal_service_id):114 internal_service_id):
109115
=== modified file 'juju/state/tests/test_service.py'
--- juju/state/tests/test_service.py 2012-04-11 19:38:41 +0000
+++ juju/state/tests/test_service.py 2012-06-04 23:05:24 +0000
@@ -159,6 +159,43 @@
159 unit_state = yield service_state.add_unit_state()159 unit_state = yield service_state.add_unit_state()
160 returnValue(unit_state)160 returnValue(unit_state)
161161
162 @inlineCallbacks
163 def setup_subordinate_defaults(self):
164 mysql_ep = RelationEndpoint("mysql", "juju-info", "juju-info",
165 "server", "global")
166 logging_ep = RelationEndpoint("logging", "juju-info", "juju-info",
167 "client", "container")
168
169 logging_charm_state = yield self.get_subordinate_charm()
170 self.assertTrue(logging_charm_state.is_subordinate())
171 log_state = yield self.service_state_manager.add_service_state(
172 "logging", logging_charm_state, dummy_constraints)
173 mysql_state = yield self.service_state_manager.add_service_state(
174 "mysql", self.charm_state, dummy_constraints)
175
176 m1 = yield self.machine_state_manager.add_machine_state(
177 series_constraints)
178 m2 = yield self.machine_state_manager.add_machine_state(
179 series_constraints)
180
181 relation_state, service_states = (yield
182 self.relation_state_manager.add_relation_state(
183 mysql_ep, logging_ep))
184
185 unit_state1 = yield mysql_state.add_unit_state()
186 yield unit_state1.assign_to_machine(m1)
187 unit_state0 = yield log_state.add_unit_state(
188 container=unit_state1)
189
190 unit_state3 = yield mysql_state.add_unit_state()
191 yield unit_state3.assign_to_machine(m2)
192 unit_state2 = yield log_state.add_unit_state(
193 container=unit_state3)
194
195 returnValue([service_states, relation_state,
196 [(unit_state0, unit_state1),
197 (unit_state2, unit_state3)]])
198
162199
163class ServiceStateManagerTest(ServiceStateManagerTestBase):200class ServiceStateManagerTest(ServiceStateManagerTestBase):
164201
@@ -455,27 +492,9 @@
455 """492 """
456 Validate adding units with containers specified and recovering that.493 Validate adding units with containers specified and recovering that.
457 """494 """
458 mysql_ep = RelationEndpoint("mysql", "juju-info", "juju-info",495 service_states, relation_state, unit_states = (
459 "server", "global")496 yield self.setup_subordinate_defaults())
460 logging_ep = RelationEndpoint("logging", "juju-info", "juju-info",497 (unit_state0, unit_state1), (unit_state2, unit_state3) = unit_states
461 "client", "container")
462
463 logging_charm_state = yield self.get_subordinate_charm()
464 self.assertTrue(logging_charm_state.is_subordinate())
465 log_state = yield self.service_state_manager.add_service_state(
466 "logging", logging_charm_state, dummy_constraints)
467 mysql_state = yield self.service_state_manager.add_service_state(
468 "mysql", self.charm_state, dummy_constraints)
469
470 relation_state, service_states = (yield
471 self.relation_state_manager.add_relation_state(
472 mysql_ep, logging_ep))
473
474 unit_state1 = yield mysql_state.add_unit_state()
475 unit_state0 = yield log_state.add_unit_state(container=unit_state1)
476
477 unit_state3 = yield mysql_state.add_unit_state()
478 unit_state2 = yield log_state.add_unit_state(container=unit_state3)
479498
480 self.assertEquals((yield unit_state1.get_container()), None)499 self.assertEquals((yield unit_state1.get_container()), None)
481 self.assertEquals((yield unit_state0.get_container()), unit_state1)500 self.assertEquals((yield unit_state0.get_container()), unit_state1)
@@ -498,7 +517,7 @@
498517
499 @inlineCallbacks518 @inlineCallbacks
500 def verify_container(relation_state, service_relation_state,519 def verify_container(relation_state, service_relation_state,
501 unit_state, container):520 unit_state, container):
502 presence_path = self.get_presence_path(521 presence_path = self.get_presence_path(
503 relation_state,522 relation_state,
504 service_relation_state.relation_role,523 service_relation_state.relation_role,
@@ -533,6 +552,11 @@
533 # we verify the content elsewhere552 # we verify the content elsewhere
534 self.assertTrue(settings_info["private-address"])553 self.assertTrue(settings_info["private-address"])
535554
555 mid = yield unit_state.get_assigned_machine_id()
556 cmid = yield container.get_assigned_machine_id()
557 if container:
558 self.assertEqual(mid, cmid)
559
536 # verify all the units are constructed as expected560 # verify all the units are constructed as expected
537 # first the client roles with another container561 # first the client roles with another container
538 yield verify_container(relation_state, logstate,562 yield verify_container(relation_state, logstate,
@@ -2540,6 +2564,38 @@
2540 self.assertEqual(exposed_flag, False)2564 self.assertEqual(exposed_flag, False)
25412565
2542 @inlineCallbacks2566 @inlineCallbacks
2567 def test_set_and_clear_exposed_flag_subordinate(self):
2568 """An exposed flag can be set on a subordinate service."""
2569 service_states, relation_state, unit_states = (
2570 yield self.setup_subordinate_defaults())
2571 (u0, u1), (u2, u3) = unit_states
2572 service_state = yield u1.get_service_state()
2573
2574 # Defaults to false
2575 exposed_flag = yield service_state.get_exposed_flag()
2576 self.assertEqual(exposed_flag, False)
2577
2578 # Can be set
2579 yield service_state.set_exposed_flag()
2580 exposed_flag = yield service_state.get_exposed_flag()
2581 self.assertEqual(exposed_flag, True)
2582
2583 # Can be set multiple times
2584 yield service_state.set_exposed_flag()
2585 exposed_flag = yield service_state.get_exposed_flag()
2586 self.assertEqual(exposed_flag, True)
2587
2588 # Can be cleared
2589 yield service_state.clear_exposed_flag()
2590 exposed_flag = yield service_state.get_exposed_flag()
2591 self.assertEqual(exposed_flag, False)
2592
2593 # Can be cleared multiple times
2594 yield service_state.clear_exposed_flag()
2595 exposed_flag = yield service_state.get_exposed_flag()
2596 self.assertEqual(exposed_flag, False)
2597
2598 @inlineCallbacks
2543 def test_watch_exposed_flag(self):2599 def test_watch_exposed_flag(self):
2544 """An exposed watch is setup on a permanent basis."""2600 """An exposed watch is setup on a permanent basis."""
2545 service_state = yield self.add_service("wordpress")2601 service_state = yield self.add_service("wordpress")
25462602
=== modified file 'juju/unit/lifecycle.py'
--- juju/unit/lifecycle.py 2012-05-01 00:26:25 +0000
+++ juju/unit/lifecycle.py 2012-06-04 23:05:24 +0000
@@ -106,6 +106,7 @@
106 self._state_dir = state_dir106 self._state_dir = state_dir
107 self._relations = None107 self._relations = None
108 self._running = False108 self._running = False
109 self._remote_service = {}
109 self._watching_relation_memberships = False110 self._watching_relation_memberships = False
110 self._watching_relation_resolved = False111 self._watching_relation_resolved = False
111 self._run_lock = DeferredLock()112 self._run_lock = DeferredLock()
@@ -404,6 +405,14 @@
404 yield workflow.transition_state("departed")405 yield workflow.transition_state("departed")
405 self._store_relations()406 self._store_relations()
406407
408 if not is_principal:
409 service_relation = old_relations[relation_id]
410 removed = yield self._undeploy_subordinate(service_relation)
411 if removed:
412 # we don't even need to process adds at this point
413 # (that would be a sick, sad world)
414 returnValue(None)
415
407 # Process new relations.416 # Process new relations.
408 for relation_id in added:417 for relation_id in added:
409 service_relation = new_relations[relation_id]418 service_relation = new_relations[relation_id]
@@ -434,10 +443,65 @@
434443
435 self._relations[service_relation.internal_relation_id] = workflow444 self._relations[service_relation.internal_relation_id] = workflow
436445
446 # Cache the remote unit's service (to ease subordinate undeploy)
447 service_states = yield service_relation.get_service_states()
448 remote_service = [s for s in service_states if
449 s.service_name != self._unit.service_name][0]
450
451 self._remote_service[ \
452 service_relation.internal_relation_id] = remote_service
453
437 with (yield workflow.lock()):454 with (yield workflow.lock()):
438 yield workflow.synchronize()455 yield workflow.synchronize()
439456
440 @inlineCallbacks457 @inlineCallbacks
458 def _undeploy_subordinate(self, service_relation):
459 """Conditionally stop and remove _this_ subordinate unit.
460
461 When a relationship is removed we check if its with this subordinate's
462 principal service. If it is we need to stop the agent and remove the
463 on disk deployment.
464
465 returns boolean indicating if we have self-terminated or not.
466 """
467 # determine if the remote end of this service relation is the
468 # container of this unit (it could be a scope:container relation under
469 # the same principal).
470
471 container = yield self._unit.get_container()
472 container_service = yield container.get_service_state()
473 # We cannot use service_relation.get_service_states() as we're
474 # reacting to relation removal and its no longer in the topology.
475 # (forcing us to manage this cache)
476 remote_service = self._remote_service[
477 service_relation.internal_relation_id]
478
479 if container is None or (
480 remote_service.internal_id == container_service.internal_id):
481 # The container has already been removed we can safely proceed
482 # OR
483 # The remote side of the relationship being removed is the
484 # principal.
485 # At this point we will being shutting down this unit,
486 # remove our unit state
487 self._log.debug("Removing unit state for %s", self._unit)
488 yield self._service.remove_unit_state(self._unit)
489 # the final problem
490 yield self._do_unit_kill(self._unit.unit_name, "",
491 self._unit_dir)
492 returnValue(True)
493
494 returnValue(False)
495
496 @inlineCallbacks
497 def _do_unit_kill(self, unit_name, machine_id, charm_dir):
498 unit_deployer = UnitDeployer(self._client, machine_id, charm_dir)
499 # start the unit deployer (this is needed to bootstrap the factory)
500 # kill ourself
501 yield unit_deployer.start("subordinate")
502 yield unit_deployer.kill_service_unit(unit_name)
503
504 @inlineCallbacks
441 def _do_unit_deploy(self, unit_name, machine_id, charm_dir):505 def _do_unit_deploy(self, unit_name, machine_id, charm_dir):
442 # this method exists to aid testing rather than being an506 # this method exists to aid testing rather than being an
443 # inline507 # inline
444508
=== modified file 'juju/unit/tests/test_lifecycle.py'
--- juju/unit/tests/test_lifecycle.py 2012-05-04 03:39:27 +0000
+++ juju/unit/tests/test_lifecycle.py 2012-06-04 23:05:24 +0000
@@ -23,7 +23,8 @@
23from juju.machine.tests.test_constraints import series_constraints23from juju.machine.tests.test_constraints import series_constraints
2424
25from juju.state.endpoint import RelationEndpoint25from juju.state.endpoint import RelationEndpoint
26from juju.state.errors import UnitRelationStateNotFound26from juju.state.errors import (UnitRelationStateNotFound,
27 ServiceUnitStateNotFound)
27from juju.state.machine import MachineStateManager28from juju.state.machine import MachineStateManager
28from juju.state.relation import ClientServerUnitWatcher29from juju.state.relation import ClientServerUnitWatcher
29from juju.state.service import NO_HOOKS30from juju.state.service import NO_HOOKS
@@ -34,7 +35,7 @@
34from juju.unit.tests.test_charm import CharmPublisherTestBase35from juju.unit.tests.test_charm import CharmPublisherTestBase
3536
36from juju.lib.testing import TestCase37from juju.lib.testing import TestCase
37from juju.lib.mocker import MATCH38from juju.lib.mocker import MATCH, ANY, Mocker
3839
3940
40class UnwriteablePath(object):41class UnwriteablePath(object):
@@ -1441,3 +1442,76 @@
1441 yield lifecycle.start()1442 yield lifecycle.start()
1442 yield test_deferred1443 yield test_deferred
1443 # mocker has already verified the call signature, we're happy1444 # mocker has already verified the call signature, we're happy
1445
1446 @inlineCallbacks
1447 def test_subordinate_lifecycle_stop_principal(self):
1448 state = yield self.setup_default_subordinate_relation()
1449 mu1, my_srv = state["principal"]
1450 lu1, log_srv = state["subordinate"]
1451
1452 # Principal setup
1453 principal_lifecycle = UnitLifecycle(
1454 self.client, mu1, my_srv,
1455 self.unit_directory, self.state_directory, self.executor)
1456
1457 test_deferred = Deferred()
1458 principal_results = []
1459
1460 def test_deploy(unit_name, machine_id, charm_dir):
1461 principal_results.append([unit_name, machine_id, charm_dir])
1462 test_deferred.callback(True)
1463
1464 mock_unit_deploy = self.mocker.patch(principal_lifecycle)
1465 mock_unit_deploy._do_unit_deploy("logging/0", 0, ANY)
1466 self.mocker.call(test_deploy)
1467
1468 # Subordinate setup
1469 test_sub_deferred = Deferred()
1470 sub_results = []
1471
1472 def test_undeploy(unit_name, machine_id, charm_dir):
1473 sub_results.append([unit_name, machine_id, charm_dir])
1474 test_sub_deferred.callback(True)
1475
1476 # Actual Test
1477 self.mocker.replay()
1478 yield principal_lifecycle.start()
1479 yield test_deferred
1480
1481 # The principal is running and did a deploy (mocked out)
1482 # at this point we should be able to collect the relation
1483 # and the unit and inject them into the subordinate (which
1484 # would have them in its own process normally)
1485 log = state["subordinate"][1]
1486 log_units = yield log.get_all_unit_states()
1487 lu1 = log_units[0]
1488
1489 # A second instance of mocker around the subordinate
1490 # avoids issues with patch/replace confusing the single
1491 # instance
1492 mocker2 = Mocker()
1493 subordinate_lifecycle = UnitLifecycle(
1494 self.client, lu1, log_srv,
1495 self.unit_directory, self.state_directory, self.executor)
1496
1497 mock_subunit_deploy = mocker2.patch(subordinate_lifecycle)
1498 mock_subunit_deploy._do_unit_kill(
1499 "logging/0", "", ANY)
1500 mocker2.call(test_undeploy)
1501
1502 with mocker2:
1503 yield subordinate_lifecycle.start()
1504
1505 # break relation to principal
1506 yield self.relation_manager.remove_relation_state(
1507 state["relation_state"])
1508
1509 # verify that we attempt to we see the departed event
1510 # and the attempt of the subordinate to self terminate
1511 yield test_sub_deferred
1512 # again, mocker verified the call on do_unit_kill
1513 # this verifies that the unit attempted to self destruct
1514
1515 # verify the subordinate unit state was removed
1516 failure = self.service_manager.get_unit_state("logging/0")
1517 yield self.assertFailure(failure, ServiceUnitStateNotFound)
14441518
=== modified file 'juju/unit/workflow.py'
--- juju/unit/workflow.py 2012-03-29 07:52:41 +0000
+++ juju/unit/workflow.py 2012-06-04 23:05:24 +0000
@@ -1,7 +1,7 @@
1import yaml
2import csv1import csv
2import logging
3import os3import os
4import logging4import yaml
55
6from zookeeper import NoNodeException6from zookeeper import NoNodeException
7from twisted.internet.defer import inlineCallbacks, returnValue7from twisted.internet.defer import inlineCallbacks, returnValue
@@ -226,6 +226,9 @@
226 state_serialized = yaml.safe_dump(state_dict)226 state_serialized = yaml.safe_dump(state_dict)
227227
228 def update_state(content, stat):228 def update_state(content, stat):
229 if not stat:
230 # State has gone away
231 return
229 unit_data = yaml.load(content)232 unit_data = yaml.load(content)
230 if not unit_data:233 if not unit_data:
231 unit_data = {}234 unit_data = {}

Subscribers

People subscribed via source and target branches

to status/vote changes: