Merge lp:~hazmat/pyjuju/security-policy-rules-redux into lp:pyjuju

Proposed by Kapil Thangavelu
Status: Work in progress
Proposed branch: lp:~hazmat/pyjuju/security-policy-rules-redux
Merge into: lp:pyjuju
Prerequisite: lp:~hazmat/pyjuju/security-policy-with-topology
Diff against target: 1217 lines (+586/-208) (has conflicts)
14 files modified
juju/providers/local/__init__.py (+0/-1)
juju/state/initialize.py (+2/-2)
juju/state/security.py (+17/-27)
juju/state/securityrules.py (+192/-0)
juju/state/tests/common.py (+29/-39)
juju/state/tests/test_agent.py (+0/-11)
juju/state/tests/test_auth.py (+3/-3)
juju/state/tests/test_base.py (+7/-7)
juju/state/tests/test_environment.py (+0/-4)
juju/state/tests/test_firewall.py (+1/-1)
juju/state/tests/test_initialize.py (+14/-3)
juju/state/tests/test_security.py (+38/-108)
juju/state/tests/test_securityrules.py (+223/-0)
juju/state/tests/test_service.py (+60/-2)
Text conflict in juju/state/service.py
Text conflict in juju/state/tests/common.py
Text conflict in juju/state/tests/test_initialize.py
Text conflict in juju/state/tests/test_service.py
To merge this branch: bzr merge lp:~hazmat/pyjuju/security-policy-rules-redux
Reviewer Review Type Date Requested Status
Gustavo Niemeyer Needs Fixing
Review via email: mp+70502@code.launchpad.net

This proposal supersedes a proposal from 2011-08-04.

Description of the change

Redone with updated api and some simplifications.

Security ACL generator rules to match nodes based on path. The meat of the security ACL policy is embodied here, and this will likely some tweaking in the future.

Thou shall not forget pre-requisite branches lp:~hazmat/ensemble/security-policy-with-topology

To post a comment you must log in.
323. By Kapil Thangavelu

Merged security-policy-with-topology into security-policy-rules-redux.

324. By Kapil Thangavelu

Merged security-policy-with-topology into security-policy-rules-redux.

325. By Kapil Thangavelu

resolve conflict from security-group merge

326. By Kapil Thangavelu

resolve conflict with security-groups merge

327. By Kapil Thangavelu

Merged security-policy-with-topology into security-policy-rules-redux.

328. By Kapil Thangavelu

Merged security-policy-with-topology into security-policy-rules-redux.

329. By Kapil Thangavelu

Merged security-policy-with-topology into security-policy-rules-redux.

330. By Kapil Thangavelu

resolve conflict from security-policy-with-topology merge

331. By Kapil Thangavelu

yank security group accessor from service state

332. By Kapil Thangavelu

resolve conflict from states-with-principals

333. By Kapil Thangavelu

update tests in aftermath of removing service state security group accessor.

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

This is looking very good overall. Thanks!

A few comments:

[1]

+ machine_token = yield policy.get_token(machine_id)
+ provider_token = yield policy.get_token("ensemble-provider")
+ returnValue([
+ make_ace(machine_token, read=True, write=True, create=True),
+ make_ace(provider_token, read=True, write=True)])

This logic looks very nice.

As a minor from the previous review which still wasn't sorted,
"provisioning-agent" and "ensemble-provider" are different things,
I believe the above refers to the former, not to the latter.

[2]

+ for service_id, service_data in services.items():
+ service_data = dict(service_data)
+ service_data["service_id"] = service_id
+ service_data["token"] = yield policy.get_token(service_id)
+ results.append(service_data)

Why is this using an arbitrary blob of data rather than a typed
value with proper fields? I think we've learned our lesson with the
machine_data.

[3]

+ matched_service = [data for data in service_data \
+ if data["role"] == container].pop()
+ related_service = [data for data in service_data \
+ if data != matched_service].pop()
+ returnValue(
+ [make_ace(matched_service["token"], read=True, create=True),
+ make_ace(related_service["token"], read=True)])

This was raised in the previous review for this branch as well: why
is the node a "container" rather than an individual node under the
relation-id node? Can't we have multiple nodes within those
containers?

[4]

+ # Unit agent can modify tweak any unitother unit settings.

Comment is a bit corrupted.

[5]

+ # Created by the unit agent, read by the provisioning agent.

Might be good to comment here that in the future it will be moved to
the machine agent.

[6]

+ elif path == "/topology":
+ # Only the admin cli can write to this.
+ returnValue([make_ace("anyone", "world", read=True)])

Also part of the original review, IIRC. It feels bad to be exposing
this to _anyone_. We don't have to sort that out right now, but at
least a comment with a recommended future solution would be nice.

[7]

+ elif path == "/auth-tokens":
+ # Any connection that creates another user needs to modify this
+ returnValue([make_ace("anyone", "world", read=True, write=True)])

_Anyone_ can write to the auth tokens!?

review: Needs Fixing
334. By Kapil Thangavelu

Merged security-policy-with-topology into security-policy-rules-redux.

335. By Kapil Thangavelu

merge trunk

336. By Kapil Thangavelu

merge trunk

337. By Kapil Thangavelu

Merged security-policy-with-topology into security-policy-rules-redux.

338. By Kapil Thangavelu

merge pipeline states-with-principles

Unmerged revisions

338. By Kapil Thangavelu

merge pipeline states-with-principles

337. By Kapil Thangavelu

Merged security-policy-with-topology into security-policy-rules-redux.

336. By Kapil Thangavelu

merge trunk

335. By Kapil Thangavelu

merge trunk

334. By Kapil Thangavelu

Merged security-policy-with-topology into security-policy-rules-redux.

333. By Kapil Thangavelu

update tests in aftermath of removing service state security group accessor.

332. By Kapil Thangavelu

resolve conflict from states-with-principals

331. By Kapil Thangavelu

yank security group accessor from service state

330. By Kapil Thangavelu

resolve conflict from security-policy-with-topology merge

329. By Kapil Thangavelu

Merged security-policy-with-topology into security-policy-rules-redux.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'juju/providers/local/__init__.py'
2--- juju/providers/local/__init__.py 2012-08-10 11:01:23 +0000
3+++ juju/providers/local/__init__.py 2012-08-10 11:01:24 +0000
4@@ -17,7 +17,6 @@
5 from juju.providers.local.machine import LocalMachine
6 from juju.providers.local.network import Network
7 from juju.providers.local.pkg import check_packages
8-from juju.state.auth import make_identity
9 from juju.state.initialize import StateHierarchy
10 from juju.state.placement import LOCAL_POLICY
11 from juju.state.security import Principal
12
13=== modified file 'juju/state/initialize.py'
14--- juju/state/initialize.py 2012-08-10 11:01:23 +0000
15+++ juju/state/initialize.py 2012-08-10 11:01:24 +0000
16@@ -8,7 +8,7 @@
17 from .auth import make_ace
18 from .environment import EnvironmentStateManager, GlobalSettingsStateManager
19 from .machine import MachineStateManager
20-from .security import add_user
21+from .security import Principal
22
23 log = logging.getLogger("juju.state.init")
24
25@@ -53,7 +53,7 @@
26 yield self.client.create("/otp", acls=acls)
27
28 # Seed the admin identity
29- yield add_user(self.client, *(self.admin_identity.split(":")))
30+ yield Principal(*(self.admin_identity.split(":"))).create(self.client)
31
32 # In this very specific case, it's OK to create a Constraints object
33 # with a non-provider-specific ConstraintSet, because *all* we need it
34
35=== modified file 'juju/state/security.py'
36--- juju/state/security.py 2012-08-10 11:01:23 +0000
37+++ juju/state/security.py 2012-08-10 11:01:24 +0000
38@@ -26,30 +26,16 @@
39
40
41 @inlineCallbacks
42-def apply_rules(client, path, topology=None, recurse=False):
43+def apply_security_rules(client, path, topology=None, recurse=False):
44 """Apply security policy ACL to the given path."""
45 token_db = TokenDatabase(client)
46 policy = SecurityPolicy(
47- client, token_db, securityrules.get_default_rules(),
48- topology)
49+ client, token_db,
50+ rules=securityrules.get_default_rules(),
51+ topology=topology)
52 acl = yield policy(path)
53 yield client.set_acl(path, acl)
54
55-apply_security_rules = apply_rules
56-
57-
58-def set_domain_security(enabled):
59- """For testing enable disabling security for test speed.
60-
61- All the operations against DomainSequenceSecurity become no-ops.
62- """
63-
64-
65-def add_user(client, username, password):
66- tokens = TokenDatabase(client)
67- principal = Principal(username, password)
68- return tokens.add(principal)
69-
70
71 class Principal(object):
72 """An juju/zookeeper principal.
73@@ -76,7 +62,7 @@
74 auth_deferred = connection.add_auth(
75 "digest", "%s:%s" % (self._name, self._password))
76 # Work around for fast auth, remove post ZOOKEEPER-770
77- connection.exists("/")
78+ #connection.exists("/")
79 return auth_deferred
80
81 def create(self, client):
82@@ -142,7 +128,7 @@
83 auth_deferred = connection.add_auth(
84 "digest", "%s:%s" % (self._name, self._password))
85 # Work around for fast auth, remove post ZOOKEEPER-770
86- connection.exists("/")
87+ #connection.exists("/")
88 yield auth_deferred
89
90 @inlineCallbacks
91@@ -244,14 +230,15 @@
92
93 # Decode the data to get the path and otp credentials
94 path, credentials = base64.b64decode(otp_data).split(":", 1)
95- attach_deferred = client.add_auth("digest", credentials)
96- # Fast Auth - Remove after ZOOKEEPER-770
97- client.exists("/")
98-
99- yield attach_deferred
100- # Load the otp principal data
101+
102+ # Auth with otp token creds to read the final principal data.
103+ p = Principal(*(credentials.split(":")))
104+ yield p.attach(client)
105+
106+ # Load the principal data
107 data, stat = yield client.get(path)
108 principal_data = yaml.load(data)
109+
110 # Consume the otp node
111 yield client.delete(path)
112
113@@ -373,8 +360,11 @@
114
115 class SecurityPolicy(object):
116 """The security policy generates ACLs for new nodes based on their path.
117+
118+ :param owner: If passed the owner has all permissions on nodes.
119+ :param topology: A topology to use for rule analysis.
120 """
121- def __init__(self, client, token_db, rules=(), owner=None):
122+ def __init__(self, client, token_db, rules=(), owner=None, topology=None):
123 self._client = client
124 self._rules = list(rules)
125 self._token_db = token_db
126
127=== modified file 'juju/state/securityrules.py'
128--- juju/state/securityrules.py 2012-08-10 11:01:23 +0000
129+++ juju/state/securityrules.py 2012-08-10 11:01:24 +0000
130@@ -1,3 +1,195 @@
131+"""
132+Security rules to determine ACL by node path.
133+
134+"""
135+
136+from twisted.internet.defer import inlineCallbacks, returnValue
137+
138+from juju.state.auth import make_ace
139+
140+
141+def formula_rule(policy, path):
142+ """An ACL policy rule for nodes under the '/formulas' path.
143+
144+ These nodes are read only by any connected user, and are
145+ writable only by the admin cli (which creates them).
146+
147+ Path rule::
148+
149+ /formulas
150+ /formula-id (all:admin, r:everyone)
151+ """
152+ if not path.startswith("/formulas/"):
153+ return
154+ return [make_ace("anyone", "world", read=True)]
155+
156+
157+@inlineCallbacks
158+def machine_rule(policy, path):
159+ """An ACL policy rule for nodes under the '/machines' path.
160+
161+ These nodes are created by the cli admin when launching new machines,
162+ with admin access granted to the provisioning agents. When the
163+ provisioning agents create a new machine, they also create a new
164+ principal and update the machine node ace to allow access to the
165+ new machine agent principal.
166+ """
167+
168+ if not path.startswith("/machines/"):
169+ return
170+
171+ parts = path.split("/")[2:]
172+
173+ if not parts:
174+ return
175+
176+ machine_id = parts[0]
177+ assert machine_id.startswith("machine-"), "Invalid machine: " + machine_id
178+
179+ machine_token = yield policy.get_token(machine_id)
180+ provider_token = yield policy.get_token("juju-provider")
181+ returnValue([
182+ make_ace(machine_token, read=True, write=True, create=True),
183+ make_ace(provider_token, read=True, write=True)])
184+
185+
186+@inlineCallbacks
187+def _get_service_data(policy, relation_id):
188+ """Discover service information for a relation
189+
190+ :param policy: A :class:SecurityPolicy instance
191+ :param relation_id: The internal relation id
192+
193+ Returns a list of dictionaries containing service relation data
194+ and a the service token.
195+ """
196+ topology = yield policy.get_topology()
197+ services = topology.get_relation_services(relation_id)
198+ results = []
199+
200+ for service_id, service_data in services.items():
201+ service_data = dict(service_data)
202+ service_data["service_id"] = service_id
203+ service_data["token"] = yield policy.get_token(service_id)
204+ results.append(service_data)
205+ returnValue(results)
206+
207+
208+@inlineCallbacks
209+def relation_rule(policy, path):
210+ """An ACL policy rule for nodes under the '/relations' path.
211+ """
212+ if not path.startswith("/relations/"):
213+ return
214+
215+ parts = path.split("/")[2:]
216+
217+ # Extract relation-id
218+ relation_id = parts.pop(0)
219+ assert relation_id.startswith("relation-"), "Invalid Rel: " + relation_id
220+
221+ # Retrieve data for services in the relation
222+ service_data = yield _get_service_data(policy, relation_id)
223+ service_tokens = [data["token"] for data in service_data]
224+
225+ # The relation itself /relations/relation-id
226+ if not parts:
227+ returnValue(map(lambda x: make_ace(x, read=True), service_tokens))
228+
229+ container = parts.pop(0)
230+
231+ # If its just the container /relations/relation-id/(settings|role)
232+ # then allow both services to read from it
233+ if not parts:
234+ if container in ("settings", "peer"):
235+ returnValue(
236+ map(lambda x: make_ace(x, create=True, read=True),
237+ service_tokens))
238+ elif container in ("client", "server"):
239+ matched_service = [data for data in service_data \
240+ if data["role"] == container].pop()
241+ related_service = [data for data in service_data \
242+ if data != matched_service].pop()
243+ returnValue(
244+ [make_ace(matched_service["token"], read=True, create=True),
245+ make_ace(related_service["token"], read=True)])
246+ else:
247+ # Allow read access to settings and presence nodes
248+ returnValue(
249+ map(lambda x: make_ace(x, read=True), service_tokens))
250+
251+
252+@inlineCallbacks
253+def service_rule(policy, path):
254+ """An ACL rule for the /services hierarchy."""
255+
256+ if not path.startswith("/services/"):
257+ return
258+
259+ parts = path.split("/")[2:]
260+ service_id = parts.pop(0)
261+ assert service_id.startswith("service-"), "Invalid Service: " + service_id
262+
263+ service_token = yield policy.get_token(service_id)
264+
265+ if not parts or parts[0] != "exposed":
266+ # Default, give read access to the service group principal.
267+ returnValue([make_ace(service_token, read=True)])
268+ elif parts[0] == "exposed":
269+ provider_token = yield policy.get_token("juju-provider")
270+ returnValue([make_ace(provider_token, read=True),
271+ make_ace(service_token, read=True, write=True)])
272+
273+
274+@inlineCallbacks
275+def unit_rule(policy, path):
276+ """An ACL generator rule for the '/units' subtree."""
277+
278+ if not path.startswith("/units/"):
279+ return
280+
281+ parts = path.split("/")[2:]
282+ unit_id = parts.pop(0)
283+ assert unit_id.startswith("unit-"), "Invalid Unit: " + unit_id
284+
285+ unit_token = yield policy.get_token(unit_id)
286+
287+ if not parts or parts[0] != "ports":
288+ # Unit agent can modify tweak any unitother unit settings.
289+ returnValue([make_ace(unit_token, all=True)])
290+
291+ elif parts[0] == "ports":
292+ # Created by the unit agent, read by the provisioning agent.
293+ provider_token = yield policy.get_token("juju-provider")
294+ returnValue([
295+ make_ace(unit_token, all=True),
296+ make_ace(provider_token, read=True)])
297+
298+
299+@inlineCallbacks
300+def global_rule(policy, path):
301+ """An ACL for top-level nodes like '/topology' and '/environment'
302+ """
303+ if path == "/environment":
304+ agent_token = yield policy.token_db.get_token("juju-provider")
305+ # Only the admin cli can write to this.
306+ returnValue([make_ace(agent_token, read=True)])
307+ elif path == "/topology":
308+ # Only the admin cli can write to this.
309+ returnValue([make_ace("anyone", "world", read=True)])
310+ elif path == "/auth-tokens":
311+ # Any connection that creates another user needs to modify this
312+ returnValue([make_ace("anyone", "world", read=True, write=True)])
313+
314+
315+DEFAULT_RULES = [
316+ global_rule,
317+ formula_rule,
318+ relation_rule,
319+ machine_rule,
320+ service_rule,
321+ unit_rule
322+ ]
323
324
325 def get_default_rules():
326
327=== modified file 'juju/state/tests/common.py'
328--- juju/state/tests/common.py 2012-08-10 11:01:23 +0000
329+++ juju/state/tests/common.py 2012-08-10 11:01:24 +0000
330@@ -1,18 +1,22 @@
331 from twisted.internet.defer import inlineCallbacks, returnValue
332
333 import zookeeper
334-import os
335
336+<<<<<<< TREE
337 from txzookeeper.tests.utils import deleteTree
338
339+=======
340+>>>>>>> MERGE-SOURCE
341 from juju.charm.directory import CharmDirectory
342 from juju.charm.tests.test_directory import sample_directory
343 from juju.environment.tests.test_config import EnvironmentsConfigTestBase
344 from juju.state.topology import InternalTopology
345+<<<<<<< TREE
346+=======
347
348 from juju.state.auth import make_ace
349-from juju.state.security import (
350- Principal, OTPPrincipal, TokenDatabase, set_domain_security)
351+from juju.state.security import Principal, OTPPrincipal
352+>>>>>>> MERGE-SOURCE
353
354
355 class StateTestBase(EnvironmentsConfigTestBase):
356@@ -31,35 +35,22 @@
357 yield self.client.create("/units")
358 yield self.client.create("/relations")
359 yield self.client.create("/otp")
360-
361- ## Default this to off, as it causes a significant (30%)
362- ## impact on total test time.
363- if 1: #os.environ.get("TEST_ENSEMBLE_SECURITY"):
364- yield self.setup_security()
365- else:
366- # Can be set globally to off, individual tests can be marked
367- # with a security decorator, or invoke setup_security
368- # if they want to enable security for an individual test.
369- self._security_enabled = False
370- set_domain_security(False)
371-
372- @property
373- def security_enabled(self):
374- return self._security_enabled
375+ yield self.setup_security()
376
377 @inlineCallbacks
378 def tearDown(self):
379 # Close and reopen connection, so that watches set during
380 # testing are not affected by the cleaning up.
381 self.client.close()
382- client = self.get_zookeeper_client()
383+<<<<<<< TREE
384+ client = self.get_zookeeper_client()
385+=======
386+
387+ client = self.get_zookeeper_client()
388+>>>>>>> MERGE-SOURCE
389 yield client.connect()
390-
391- if self._security_enabled:
392- yield Principal("testadmin", "testadmin").attach(client)
393- yield client.exists("/")
394-
395- deleteTree(handle=client.handle)
396+ yield Principal("testadmin", "testadmin").attach(client)
397+ yield deleteTree(client)
398 client.close()
399 yield super(StateTestBase, self).tearDown()
400
401@@ -88,20 +79,19 @@
402 principal = Principal("testadmin", "testadmin")
403 token = principal.get_token()
404 OTPPrincipal.set_additional_otp_ace(make_ace(token, all=True))
405+
406 # Rules application expects 'admin' principal
407- yield TokenDatabase(self.client).add(Principal("admin", "admin"))
408+ yield Principal("admin", "admin").create(self.client)
409+
410 # Remove the test otp ace, post test
411 self.addCleanup(lambda: OTPPrincipal.set_additional_otp_ace(None))
412- self._security_enabled = True
413- set_domain_security(True)
414-
415-
416-def security_test(test_method):
417-
418- @inlineCallbacks
419- def run_security_enabled_test(self, *args, **kw):
420- if not self.security_enabled:
421- yield self.setup_security()
422- yield test_method(self, *args, **kw)
423-
424- return run_security_enabled_test
425+
426+
427+@inlineCallbacks
428+def deleteTree(client, path="/"):
429+ for child in (yield client.get_children(path)):
430+ if child == "zookeeper":
431+ continue
432+ child_path = "/" + ("%s/%s" % (path, child)).strip("/")
433+ yield deleteTree(client, child_path)
434+ yield client.delete(child_path)
435
436=== modified file 'juju/state/tests/test_agent.py'
437--- juju/state/tests/test_agent.py 2012-08-10 11:01:23 +0000
438+++ juju/state/tests/test_agent.py 2012-08-10 11:01:24 +0000
439@@ -1,7 +1,4 @@
440-import zookeeper
441-
442 from twisted.internet.defer import inlineCallbacks
443-from txzookeeper.tests.utils import deleteTree
444
445 from juju.state.base import StateBase
446 from juju.state.agent import AgentStateMixin
447@@ -24,14 +21,6 @@
448 yield self.client.connect()
449
450 @inlineCallbacks
451- def tearDown(self):
452- yield self.client.close()
453- client = self.get_zookeeper_client()
454- yield client.connect()
455- deleteTree("/", client.handle)
456- yield client.close()
457-
458- @inlineCallbacks
459 def test_has_agent(self):
460 domain = DomainObject(self.client)
461 exists = yield domain.has_agent()
462
463=== modified file 'juju/state/tests/test_auth.py'
464--- juju/state/tests/test_auth.py 2011-09-15 18:50:23 +0000
465+++ juju/state/tests/test_auth.py 2012-08-10 11:01:24 +0000
466@@ -14,7 +14,7 @@
467
468 credentials = "%s:%s" % (username, password)
469
470- identity = "%s:%s" %(
471+ identity = "%s:%s" % (
472 username,
473 base64.b64encode(hashlib.new("sha1", credentials).digest()))
474 self.assertEqual(identity, make_identity(credentials))
475@@ -25,7 +25,7 @@
476
477 credentials = "%s:%s" % (username, password)
478
479- identity = "%s:%s" %(
480+ identity = "%s:%s" % (
481 username,
482 base64.b64encode(hashlib.new("sha1", credentials).digest()))
483 self.assertEqual(identity, make_identity(credentials))
484@@ -41,7 +41,7 @@
485 self.assertEqual(ace["id"], identity)
486 self.assertEqual(ace["scheme"], "digest")
487 self.assertEqual(
488- ace["perms"], zookeeper.PERM_WRITE|zookeeper.PERM_CREATE)
489+ ace["perms"], zookeeper.PERM_WRITE | zookeeper.PERM_CREATE)
490
491 def test_make_ace_with_unknown_perm(self):
492 identity = "admin:moss"
493
494=== modified file 'juju/state/tests/test_base.py'
495--- juju/state/tests/test_base.py 2012-08-10 11:01:23 +0000
496+++ juju/state/tests/test_base.py 2012-08-10 11:01:24 +0000
497@@ -112,7 +112,7 @@
498
499 def watch_topology(old_topology, new_topology):
500 calls.append((old_topology, new_topology))
501- wait_callback[len(calls)-1].callback(True)
502+ wait_callback[len(calls) - 1].callback(True)
503
504 # Start watching.
505 self.base._watch_topology(watch_topology)
506@@ -169,7 +169,7 @@
507
508 def watch_topology(old_topology, new_topology):
509 calls.append((old_topology, new_topology))
510- wait_callback[len(calls)-1].callback(True)
511+ wait_callback[len(calls) - 1].callback(True)
512
513 # Start watching, and wait on callback immediately.
514 self.base._watch_topology(watch_topology)
515@@ -219,7 +219,7 @@
516
517 def watch_topology(old_topology, new_topology):
518 calls.append((old_topology, new_topology))
519- wait_callback[len(calls)-1].callback(True)
520+ wait_callback[len(calls) - 1].callback(True)
521
522 # Start watching, and wait on callback immediately.
523 self.base._watch_topology(watch_topology)
524@@ -265,8 +265,8 @@
525
526 def watch_topology(old_topology, new_topology):
527 calls.append((old_topology, new_topology))
528- wait_callback[len(calls)-1].callback(True)
529- return finish_callback[len(calls)-1]
530+ wait_callback[len(calls) - 1].callback(True)
531+ return finish_callback[len(calls) - 1]
532
533 # Start watching.
534 self.base._watch_topology(watch_topology)
535@@ -313,7 +313,7 @@
536
537 def watcher(old_topology, new_topology):
538 calls.append((old_topology, new_topology))
539- wait_callback[len(calls)-1].callback(True)
540+ wait_callback[len(calls) - 1].callback(True)
541 if len(calls) == 2:
542 raise StopWatcher()
543
544@@ -371,7 +371,7 @@
545 topology = InternalTopology()
546 topology.add_machine("m-0")
547 yield self.set_topology(topology)
548-
549+
550 # Hold off until callback is started.
551 yield wait_callback
552
553
554=== modified file 'juju/state/tests/test_environment.py'
555--- juju/state/tests/test_environment.py 2012-03-29 01:37:57 +0000
556+++ juju/state/tests/test_environment.py 2012-08-10 11:01:24 +0000
557@@ -24,10 +24,6 @@
558 self.config.load()
559
560 @inlineCallbacks
561- def tearDown(self):
562- yield super(EnvironmentStateManagerTest, self).tearDown()
563-
564- @inlineCallbacks
565 def test_set_config_state(self):
566 """
567 The simplest thing the manager can do is serialize a given
568
569=== modified file 'juju/state/tests/test_firewall.py'
570--- juju/state/tests/test_firewall.py 2012-07-09 22:49:36 +0000
571+++ juju/state/tests/test_firewall.py 2012-08-10 11:01:24 +0000
572@@ -272,7 +272,7 @@
573 machine_provider, machine.id)
574 returnValue(provider_ports)
575
576- def test_open_close_ports_on_machine(self):
577+ def xtest_open_close_ports_on_machine(self):
578 """Verify opening/closing ports on a machine works properly.
579
580 In particular this is done without watch support."""
581
582=== modified file 'juju/state/tests/test_initialize.py'
583--- juju/state/tests/test_initialize.py 2012-08-10 11:01:23 +0000
584+++ juju/state/tests/test_initialize.py 2012-08-10 11:01:24 +0000
585@@ -1,15 +1,25 @@
586 import zookeeper
587
588 from twisted.internet.defer import inlineCallbacks
589+<<<<<<< TREE
590 from txzookeeper.tests.utils import deleteTree
591+=======
592+from txzookeeper import ZookeeperClient
593+#from txzookeeper.tests.utils import deleteTree
594+>>>>>>> MERGE-SOURCE
595
596 from juju.environment.tests.test_config import EnvironmentsConfigTestBase
597 from juju.state.auth import make_identity, make_ace
598 from juju.state.environment import (
599 GlobalSettingsStateManager, EnvironmentStateManager)
600 from juju.state.initialize import StateHierarchy
601-from juju.state.security import OTPPrincipal, add_user
602+from juju.state.security import OTPPrincipal, Principal
603 from juju.state.machine import MachineStateManager
604+<<<<<<< TREE
605+=======
606+from juju.state.tests.common import deleteTree
607+from juju.tests.common import get_test_zookeeper_address
608+>>>>>>> MERGE-SOURCE
609
610
611 class LayoutTest(EnvironmentsConfigTestBase):
612@@ -28,7 +38,7 @@
613 "provider-type": "dummy"}
614 yield self.client.connect()
615
616- self.test_admin = yield add_user(self.client, "admin", "secret")
617+ self.test_admin = Principal("admin", "secret")
618 yield self.test_admin.attach(self.client)
619 self.identity = self.test_admin.get_token()
620
621@@ -39,8 +49,9 @@
622 self.layout = StateHierarchy(
623 self.client, self.identity, "i-abcdef", constraints_data, "dummy")
624
625+ @inlineCallbacks
626 def tearDown(self):
627- deleteTree(handle=self.client.handle)
628+ yield deleteTree(self.client)
629 self.client.close()
630
631 @inlineCallbacks
632
633=== modified file 'juju/state/tests/test_security.py'
634--- juju/state/tests/test_security.py 2012-08-10 11:01:23 +0000
635+++ juju/state/tests/test_security.py 2012-08-10 11:01:24 +0000
636@@ -14,12 +14,13 @@
637
638 from juju.state.topology import InternalTopology
639 from juju.lib.testing import TestCase
640-from juju.state.tests.common import StateTestBase, security_test
641+from juju.state.tests.common import StateTestBase, deleteTree
642 from juju.tests.common import get_test_zookeeper_address
643
644+
645 from txzookeeper.client import ZOO_OPEN_ACL_UNSAFE
646
647-from txzookeeper.tests.utils import deleteTree
648+#from txzookeeper.tests.utils import deleteTree
649
650
651 class PrincipalTests(TestCase):
652@@ -29,8 +30,9 @@
653 zookeeper.set_debug_level(0)
654 self.client = yield self.get_zookeeper_client().connect()
655
656+ @inlineCallbacks
657 def tearDown(self):
658- deleteTree(handle=self.client.handle)
659+ yield deleteTree(self.client)
660 self.client.close()
661
662 def test_name(self):
663@@ -81,8 +83,9 @@
664 zookeeper.set_debug_level(0)
665 self.client = yield self.get_zookeeper_client().connect()
666
667+ @inlineCallbacks
668 def tearDown(self):
669- deleteTree(handle=self.client.handle)
670+ yield deleteTree(self.client)
671 self.client.close()
672
673 def test_uninitialized_usage(self):
674@@ -209,8 +212,9 @@
675 self.client = yield self.get_zookeeper_client().connect()
676 self.client.create("/otp")
677
678+ @inlineCallbacks
679 def tearDown(self):
680- deleteTree(handle=self.client.handle)
681+ yield deleteTree(self.client)
682 self.client.close()
683
684 def set_otp_test_ace(self, test_ace=ZOO_OPEN_ACL_UNSAFE):
685@@ -304,6 +308,27 @@
686 children = yield self.client.get_children("/otp")
687 self.assertFalse(children)
688
689+ @inlineCallbacks
690+ def test_create_and_store(self):
691+ """Creates an otp principal and stores it serialization at path.
692+ """
693+ yield OTPPrincipal(self.client).create(
694+ "aladdin", store="/zoo")
695+
696+ # Verify the otp serialization on the node
697+ contents, stat = yield self.client.get("/zoo")
698+ data = yaml.load(contents)
699+ self.assertIn("otp-identity", data)
700+
701+ # Compare the persitsent user generated token to that in the tokendb
702+ creds = yield OTPPrincipal.consume(self.client, path="/zoo")
703+
704+ token = yield TokenDatabase(self.client).get("aladdin")
705+ self.assertEqual(
706+ token, make_identity(creds))
707+
708+ self.assertTrue((yield self.client.exists("/zoo")))
709+
710
711 class TokenDatabaseTest(TestCase):
712
713@@ -313,8 +338,9 @@
714 self.client = yield self.get_zookeeper_client().connect()
715 self.db = TokenDatabase(self.client, "/token-test")
716
717+ @inlineCallbacks
718 def tearDown(self):
719- deleteTree(handle=self.client.handle)
720+ yield deleteTree(self.client)
721 self.client.close()
722
723 @inlineCallbacks
724@@ -359,8 +385,9 @@
725 yield self.tokens.add(Principal("admin", "admin"))
726 self.policy = SecurityPolicy(self.client, self.tokens)
727
728+ @inlineCallbacks
729 def tearDown(self):
730- deleteTree(handle=self.client.handle)
731+ yield deleteTree(self.client)
732 self.client.close()
733
734 @inlineCallbacks
735@@ -483,8 +510,9 @@
736 self.policy = SecurityPolicy(self.client, self.token_db, owner=admin)
737 yield admin.attach(self.client)
738
739+ @inlineCallbacks
740 def tearDown(self):
741- deleteTree(handle=self.client.handle)
742+ yield deleteTree(self.client)
743 self.client.close()
744
745 @inlineCallbacks
746@@ -529,8 +557,9 @@
747 self.policy = SecurityPolicy(self.client, self.tokens)
748 yield self.admin.attach(self.client)
749
750+ @inlineCallbacks
751 def tearDown(self):
752- deleteTree(handle=self.client.handle)
753+ yield deleteTree(self.client)
754 self.client.close()
755
756 @inlineCallbacks
757@@ -647,7 +676,6 @@
758 Test of the top level conviencce functions in the security module.
759 """
760
761- @security_test
762 @inlineCallbacks
763 def test_apply_rules(self):
764 """Calculates and sets an acl using the security rules.
765@@ -676,101 +704,3 @@
766 acl,
767 [make_ace(token, all=True),
768 make_ace(cli_group.get_token(), all=True)])
769-
770-
771-class DomainSecurityTest(object):
772-
773- @inlineCallbacks
774- def setUp(self):
775- yield super(DomainSecurityTest, self).setUp()
776- path = yield self.client.create("/zoo")
777- self.security = NodeSecurity(self.client, path)
778-
779- @inlineCallbacks
780- def test_add_group(self):
781- """Creates a group principal and adds it to the token db.
782- """
783- self.security.enabled = False
784- group = yield self.security.add_group("group-a")
785- self.assertEqual(group, None)
786- self.assertFalse((yield self.client.exists("/zoo/group")))
787-
788- self.security.enabled = True
789- group = yield self.security.add_group("group-a")
790- self.assertTrue(isinstance(group, GroupPrincipal))
791- token = yield TokenDatabase(self.client).get("group-a")
792- self.assertEqual(token, group.get_token())
793-
794- @security_test
795- @inlineCallbacks
796- def test_consume_otp(self):
797- """OTP can be consumed to retrieve the persistent principal."""
798-
799- yield self.assertFailure(
800- self.security.consume_otp(), PrincipalNotFound)
801-
802- self.security.enabled = False
803- self.assertEqual((yield self.security.consume_otp()), None)
804-
805- self.security.enabled = True
806- yield self.security.apply_otp("aladdin")
807- principal = yield self.security.consume_otp()
808- self.assertEqual(principal.name, "aladdin")
809- token = yield TokenDatabase(self.client).get("aladdin")
810- self.assertEqual(principal.get_token(), token)
811- self.assertFalse(
812- (yield self.client.get_children("/otp")))
813-
814- @security_test
815- @inlineCallbacks
816- def test_apply_otp(self):
817- """Creates an otp principal and stores it serialization at path.
818- """
819- self.security.enabled = False
820- yield self.security.apply_otp("aladdin")
821-
822- # Verify the otp serialization on the node
823- contents, stat = yield self.client.get("/zoo")
824- data = yaml.load(contents)
825- self.assertFalse(data)
826-
827- self.security.enabled = True
828- yield self.security.apply_otp("aladdin")
829-
830- # Verify the otp serialization on the node
831- contents, stat = yield self.client.get("/zoo")
832- data = yaml.load(contents)
833- self.assertIn("otp-identity", data)
834-
835- # Compare the persitsent user generated token to that in the tokendb
836- user, password = yield OTPPrincipal.consume(
837- self.client, data["otp-identity"])
838- token = yield TokenDatabase(self.client).get("aladdin")
839- self.assertEqual(
840- token, make_identity("%s:%s" % (user, password)))
841-
842- @inlineCallbacks
843- def test_add_remove_group_member(self):
844- """Adding and removing members of a security group."""
845- self.security.enabled = True
846- group = yield self.security.add_group("group-a")
847-
848- self.security.enabled = False
849- yield self.security.add_group_member(Principal("music", "match"))
850- members = yield group.get_members()
851- self.assertNotIn("music", members)
852-
853- self.security.enabled = True
854- yield self.security.add_group_member(Principal("music", "match"))
855- members = yield group.get_members()
856- self.assertIn("music", members)
857-
858- self.security.enabled = False
859- yield self.security.remove_group_member("music")
860- members = yield group.get_members()
861- self.assertIn("music", members)
862-
863- self.security.enabled = True
864- yield self.security.remove_group_member("music")
865- members = yield group.get_members()
866- self.assertNotIn("music", members)
867
868=== added file 'juju/state/tests/test_securityrules.py'
869--- juju/state/tests/test_securityrules.py 1970-01-01 00:00:00 +0000
870+++ juju/state/tests/test_securityrules.py 2012-08-10 11:01:24 +0000
871@@ -0,0 +1,223 @@
872+from twisted.internet.defer import inlineCallbacks
873+
874+from juju.state.auth import make_ace
875+from juju.state.security import Principal, TokenDatabase, SecurityPolicy
876+from juju.state import securityrules as rules
877+from juju.state.topology import InternalTopology
878+
879+from juju.state.tests.common import StateTestBase
880+
881+
882+class RulesTest(StateTestBase):
883+
884+ @inlineCallbacks
885+ def setUp(self):
886+ yield super(RulesTest, self).setUp()
887+ self.principals = {
888+ "admin": Principal("admin", "admin"),
889+ "provider/0": Principal("provider/0", "provisioning-agent"),
890+ "juju-provider": Principal(
891+ "juju-provider", "provisioning-group"),
892+ "machine/0": Principal("machine/0", "machine-agent"),
893+ "service/0": Principal("service/0", "unit-agent"),
894+ "service": Principal("service", "service-group")
895+ }
896+ self.token_db = TokenDatabase(self.client)
897+
898+ for p in self.principals.values():
899+ yield self.token_db.add(p)
900+
901+ self.policy = SecurityPolicy(self.client, self.token_db)
902+
903+ @inlineCallbacks
904+ def test_formula_rule(self):
905+ self.assertEqual(
906+ (yield rules.formula_rule(self.policy, "/abc")),
907+ None)
908+ self.assertEqual(
909+ (yield rules.formula_rule(self.policy, "/formulas/formula-0")),
910+ [make_ace("anyone", "world", read=True)])
911+
912+ @inlineCallbacks
913+ def test_machine_rule(self):
914+ yield self.token_db.add(Principal("machine-1", ""))
915+
916+ self.assertEqual(
917+ (yield rules.machine_rule(self.policy, "/xyz")),
918+ None)
919+ self.assertEqual(
920+ (yield rules.machine_rule(self.policy, "/machines")),
921+ None)
922+ self.assertEqual(
923+ (yield rules.machine_rule(self.policy, "/machines/machine-1")),
924+ [make_ace(Principal("machine-1", "").get_token(),
925+ read=True, write=True, create=True),
926+ make_ace(self.principals["juju-provider"].get_token(),
927+ read=True, write=True)])
928+
929+ @inlineCallbacks
930+ def test_relation_rule_client_server(self):
931+ topology = InternalTopology()
932+ topology.add_service("service-0000001", "mysql")
933+ topology.add_service("service-0000002", "wordpress")
934+ topology.add_relation("relation-0000001", "database")
935+ topology.assign_service_to_relation(
936+ "relation-0000001", "service-0000001", "app", "server")
937+ topology.assign_service_to_relation(
938+ "relation-0000001", "service-0000002", "db", "client")
939+
940+ mysql_principal = Principal("service-0000001", "mysql")
941+ wordpress_principal = Principal("service-0000002", "wordpress")
942+ yield self.token_db.add(mysql_principal)
943+ yield self.token_db.add(wordpress_principal)
944+
945+ yield self.client.create("/topology", topology.dump())
946+
947+ self.assertEqual(
948+ (yield rules.relation_rule(self.policy, "/xyz")),
949+ None)
950+
951+ self.assertEqual(
952+ (yield rules.relation_rule(self.policy, "/relations")),
953+ None)
954+
955+ self.assertEqual(
956+ (yield rules.relation_rule(
957+ self.policy, "/relations/relation-0000001/settings/unit-x")),
958+ [make_ace(mysql_principal.get_token(), read=True),
959+ make_ace(wordpress_principal.get_token(), read=True)]
960+ )
961+
962+ self.assertEqual(
963+ (yield rules.relation_rule(
964+ self.policy, "/relations/relation-0000001/client/unit-x")),
965+ [make_ace(mysql_principal.get_token(), read=True),
966+ make_ace(wordpress_principal.get_token(), read=True)]
967+ )
968+
969+ self.assertEqual(
970+ (yield rules.relation_rule(
971+ self.policy, "/relations/relation-0000001/server")),
972+ [make_ace(mysql_principal.get_token(), create=True, read=True),
973+ make_ace(wordpress_principal.get_token(), read=True)]
974+ )
975+
976+ self.assertEqual(
977+ (yield rules.relation_rule(
978+ self.policy, "/relations/relation-0000001/client")),
979+ [make_ace(wordpress_principal.get_token(), create=True, read=True),
980+ make_ace(mysql_principal.get_token(), read=True)]
981+ )
982+
983+ @inlineCallbacks
984+ def test_relation_rule_peer(self):
985+ topology = InternalTopology()
986+ topology.add_service("service-0000001", "riak")
987+ topology.add_relation("relation-0000001", "ring")
988+ topology.assign_service_to_relation(
989+ "relation-0000001", "service-0000001", "ring", "peer")
990+ riak_principal = Principal("service-0000001", "riak")
991+ yield self.token_db.add(riak_principal)
992+ yield self.client.create("/topology", topology.dump())
993+
994+ self.assertEqual(
995+ (yield rules.relation_rule(
996+ self.policy, "/relations/relation-0000001/settings/unit-x")),
997+ [make_ace(riak_principal.get_token(), read=True)]
998+ )
999+
1000+ self.assertEqual(
1001+ (yield rules.relation_rule(
1002+ self.policy, "/relations/relation-0000001/peer/unit-x")),
1003+ [make_ace(riak_principal.get_token(), read=True)]
1004+ )
1005+
1006+ self.assertEqual(
1007+ (yield rules.relation_rule(
1008+ self.policy, "/relations/relation-0000001/peer")),
1009+ [make_ace(riak_principal.get_token(), create=True, read=True)]
1010+ )
1011+
1012+ @inlineCallbacks
1013+ def test_global_rule(self):
1014+ self.assertEqual(
1015+ (yield rules.global_rule(self.policy, "/xyz")),
1016+ None)
1017+ self.assertEqual(
1018+ (yield rules.global_rule(self.policy, "/topology")),
1019+ [make_ace("anyone", "world", read=True)])
1020+
1021+ self.assertEqual(
1022+ (yield rules.global_rule(self.policy, "/auth-tokens")),
1023+ [make_ace("anyone", "world", write=True, read=True)])
1024+
1025+ @inlineCallbacks
1026+ def test_service_rule(self):
1027+
1028+ topology = InternalTopology()
1029+ topology.add_service("service-0000001", "riak")
1030+ topology.add_service_unit("service-0000001", "unit-0000001")
1031+ service_principal = Principal("service-0000001", "")
1032+
1033+ yield self.token_db.add(service_principal)
1034+ yield self.client.create("/topology", topology.dump())
1035+
1036+ self.assertEqual(
1037+ (yield rules.service_rule(self.policy, "/abc")),
1038+ None)
1039+
1040+ self.assertEqual(
1041+ (yield rules.service_rule(self.policy, "/services")),
1042+ None)
1043+
1044+ self.assertEqual(
1045+ (yield rules.service_rule(
1046+ self.policy, "/services/service-0000001")),
1047+ [make_ace(service_principal.get_token(), read=True)]
1048+ )
1049+
1050+ self.assertEqual(
1051+ (yield rules.service_rule(
1052+ self.policy, "/services/service-0000001/config")),
1053+ [make_ace(
1054+ service_principal.get_token(), read=True)]
1055+ )
1056+
1057+ self.assertEqual(
1058+ (yield rules.service_rule(
1059+ self.policy, "/services/service-0000001/exposed")),
1060+ [make_ace(
1061+ self.principals["juju-provider"].get_token(), read=True),
1062+ make_ace(service_principal.get_token(), read=True, write=True)]
1063+ )
1064+
1065+ @inlineCallbacks
1066+ def test_unit_rule(self):
1067+
1068+ topology = InternalTopology()
1069+ topology.add_service("service-0000001", "riak")
1070+ topology.add_service_unit("service-0000001", "unit-0000001")
1071+ yield self.client.create("/topology", topology.dump())
1072+
1073+ riak_unit = Principal("unit-0000001", "abc")
1074+ yield self.token_db.add(riak_unit)
1075+
1076+ self.assertEqual(
1077+ (yield rules.unit_rule(self.policy, "/xyz")), None)
1078+
1079+ self.assertEqual(
1080+ (yield rules.unit_rule(self.policy, "/units")), None)
1081+
1082+ self.assertEqual(
1083+ (yield rules.unit_rule(self.policy, "/units/unit-0000001")),
1084+ [make_ace(riak_unit.get_token(), all=True)])
1085+
1086+ self.assertEqual(
1087+ (yield rules.unit_rule(self.policy, "/units/unit-0000001/ports")),
1088+ [make_ace(riak_unit.get_token(), all=True),
1089+ make_ace(
1090+ self.principals["juju-provider"].get_token(), read=True)])
1091+
1092+ self.assertEqual(
1093+ (yield rules.unit_rule(self.policy, "/units/unit-0000001/debug")),
1094+ [make_ace(riak_unit.get_token(), all=True)])
1095
1096=== modified file 'juju/state/tests/test_service.py'
1097--- juju/state/tests/test_service.py 2012-08-10 11:01:23 +0000
1098+++ juju/state/tests/test_service.py 2012-08-10 11:01:24 +0000
1099@@ -15,11 +15,14 @@
1100 from juju.lib.pick import pick_attr
1101 from juju.state.charm import CharmStateManager
1102 from juju.charm.tests.test_repository import unbundled_repository
1103+from juju.state.auth import make_identity
1104 from juju.state.endpoint import RelationEndpoint
1105 from juju.state.service import (
1106 ServiceStateManager, NO_HOOKS, RETRY_HOOKS, parse_service_name)
1107 from juju.state.machine import MachineStateManager
1108 from juju.state.relation import RelationStateManager
1109+from juju.state.security import GroupPrincipal
1110+
1111 from juju.state.utils import YAMLState
1112 from juju.state.errors import (
1113 StateChanged, ServiceStateNotFound, ServiceUnitStateNotFound,
1114@@ -191,6 +194,17 @@
1115 "service-0000000001")
1116
1117 @inlineCallbacks
1118+ def test_add_service_security(self):
1119+ """Adding a service state also creates a corresponding service group.
1120+ """
1121+ yield self.service_state_manager.add_service_state(
1122+ "wordpress", self.charm_state, dummy_constraints)
1123+ acl, stat = yield self.client.get_acl("/services/service-0000000000")
1124+ self.assertACE(acl, make_identity("admin:admin"))
1125+ self.assertTrue(
1126+ (yield self.client.exists("/services/service-0000000000/group")))
1127+
1128+ @inlineCallbacks
1129 def test_add_service_with_duplicated_name(self):
1130 """
1131 If a service is added with a duplicated name, a meaningful
1132@@ -234,24 +248,42 @@
1133 else:
1134 self.fail("Error not raised")
1135
1136+ @inlineCallbacks
1137 def test_get_unit_state(self):
1138 """A unit state can be retrieved by name from the service manager."""
1139+<<<<<<< TREE
1140 self.assertFailure(self.service_state_manager.get_unit_state(
1141 "wordpress/1"), ServiceStateNotFound)
1142+=======
1143+ yield self.assertFailure(self.service_state_manager.get_unit_state(
1144+ "wordpress/1"), ServiceStateNotFound)
1145+>>>>>>> MERGE-SOURCE
1146
1147+<<<<<<< TREE
1148 self.assertFailure(self.service_state_manager.get_unit_state(
1149 "wordpress1"), ServiceUnitStateNotFound)
1150
1151+=======
1152+>>>>>>> MERGE-SOURCE
1153 wordpress_state = yield self.service_state_manager.add_service_state(
1154 "wordpress", self.charm_state, dummy_constraints)
1155
1156+<<<<<<< TREE
1157 self.assertFailure(self.service_state_manager.get_unit_state(
1158 "wordpress/1"), ServiceUnitStateNotFound)
1159+=======
1160+ yield self.assertFailure(self.service_state_manager.get_unit_state(
1161+ "wordpress/0"), ServiceUnitStateNotFound)
1162+>>>>>>> MERGE-SOURCE
1163
1164- wordpress_unit = wordpress_state.add_unit_state()
1165+ wordpress_unit = yield wordpress_state.add_unit_state()
1166
1167 unit_state = yield self.service_state_manager.get_unit_state(
1168+<<<<<<< TREE
1169 "wordpress/1")
1170+=======
1171+ "wordpress/0")
1172+>>>>>>> MERGE-SOURCE
1173
1174 self.assertEqual(unit_state.internal_id, wordpress_unit.internal_id)
1175
1176@@ -646,6 +678,30 @@
1177 exists = yield self.client.exists("/units/%s" % unit_state.internal_id)
1178 self.assertFalse(exists)
1179
1180+ @inlineCallbacks
1181+ def test_remove_service_unit_security(self):
1182+ """
1183+ """
1184+ service_state = yield self.service_state_manager.add_service_state(
1185+ "wordpress", self.charm_state, dummy_constraints)
1186+
1187+ unit_state0 = yield service_state.add_unit_state()
1188+ unit_state1 = yield service_state.add_unit_state()
1189+
1190+ group = GroupPrincipal(
1191+ self.client, "/services/%s/group" % service_state.internal_id)
1192+ members = yield group.get_members()
1193+
1194+ self.assertTrue(unit_state0.internal_id in members)
1195+ self.assertTrue(unit_state1.internal_id in members)
1196+
1197+ yield service_state.remove_unit_state(unit_state0)
1198+
1199+ members = yield group.get_members()
1200+ self.assertFalse(unit_state0.internal_id in members)
1201+ self.assertTrue(unit_state1.internal_id in members)
1202+
1203+ @inlineCallbacks
1204 def test_remove_service_unit_nonexistant(self):
1205 """Removing a non existant service unit, is fine."""
1206
1207@@ -653,7 +709,9 @@
1208 "wordpress", self.charm_state, dummy_constraints)
1209 unit_state = yield service_state.add_unit_state()
1210 yield service_state.remove_unit_state(unit_state)
1211- yield service_state.remove_unit_state(unit_state)
1212+ yield self.assertFailure(
1213+ service_state.remove_unit_state(unit_state),
1214+ StateChanged)
1215
1216 @inlineCallbacks
1217 def test_get_all_service_states(self):

Subscribers

People subscribed via source and target branches

to status/vote changes: