~dpb/pyjuju/dont-proxy-https

« back to all changes in this revision

Viewing changes to juju/state/tests/test_service.py

Subordinate support in unit agent lifecycle [r=kapil] [f=805585,963355]

This branch lands the support in the unit agent which allows it to deploy 
subordinate services in its own container.

Show diffs side-by-side

added added

removed removed

Lines of Context:
22
22
from juju.state.relation import RelationStateManager
23
23
from juju.state.utils import YAMLState
24
24
from juju.state.errors import (
25
 
        StateChanged, ServiceStateNotFound, ServiceUnitStateNotFound,
26
 
        ServiceUnitStateMachineAlreadyAssigned, ServiceStateNameInUse,
27
 
        BadDescriptor, BadServiceStateName, ServiceUnitDebugAlreadyEnabled,
28
 
        MachineStateNotFound, NoUnusedMachines, ServiceUnitResolvedAlreadyEnabled,
29
 
        ServiceUnitRelationResolvedAlreadyEnabled, StopWatcher,
30
 
        ServiceUnitUpgradeAlreadyEnabled)
31
 
 
 
25
    StateChanged, ServiceStateNotFound, ServiceUnitStateNotFound,
 
26
    ServiceUnitStateMachineAlreadyAssigned, ServiceStateNameInUse,
 
27
    BadDescriptor, BadServiceStateName, ServiceUnitDebugAlreadyEnabled,
 
28
    MachineStateNotFound, NoUnusedMachines, ServiceUnitResolvedAlreadyEnabled,
 
29
    ServiceUnitRelationResolvedAlreadyEnabled, StopWatcher,
 
30
    ServiceUnitUpgradeAlreadyEnabled)
32
31
from juju.state.tests.common import StateTestBase
33
32
 
34
33
 
35
34
class ServiceStateManagerTestBase(StateTestBase):
36
35
 
37
 
        @inlineCallbacks
38
 
        def setUp(self):
39
 
                yield super(ServiceStateManagerTestBase, self).setUp()
40
 
                yield self.push_default_config()
41
 
                self.charm_state_manager = CharmStateManager(self.client)
42
 
                self.service_state_manager = ServiceStateManager(self.client)
43
 
                self.machine_state_manager = MachineStateManager(self.client)
44
 
                self.charm_state = yield self.charm_state_manager.add_charm_state(
45
 
                        local_charm_id(self.charm), self.charm, "")
46
 
                self.relation_state_manager = RelationStateManager(self.client)
47
 
 
48
 
        @inlineCallbacks
49
 
        def add_service(self, name):
50
 
                service_state = yield self.service_state_manager.add_service_state(
51
 
                        name, self.charm_state, dummy_constraints)
52
 
                returnValue(service_state)
53
 
 
54
 
        @inlineCallbacks
55
 
        def add_service_from_charm(
56
 
                        self, service_name, charm_id=None, constraints=None,
57
 
                        charm_dir=None, charm_name=None):
58
 
                """Add a service from a charm.
59
 
                """
60
 
                if not charm_id and charm_dir is None:
61
 
                        charm_name = charm_name or service_name
62
 
                        charm_dir = CharmDirectory(os.path.join(
63
 
                                test_repository_path, "series", charm_name))
64
 
                if charm_id is None:
65
 
                        charm_state = yield self.charm_state_manager.add_charm_state(
66
 
                                local_charm_id(charm_dir), charm_dir, "")
67
 
                else:
68
 
                        charm_state = yield self.charm_state_manager.get_charm_state(
69
 
                                charm_id)
70
 
                service_state = yield self.service_state_manager.add_service_state(
71
 
                        service_name, charm_state, constraints or dummy_constraints)
72
 
                returnValue(service_state)
73
 
 
74
 
        @inlineCallbacks
75
 
        def get_subordinate_charm(self):
76
 
                """Return charm state for a subordinate charm.
77
 
 
78
 
                Many tests rely on adding relationships to a proper subordinate.
79
 
                This return the charm state of a testing subordinate charm.
80
 
                """
81
 
                unbundled_repo_path = self.makeDir()
82
 
                os.rmdir(unbundled_repo_path)
83
 
                shutil.copytree(unbundled_repository, unbundled_repo_path)
84
 
 
85
 
                sub_charm = CharmDirectory(
86
 
                        os.path.join(unbundled_repo_path, "series", "logging"))
87
 
                self.charm_state_manager.add_charm_state("local:series/logging-1",
88
 
                                                                                                  sub_charm, "")
89
 
                logging_charm_state = yield self.charm_state_manager.get_charm_state(
90
 
                        "local:series/logging-1")
91
 
                returnValue(logging_charm_state)
92
 
 
93
 
        @inlineCallbacks
94
 
        def add_relation(self, relation_type, relation_scope, *services):
95
 
                endpoints = []
96
 
                for service_meta in services:
97
 
                        service_state, relation_name, relation_role = service_meta
98
 
                        endpoints.append(RelationEndpoint(
99
 
                                service_state.service_name,
100
 
                                relation_type,
101
 
                                relation_name,
102
 
                                relation_role,
103
 
                                relation_scope))
104
 
                relation_state = yield self.relation_state_manager.add_relation_state(
105
 
                        *endpoints)
106
 
                returnValue(relation_state[0])
107
 
 
108
 
        @inlineCallbacks
109
 
        def remove_service(self, internal_service_id):
110
 
                topology = yield self.get_topology()
111
 
                topology.remove_service(internal_service_id)
112
 
                yield self.set_topology(topology)
113
 
 
114
 
        @inlineCallbacks
115
 
        def remove_service_unit(self, internal_service_id, internal_unit_id):
116
 
                topology = yield self.get_topology()
117
 
                topology.remove_service_unit(internal_service_id, internal_unit_id)
118
 
                yield self.set_topology(topology)
119
 
 
120
 
        def add_machine_state(self, constraints=None):
121
 
                return self.machine_state_manager.add_machine_state(
122
 
                        constraints or series_constraints)
123
 
 
124
 
        @inlineCallbacks
125
 
        def assert_machine_states(self, present, absent):
126
 
                """Assert that machine IDs are either `present` or `absent` in topo."""
127
 
                for machine_id in present:
128
 
                        self.assertTrue(
129
 
                                (yield self.machine_state_manager.get_machine_state(
130
 
                                                machine_id)))
131
 
                for machine_id in absent:
132
 
                        ex = yield self.assertFailure(
133
 
                                self.machine_state_manager.get_machine_state(machine_id),
134
 
                                MachineStateNotFound)
135
 
                        self.assertEqual(ex.machine_id, machine_id)
136
 
 
137
 
        @inlineCallbacks
138
 
        def assert_machine_assignments(self, service_name, machine_ids):
139
 
                """Assert that `service_name` is deployed on `machine_ids`."""
140
 
                topology = yield self.get_topology()
141
 
                service_id = topology.find_service_with_name(service_name)
142
 
                assigned_machine_ids = [
143
 
                        topology.get_service_unit_machine(service_id, unit_id)
144
 
                        for unit_id in topology.get_service_units(service_id)]
145
 
                internal_machine_ids = []
146
 
                for machine_id in machine_ids:
147
 
                        if machine_id is None:
148
 
                                # corresponds to get_service_unit_machine API in this case
149
 
                                internal_machine_ids.append(None)
150
 
                        else:
151
 
                                internal_machine_ids.append("machine-%010d" % machine_id)
152
 
                self.assertEqual(
153
 
                        set(assigned_machine_ids), set(internal_machine_ids))
154
 
 
155
 
        @inlineCallbacks
156
 
        def get_unit_state(self):
157
 
                """Simple test helper to get a unit state."""
158
 
                service_state = yield self.service_state_manager.add_service_state(
159
 
                        "wordpress", self.charm_state, dummy_constraints)
160
 
                unit_state = yield service_state.add_unit_state()
161
 
                returnValue(unit_state)
 
36
    @inlineCallbacks
 
37
    def setUp(self):
 
38
        yield super(ServiceStateManagerTestBase, self).setUp()
 
39
        yield self.push_default_config()
 
40
        self.charm_state_manager = CharmStateManager(self.client)
 
41
        self.service_state_manager = ServiceStateManager(self.client)
 
42
        self.machine_state_manager = MachineStateManager(self.client)
 
43
        self.charm_state = yield self.charm_state_manager.add_charm_state(
 
44
                local_charm_id(self.charm), self.charm, "")
 
45
        self.relation_state_manager = RelationStateManager(self.client)
 
46
 
 
47
    @inlineCallbacks
 
48
    def add_service(self, name):
 
49
        service_state = yield self.service_state_manager.add_service_state(
 
50
                name, self.charm_state, dummy_constraints)
 
51
        returnValue(service_state)
 
52
 
 
53
    @inlineCallbacks
 
54
    def add_service_from_charm(
 
55
                    self, service_name, charm_id=None, constraints=None,
 
56
                    charm_dir=None, charm_name=None):
 
57
        """Add a service from a charm.
 
58
        """
 
59
        if not charm_id and charm_dir is None:
 
60
            charm_name = charm_name or service_name
 
61
            charm_dir = CharmDirectory(os.path.join(
 
62
                    test_repository_path, "series", charm_name))
 
63
        if charm_id is None:
 
64
            charm_state = yield self.charm_state_manager.add_charm_state(
 
65
                    local_charm_id(charm_dir), charm_dir, "")
 
66
        else:
 
67
            charm_state = yield self.charm_state_manager.get_charm_state(
 
68
                    charm_id)
 
69
        service_state = yield self.service_state_manager.add_service_state(
 
70
                service_name, charm_state, constraints or dummy_constraints)
 
71
        returnValue(service_state)
 
72
 
 
73
    @inlineCallbacks
 
74
    def get_subordinate_charm(self):
 
75
        """Return charm state for a subordinate charm.
 
76
 
 
77
        Many tests rely on adding relationships to a proper subordinate.
 
78
        This return the charm state of a testing subordinate charm.
 
79
        """
 
80
        unbundled_repo_path = self.makeDir()
 
81
        os.rmdir(unbundled_repo_path)
 
82
        shutil.copytree(unbundled_repository, unbundled_repo_path)
 
83
 
 
84
        sub_charm = CharmDirectory(
 
85
                os.path.join(unbundled_repo_path, "series", "logging"))
 
86
        self.charm_state_manager.add_charm_state("local:series/logging-1",
 
87
                                                 sub_charm, "")
 
88
        logging_charm_state = yield self.charm_state_manager.get_charm_state(
 
89
                "local:series/logging-1")
 
90
        returnValue(logging_charm_state)
 
91
 
 
92
    @inlineCallbacks
 
93
    def add_relation(self, relation_type, relation_scope, *services):
 
94
        endpoints = []
 
95
        for service_meta in services:
 
96
            service_state, relation_name, relation_role = service_meta
 
97
            endpoints.append(RelationEndpoint(
 
98
                    service_state.service_name,
 
99
                    relation_type,
 
100
                    relation_name,
 
101
                    relation_role,
 
102
                    relation_scope))
 
103
        relation_state = yield self.relation_state_manager.add_relation_state(
 
104
                *endpoints)
 
105
        returnValue(relation_state[0])
 
106
 
 
107
    @inlineCallbacks
 
108
    def remove_service(self, internal_service_id):
 
109
        topology = yield self.get_topology()
 
110
        topology.remove_service(internal_service_id)
 
111
        yield self.set_topology(topology)
 
112
 
 
113
    @inlineCallbacks
 
114
    def remove_service_unit(self, internal_service_id, internal_unit_id):
 
115
        topology = yield self.get_topology()
 
116
        topology.remove_service_unit(internal_service_id, internal_unit_id)
 
117
        yield self.set_topology(topology)
 
118
 
 
119
    def add_machine_state(self, constraints=None):
 
120
        return self.machine_state_manager.add_machine_state(
 
121
                constraints or series_constraints)
 
122
 
 
123
    @inlineCallbacks
 
124
    def assert_machine_states(self, present, absent):
 
125
        """Assert that machine IDs are either `present` or `absent` in topo."""
 
126
        for machine_id in present:
 
127
            self.assertTrue(
 
128
                    (yield self.machine_state_manager.get_machine_state(
 
129
                                    machine_id)))
 
130
        for machine_id in absent:
 
131
            ex = yield self.assertFailure(
 
132
                    self.machine_state_manager.get_machine_state(machine_id),
 
133
                    MachineStateNotFound)
 
134
            self.assertEqual(ex.machine_id, machine_id)
 
135
 
 
136
    @inlineCallbacks
 
137
    def assert_machine_assignments(self, service_name, machine_ids):
 
138
        """Assert that `service_name` is deployed on `machine_ids`."""
 
139
        topology = yield self.get_topology()
 
140
        service_id = topology.find_service_with_name(service_name)
 
141
        assigned_machine_ids = [
 
142
                topology.get_service_unit_machine(service_id, unit_id)
 
143
                for unit_id in topology.get_service_units(service_id)]
 
144
        internal_machine_ids = []
 
145
        for machine_id in machine_ids:
 
146
            if machine_id is None:
 
147
                # corresponds to get_service_unit_machine API in this case
 
148
                internal_machine_ids.append(None)
 
149
            else:
 
150
                internal_machine_ids.append("machine-%010d" % machine_id)
 
151
        self.assertEqual(
 
152
                set(assigned_machine_ids), set(internal_machine_ids))
 
153
 
 
154
    @inlineCallbacks
 
155
    def get_unit_state(self):
 
156
        """Simple test helper to get a unit state."""
 
157
        service_state = yield self.service_state_manager.add_service_state(
 
158
                "wordpress", self.charm_state, dummy_constraints)
 
159
        unit_state = yield service_state.add_unit_state()
 
160
        returnValue(unit_state)
162
161
 
163
162
 
164
163
class ServiceStateManagerTest(ServiceStateManagerTestBase):
165
164
 
166
 
        @inlineCallbacks
167
 
        def test_add_service(self):
168
 
                """
169
 
                Adding a service state should register it in zookeeper,
170
 
                including the requested charm id.
171
 
                """
172
 
                yield self.service_state_manager.add_service_state(
173
 
                        "wordpress", self.charm_state, dummy_constraints)
174
 
                yield self.service_state_manager.add_service_state(
175
 
                        "mysql", self.charm_state, dummy_constraints)
176
 
                children = yield self.client.get_children("/services")
177
 
 
178
 
                self.assertEquals(sorted(children),
179
 
                                                  ["service-0000000000", "service-0000000001"])
180
 
 
181
 
                content, stat = yield self.client.get("/services/service-0000000000")
182
 
                details = yaml.load(content)
183
 
                self.assertTrue(details)
184
 
                self.assertEquals(details.get("charm"), "local:series/dummy-1")
185
 
                self.assertFalse(isinstance(details.get("charm"), unicode))
186
 
                self.assertEquals(details.get("constraints"), series_constraints.data)
187
 
 
188
 
                topology = yield self.get_topology()
189
 
                self.assertEquals(topology.find_service_with_name("wordpress"),
190
 
                                                  "service-0000000000")
191
 
                self.assertEquals(topology.find_service_with_name("mysql"),
192
 
                                                  "service-0000000001")
193
 
 
194
 
        @inlineCallbacks
195
 
        def test_add_service_with_duplicated_name(self):
196
 
                """
197
 
                If a service is added with a duplicated name, a meaningful
198
 
                error should be raised.
199
 
                """
200
 
                yield self.service_state_manager.add_service_state(
201
 
                        "wordpress", self.charm_state, dummy_constraints)
202
 
 
203
 
                try:
204
 
                        yield self.service_state_manager.add_service_state(
205
 
                                "wordpress", self.charm_state, dummy_constraints)
206
 
                except ServiceStateNameInUse, e:
207
 
                        self.assertEquals(e.service_name, "wordpress")
208
 
                else:
209
 
                        self.fail("Error not raised")
210
 
 
211
 
        @inlineCallbacks
212
 
        def test_get_service_and_check_attributes(self):
213
 
                """
214
 
                Getting a service state should be possible, and the service
215
 
                state identification should be available through its
216
 
                attributes.
217
 
                """
218
 
                yield self.service_state_manager.add_service_state(
219
 
                        "wordpress", self.charm_state, dummy_constraints)
220
 
                service_state = yield self.service_state_manager.get_service_state(
221
 
                        "wordpress")
222
 
                self.assertEquals(service_state.service_name, "wordpress")
223
 
                self.assertEquals(service_state.internal_id, "service-0000000000")
224
 
 
225
 
        @inlineCallbacks
226
 
        def test_get_service_not_found(self):
227
 
                """
228
 
                Getting a service state which is not available should errback
229
 
                a meaningful error.
230
 
                """
231
 
                try:
232
 
                        yield self.service_state_manager.get_service_state("wordpress")
233
 
                except ServiceStateNotFound, e:
234
 
                        self.assertEquals(e.service_name, "wordpress")
235
 
                else:
236
 
                        self.fail("Error not raised")
237
 
 
238
 
        def test_get_unit_state(self):
239
 
                """A unit state can be retrieved by name from the service manager."""
240
 
                self.assertFailure(self.service_state_manager.get_unit_state(
241
 
                        "wordpress/1"), ServiceStateNotFound)
242
 
 
243
 
                self.assertFailure(self.service_state_manager.get_unit_state(
244
 
                        "wordpress1"), ServiceUnitStateNotFound)
245
 
 
246
 
                wordpress_state = yield self.service_state_manager.add_service_state(
247
 
                        "wordpress", self.charm_state, dummy_constraints)
248
 
 
249
 
                self.assertFailure(self.service_state_manager.get_unit_state(
250
 
                        "wordpress/1"), ServiceUnitStateNotFound)
251
 
 
252
 
                wordpress_unit = wordpress_state.add_unit_state()
253
 
 
254
 
                unit_state = yield self.service_state_manager.get_unit_state(
255
 
                                "wordpress/1")
256
 
 
257
 
                self.assertEqual(unit_state.internal_id, wordpress_unit.internal_id)
258
 
 
259
 
        @inlineCallbacks
260
 
        def test_get_service_charm_id(self):
261
 
                """
262
 
                The service state should make its respective charm id available.
263
 
                """
264
 
                yield self.service_state_manager.add_service_state(
265
 
                        "wordpress", self.charm_state, dummy_constraints)
266
 
                service_state = yield self.service_state_manager.get_service_state(
267
 
                        "wordpress")
268
 
                charm_id = yield service_state.get_charm_id()
269
 
                self.assertEquals(charm_id, "local:series/dummy-1")
270
 
 
271
 
        @inlineCallbacks
272
 
        def test_set_service_charm_id(self):
273
 
                """
274
 
                The service state should allow its charm id to be set.
275
 
                """
276
 
                yield self.service_state_manager.add_service_state(
277
 
                        "wordpress", self.charm_state, dummy_constraints)
278
 
                service_state = yield self.service_state_manager.get_service_state(
279
 
                        "wordpress")
280
 
                yield service_state.set_charm_id("local:series/dummy-2")
281
 
                charm_id = yield service_state.get_charm_id()
282
 
                self.assertEquals(charm_id, "local:series/dummy-2")
283
 
 
284
 
        @inlineCallbacks
285
 
        def test_get_service_constraints(self):
286
 
                """The service state should make constraints available"""
287
 
                initial_constraints = dummy_cs.parse(["cpu=256", "arch=amd64"])
288
 
                yield self.service_state_manager.add_service_state(
289
 
                        "wordpress", self.charm_state, initial_constraints)
290
 
                service_state = yield self.service_state_manager.get_service_state(
291
 
                        "wordpress")
292
 
                constraints = yield service_state.get_constraints()
293
 
                self.assertEquals(
294
 
                        constraints, initial_constraints.with_series("series"))
295
 
 
296
 
        @inlineCallbacks
297
 
        def test_get_service_constraints_inherits(self):
298
 
                """The service constraints should be combined with the environment's"""
299
 
                yield self.push_env_constraints("arch=arm", "cpu=32")
300
 
                service_constraints = dummy_cs.parse(["cpu=256"])
301
 
                yield self.service_state_manager.add_service_state(
302
 
                        "wordpress", self.charm_state, service_constraints)
303
 
                service_state = yield self.service_state_manager.get_service_state(
304
 
                        "wordpress")
305
 
                constraints = yield service_state.get_constraints()
306
 
                expected_base = dummy_cs.parse(["arch=arm", "cpu=256"])
307
 
                self.assertEquals(constraints, expected_base.with_series("series"))
308
 
 
309
 
        @inlineCallbacks
310
 
        def test_get_missing_service_constraints(self):
311
 
                """
312
 
                Nodes created before the constraints mechanism was added should have
313
 
                empty constraints.
314
 
                """
315
 
                yield self.client.delete("/constraints")
316
 
                yield self.service_state_manager.add_service_state(
317
 
                        "wordpress", self.charm_state, dummy_constraints)
318
 
                service_state = yield self.service_state_manager.get_service_state(
319
 
                        "wordpress")
320
 
                path = "/services/" + service_state.internal_id
321
 
                node = YAMLState(self.client, path)
322
 
                yield node.read()
323
 
                del node["constraints"]
324
 
                yield node.write()
325
 
                constraints = yield service_state.get_constraints()
326
 
                self.assertEquals(constraints.data, {})
327
 
 
328
 
        @inlineCallbacks
329
 
        def test_get_missing_unit_constraints(self):
330
 
                """
331
 
                Nodes created before the constraints mechanism was added should have
332
 
                empty constraints.
333
 
                """
334
 
                yield self.service_state_manager.add_service_state(
335
 
                        "wordpress", self.charm_state, dummy_constraints)
336
 
                service_state = yield self.service_state_manager.get_service_state(
337
 
                        "wordpress")
338
 
                unit_state = yield service_state.add_unit_state()
339
 
                path = "/units/" + unit_state.internal_id
340
 
                node = YAMLState(self.client, path)
341
 
                yield node.read()
342
 
                del node["constraints"]
343
 
                yield node.write()
344
 
                constraints = yield unit_state.get_constraints()
345
 
                self.assertEquals(constraints.data, {})
346
 
 
347
 
        @inlineCallbacks
348
 
        def test_set_service_constraints(self):
349
 
                """The service state should make constraints available for change"""
350
 
                initial_constraints = dummy_cs.parse(["cpu=256", "arch=amd64"])
351
 
                yield self.service_state_manager.add_service_state(
352
 
                        "wordpress", self.charm_state, initial_constraints)
353
 
                service_state = yield self.service_state_manager.get_service_state(
354
 
                        "wordpress")
355
 
                new_constraints = dummy_cs.parse(["mem=2G", "arch=arm"])
356
 
                yield service_state.set_constraints(new_constraints)
357
 
                retrieved_constraints = yield service_state.get_constraints()
358
 
                self.assertEquals(
359
 
                        retrieved_constraints, new_constraints.with_series("series"))
360
 
 
361
 
        @inlineCallbacks
362
 
        def test_remove_service_state(self):
363
 
                """
364
 
                A service state can be removed along with its relations, units,
365
 
                and zookeeper state.
366
 
                """
367
 
                service_state = yield self.service_state_manager.add_service_state(
368
 
                        "wordpress", self.charm_state, dummy_constraints)
369
 
 
370
 
                relation_state = yield self.add_relation(
371
 
                        "rel-type2", "global", [service_state, "app", "server"])
372
 
 
373
 
                unit_state = yield service_state.add_unit_state()
374
 
                machine_state = yield self.machine_state_manager.add_machine_state(
375
 
                        series_constraints)
376
 
                yield unit_state.assign_to_machine(machine_state)
377
 
 
378
 
                yield self.service_state_manager.remove_service_state(service_state)
379
 
 
380
 
                topology = yield self.get_topology()
381
 
                self.assertFalse(topology.has_relation(relation_state.internal_id))
382
 
                self.assertFalse(topology.has_service(service_state.internal_id))
383
 
                self.assertFalse(
384
 
                        topology.get_service_units_in_machine(machine_state.internal_id))
385
 
 
386
 
                exists = yield self.client.exists(
387
 
                        "/services/%s" % service_state.internal_id)
388
 
                self.assertFalse(exists)
389
 
 
390
 
        @inlineCallbacks
391
 
        def test_add_service_unit_and_check_attributes(self):
392
 
                """
393
 
                A service state should enable adding a new service unit
394
 
                under it, and again the unit should offer attributes allowing
395
 
                its identification.
396
 
                """
397
 
                service_state0 = yield self.service_state_manager.add_service_state(
398
 
                        "wordpress", self.charm_state, dummy_constraints)
399
 
                service_state1 = yield self.service_state_manager.add_service_state(
400
 
                        "mysql", self.charm_state, dummy_constraints)
401
 
 
402
 
                unit_state0 = yield service_state0.add_unit_state()
403
 
                unit_state1 = yield service_state1.add_unit_state()
404
 
                unit_state2 = yield service_state0.add_unit_state()
405
 
                unit_state3 = yield service_state1.add_unit_state()
406
 
 
407
 
                children = yield self.client.get_children("/units")
408
 
                self.assertEquals(sorted(children),
409
 
                                                  ["unit-0000000000", "unit-0000000001",
410
 
                                                   "unit-0000000002", "unit-0000000003"])
411
 
 
412
 
                self.assertEquals(unit_state0.service_name, "wordpress")
413
 
                self.assertEquals(unit_state0.internal_id, "unit-0000000000")
414
 
                self.assertEquals(unit_state0.unit_name, "wordpress/0")
415
 
 
416
 
                self.assertEquals(unit_state1.service_name, "mysql")
417
 
                self.assertEquals(unit_state1.internal_id, "unit-0000000001")
418
 
                self.assertEquals(unit_state1.unit_name, "mysql/0")
419
 
 
420
 
                self.assertEquals(unit_state2.service_name, "wordpress")
421
 
                self.assertEquals(unit_state2.internal_id, "unit-0000000002")
422
 
                self.assertEquals(unit_state2.unit_name, "wordpress/1")
423
 
 
424
 
                self.assertEquals(unit_state3.service_name, "mysql")
425
 
                self.assertEquals(unit_state3.internal_id, "unit-0000000003")
426
 
                self.assertEquals(unit_state3.unit_name, "mysql/1")
427
 
 
428
 
                topology = yield self.get_topology()
429
 
 
430
 
                self.assertTrue(
431
 
                        topology.has_service_unit("service-0000000000",
432
 
                                                                          "unit-0000000000"))
433
 
                self.assertTrue(
434
 
                        topology.has_service_unit("service-0000000001",
435
 
                                                                          "unit-0000000001"))
436
 
                self.assertTrue(
437
 
                        topology.has_service_unit("service-0000000000",
438
 
                                                                          "unit-0000000002"))
439
 
                self.assertTrue(
440
 
                        topology.has_service_unit("service-0000000001",
441
 
                                                                          "unit-0000000003"))
442
 
 
443
 
        def get_presence_path(
444
 
                self, relation_state, relation_role, unit_state, container=None):
445
 
                container = container.internal_id if container else None
446
 
                presence_path = "/".join(filter(None, [
447
 
                        "/relations",
448
 
                        relation_state.internal_id,
449
 
                        container,
450
 
                        relation_role,
451
 
                        unit_state.internal_id]))
452
 
                return presence_path
453
 
 
454
 
        @inlineCallbacks
455
 
        def test_add_service_unit_with_container(self):
456
 
                """
457
 
                Validate adding units with containers specified and recovering that.
458
 
                """
459
 
                mysql_ep = RelationEndpoint("mysql", "juju-info", "juju-info",
460
 
                                                                        "server", "global")
461
 
                logging_ep = RelationEndpoint("logging", "juju-info", "juju-info",
462
 
                                                                          "client", "container")
463
 
 
464
 
                logging_charm_state = yield self.get_subordinate_charm()
465
 
                self.assertTrue(logging_charm_state.is_subordinate())
466
 
                log_state = yield self.service_state_manager.add_service_state(
467
 
                        "logging", logging_charm_state, dummy_constraints)
468
 
                mysql_state = yield self.service_state_manager.add_service_state(
469
 
                        "mysql", self.charm_state, dummy_constraints)
470
 
 
471
 
                relation_state, service_states = (yield
472
 
                        self.relation_state_manager.add_relation_state(
473
 
                                mysql_ep, logging_ep))
474
 
 
475
 
                unit_state1 = yield mysql_state.add_unit_state()
476
 
                unit_state0 = yield log_state.add_unit_state(container=unit_state1)
477
 
 
478
 
                unit_state3 = yield mysql_state.add_unit_state()
479
 
                unit_state2 = yield log_state.add_unit_state(container=unit_state3)
480
 
 
481
 
                self.assertEquals((yield unit_state1.get_container()), None)
482
 
                self.assertEquals((yield unit_state0.get_container()), unit_state1)
483
 
                self.assertEquals((yield unit_state2.get_container()), unit_state3)
484
 
                self.assertEquals((yield unit_state3.get_container()), None)
485
 
 
486
 
                for unit_state in (unit_state1, unit_state3):
487
 
                        yield unit_state.set_private_address(
488
 
                                "%s.example.com" % (
489
 
                                        unit_state.unit_name.replace("/", "-")))
490
 
 
491
 
                # construct the proper relation state
492
 
                mystate = pick_attr(service_states, relation_role="server")
493
 
                logstate = pick_attr(service_states, relation_role="client")
494
 
                yield logstate.add_unit_state(unit_state0)
495
 
                yield logstate.add_unit_state(unit_state2)
496
 
 
497
 
                yield mystate.add_unit_state(unit_state1)
498
 
                yield mystate.add_unit_state(unit_state3)
499
 
 
500
 
                @inlineCallbacks
501
 
                def verify_container(relation_state, service_relation_state,
502
 
                                                         unit_state, container):
503
 
                        presence_path = self.get_presence_path(
504
 
                                relation_state,
505
 
                                service_relation_state.relation_role,
506
 
                                unit_state,
507
 
                                container)
508
 
 
509
 
                        content, stat = yield self.client.get(presence_path)
510
 
                        self.assertTrue(stat)
511
 
                        self.assertEqual(content, '')
512
 
                        # verify the node data on the relation role nodes
513
 
                        role_path = os.path.dirname(presence_path)
514
 
                        # role path
515
 
                        content, stat = yield self.client.get(role_path)
516
 
                        self.assertTrue(stat)
517
 
                        node_info = yaml.load(content)
518
 
 
519
 
                        self.assertEqual(
520
 
                                node_info["name"],
521
 
                                service_relation_state.relation_name)
522
 
                        self.assertEqual(
523
 
                                node_info["role"],
524
 
                                service_relation_state.relation_role)
525
 
 
526
 
                        settings_path = os.path.dirname(
527
 
                                os.path.dirname(presence_path)) + "/settings/" + \
528
 
                                        unit_state.internal_id
529
 
                        content, stat = yield self.client.get(settings_path)
530
 
                        self.assertTrue(stat)
531
 
                        settings_info = yaml.load(content)
532
 
 
533
 
                        # Verify that private address was set
534
 
                        # we verify the content elsewhere
535
 
                        self.assertTrue(settings_info["private-address"])
536
 
 
537
 
                # verify all the units are constructed as expected
538
 
                # first the client roles with another container
539
 
                yield verify_container(relation_state, logstate,
540
 
                                                           unit_state0, unit_state1)
541
 
 
542
 
                yield verify_container(relation_state, logstate,
543
 
                                                           unit_state2, unit_state3)
544
 
 
545
 
                # and now the principals (which are their own relation containers)
546
 
                yield verify_container(relation_state, logstate,
547
 
                                                           unit_state0, unit_state1)
548
 
 
549
 
                yield verify_container(relation_state, mystate,
550
 
                                                           unit_state1, unit_state1)
551
 
 
552
 
        @inlineCallbacks
553
 
        def test_get_container_no_principal(self):
554
 
                """Get container should handle no principal."""
555
 
                mysql_ep = RelationEndpoint("mysql", "juju-info", "juju-info",
556
 
                                                                        "server", "global")
557
 
                logging_ep = RelationEndpoint("logging", "juju-info", "juju-info",
558
 
                                                                          "client", "container")
559
 
 
560
 
                logging_charm_state = yield self.get_subordinate_charm()
561
 
                self.assertTrue(logging_charm_state.is_subordinate())
562
 
                log_state = yield self.service_state_manager.add_service_state(
563
 
                        "logging", logging_charm_state, dummy_constraints)
564
 
                mysql_state = yield self.service_state_manager.add_service_state(
565
 
                        "mysql", self.charm_state, dummy_constraints)
566
 
 
567
 
                relation_state, service_states = (yield
568
 
                        self.relation_state_manager.add_relation_state(
569
 
                                mysql_ep, logging_ep))
570
 
 
571
 
                unit_state1 = yield mysql_state.add_unit_state()
572
 
                unit_state0 = yield log_state.add_unit_state(container=unit_state1)
573
 
 
574
 
                unit_state3 = yield mysql_state.add_unit_state()
575
 
                unit_state2 = yield log_state.add_unit_state(container=unit_state3)
576
 
 
577
 
                self.assertEquals((yield unit_state1.get_container()), None)
578
 
                self.assertEquals((yield unit_state0.get_container()), unit_state1)
579
 
                self.assertEquals((yield unit_state2.get_container()), unit_state3)
580
 
                self.assertEquals((yield unit_state3.get_container()), None)
581
 
 
582
 
                # now remove a principal node and test again
583
 
 
584
 
                yield mysql_state.remove_unit_state(unit_state1)
585
 
                container = yield unit_state0.get_container()
586
 
                self.assertEquals(container, None)
587
 
 
588
 
                # the other pair is still fine
589
 
                self.assertEquals((yield unit_state2.get_container()), unit_state3)
590
 
                self.assertEquals((yield unit_state3.get_container()), None)
591
 
 
592
 
 
593
 
        @inlineCallbacks
594
 
        def test_add_service_unit_with_changing_state(self):
595
 
                """
596
 
                When adding a service unit, there's a chance that the
597
 
                service will go away mid-way through.  Rather than blowing
598
 
                up randomly, a nice error should be raised.
599
 
                """
600
 
                service_state = yield self.service_state_manager.add_service_state(
601
 
                        "wordpress", self.charm_state, dummy_constraints)
602
 
 
603
 
                yield self.remove_service(service_state.internal_id)
604
 
 
605
 
                d = service_state.add_unit_state()
606
 
                yield self.assertFailure(d, StateChanged)
607
 
 
608
 
        @inlineCallbacks
609
 
        def test_get_unit_names(self):
610
 
                """A service's units names are retrievable."""
611
 
                service_state = yield self.service_state_manager.add_service_state(
612
 
                        "wordpress", self.charm_state, dummy_constraints)
613
 
 
614
 
                expected_names = []
615
 
                for i in range(3):
616
 
                        unit_state = yield service_state.add_unit_state()
617
 
                        expected_names.append(unit_state.unit_name)
618
 
 
619
 
                unit_names = yield service_state.get_unit_names()
620
 
                self.assertEqual(unit_names, expected_names)
621
 
 
622
 
        @inlineCallbacks
623
 
        def test_remove_service_unit(self):
624
 
                """Removing a service unit removes all state associated.
625
 
                """
626
 
                service_state = yield self.service_state_manager.add_service_state(
627
 
                        "wordpress", self.charm_state, dummy_constraints)
628
 
 
629
 
                unit_state = yield service_state.add_unit_state()
630
 
 
631
 
                # Assign to a machine
632
 
                machine_state = yield self.machine_state_manager.add_machine_state(
633
 
                        series_constraints)
634
 
                yield unit_state.assign_to_machine(machine_state)
635
 
                # Connect a unit agent
636
 
                yield unit_state.connect_agent()
637
 
 
638
 
                # Now try and destroy it.
639
 
                yield service_state.remove_unit_state(unit_state)
640
 
 
641
 
                # Verify destruction.
642
 
                topology = yield self.get_topology()
643
 
                self.assertTrue(topology.has_service(service_state.internal_id))
644
 
                self.assertFalse(
645
 
                        topology.has_service_unit(
646
 
                                service_state.internal_id, unit_state.internal_id))
647
 
 
648
 
                exists = yield self.client.exists("/units/%s" % unit_state.internal_id)
649
 
                self.assertFalse(exists)
650
 
 
651
 
        def test_remove_service_unit_nonexistant(self):
652
 
                """Removing a non existant service unit, is fine."""
653
 
 
654
 
                service_state = yield self.service_state_manager.add_service_state(
655
 
                        "wordpress", self.charm_state, dummy_constraints)
656
 
                unit_state = yield service_state.add_unit_state()
657
 
                yield service_state.remove_unit_state(unit_state)
658
 
                yield service_state.remove_unit_state(unit_state)
659
 
 
660
 
        @inlineCallbacks
661
 
        def test_get_all_service_states(self):
662
 
                services = yield self.service_state_manager.get_all_service_states()
663
 
                self.assertFalse(services)
664
 
 
665
 
                yield self.service_state_manager.add_service_state(
666
 
                        "wordpress", self.charm_state, dummy_constraints)
667
 
                services = yield self.service_state_manager.get_all_service_states()
668
 
                self.assertEquals(len(services), 1)
669
 
 
670
 
                yield self.service_state_manager.add_service_state(
671
 
                        "mysql", self.charm_state, dummy_constraints)
672
 
                services = yield self.service_state_manager.get_all_service_states()
673
 
                self.assertEquals(len(services), 2)
674
 
 
675
 
        @inlineCallbacks
676
 
        def test_get_service_unit(self):
677
 
                """
678
 
                Getting back service units should be possible using the
679
 
                user-oriented id.
680
 
                """
681
 
                service_state0 = yield self.service_state_manager.add_service_state(
682
 
                        "wordpress", self.charm_state, dummy_constraints)
683
 
                service_state1 = yield self.service_state_manager.add_service_state(
684
 
                        "mysql", self.charm_state, dummy_constraints)
685
 
 
686
 
                yield service_state0.add_unit_state()
687
 
                yield service_state1.add_unit_state()
688
 
                yield service_state0.add_unit_state()
689
 
                yield service_state1.add_unit_state()
690
 
 
691
 
                unit_state0 = yield service_state0.get_unit_state("wordpress/0")
692
 
                unit_state1 = yield service_state1.get_unit_state("mysql/0")
693
 
                unit_state2 = yield service_state0.get_unit_state("wordpress/1")
694
 
                unit_state3 = yield service_state1.get_unit_state("mysql/1")
695
 
 
696
 
                self.assertEquals(unit_state0.internal_id, "unit-0000000000")
697
 
                self.assertEquals(unit_state1.internal_id, "unit-0000000001")
698
 
                self.assertEquals(unit_state2.internal_id, "unit-0000000002")
699
 
                self.assertEquals(unit_state3.internal_id, "unit-0000000003")
700
 
 
701
 
                self.assertEquals(unit_state0.unit_name, "wordpress/0")
702
 
                self.assertEquals(unit_state1.unit_name, "mysql/0")
703
 
                self.assertEquals(unit_state2.unit_name, "wordpress/1")
704
 
                self.assertEquals(unit_state3.unit_name, "mysql/1")
705
 
 
706
 
        @inlineCallbacks
707
 
        def test_get_all_unit_states(self):
708
 
                service_state0 = yield self.service_state_manager.add_service_state(
709
 
                        "wordpress", self.charm_state, dummy_constraints)
710
 
                service_state1 = yield self.service_state_manager.add_service_state(
711
 
                        "mysql", self.charm_state, dummy_constraints)
712
 
 
713
 
                yield service_state0.add_unit_state()
714
 
                yield service_state1.add_unit_state()
715
 
                yield service_state0.add_unit_state()
716
 
                yield service_state1.add_unit_state()
717
 
 
718
 
                unit_state0 = yield service_state0.get_unit_state("wordpress/0")
719
 
                unit_state1 = yield service_state1.get_unit_state("mysql/0")
720
 
                unit_state2 = yield service_state0.get_unit_state("wordpress/1")
721
 
                unit_state3 = yield service_state1.get_unit_state("mysql/1")
722
 
 
723
 
                wordpress_units = yield service_state0.get_all_unit_states()
724
 
                self.assertEquals(
725
 
                        set(wordpress_units), set((unit_state0, unit_state2)))
726
 
 
727
 
                mysql_units = yield service_state1.get_all_unit_states()
728
 
                self.assertEquals(set(mysql_units), set((unit_state1, unit_state3)))
729
 
 
730
 
        @inlineCallbacks
731
 
        def test_get_all_unit_states_with_changing_state(self):
732
 
                """
733
 
                When getting the service unit states, there's a chance that
734
 
                the service will go away mid-way through.  Rather than blowing
735
 
                up randomly, a nice error should be raised.
736
 
                """
737
 
                service_state = yield self.service_state_manager.add_service_state(
738
 
                        "wordpress", self.charm_state, dummy_constraints)
739
 
                yield service_state.add_unit_state()
740
 
                unit_state = (yield service_state.get_all_unit_states())[0]
741
 
                self.assertEqual(unit_state.unit_name, "wordpress/0")
742
 
                yield self.remove_service(service_state.internal_id)
743
 
                yield self.assertFailure(
744
 
                        service_state.get_all_unit_states(), StateChanged)
745
 
 
746
 
        @inlineCallbacks
747
 
        def test_set_functions(self):
748
 
                wordpress = yield self.service_state_manager.add_service_state(
749
 
                        "wordpress", self.charm_state, dummy_constraints)
750
 
                mysql = yield self.service_state_manager.add_service_state(
751
 
                        "mysql", self.charm_state, dummy_constraints)
752
 
 
753
 
                s1 = yield self.service_state_manager.get_service_state(
754
 
                        "wordpress")
755
 
                s2 = yield self.service_state_manager.get_service_state(
756
 
                        "mysql")
757
 
                self.assertEquals(hash(s1), hash(wordpress))
758
 
                self.assertEquals(hash(s2), hash(mysql))
759
 
 
760
 
                self.assertNotEqual(s1, object())
761
 
                self.assertNotEqual(s1, s2)
762
 
 
763
 
                self.assertEquals(s1, wordpress)
764
 
                self.assertEquals(s2, mysql)
765
 
 
766
 
                us0 = yield wordpress.add_unit_state()
767
 
                us1 = yield wordpress.add_unit_state()
768
 
 
769
 
                unit_state0 = yield wordpress.get_unit_state("wordpress/0")
770
 
                unit_state1 = yield wordpress.get_unit_state("wordpress/1")
771
 
 
772
 
                self.assertEquals(us0, unit_state0)
773
 
                self.assertEquals(us1, unit_state1)
774
 
                self.assertEquals(hash(us1), hash(unit_state1))
775
 
 
776
 
                self.assertNotEqual(us0, object())
777
 
                self.assertNotEqual(us0, us1)
778
 
 
779
 
        @inlineCallbacks
780
 
        def test_get_service_unit_not_found(self):
781
 
                """
782
 
                Attempting to retrieve a non-existent service unit should
783
 
                result in an errback.
784
 
                """
785
 
                service_state0 = yield self.service_state_manager.add_service_state(
786
 
                        "wordpress", self.charm_state, dummy_constraints)
787
 
                service_state1 = yield self.service_state_manager.add_service_state(
788
 
                        "mysql", self.charm_state, dummy_constraints)
789
 
 
790
 
                # Add some state in a different service to make it a little
791
 
                # bit more prone to breaking in case of errors.
792
 
                yield service_state1.add_unit_state()
793
 
 
794
 
                try:
795
 
                        yield service_state0.get_unit_state("wordpress/0")
796
 
                except ServiceUnitStateNotFound, e:
797
 
                        self.assertEquals(e.unit_name, "wordpress/0")
798
 
                else:
799
 
                        self.fail("Error not raised")
800
 
 
801
 
        @inlineCallbacks
802
 
        def test_get_set_public_address(self):
803
 
                service_state = yield self.service_state_manager.add_service_state(
804
 
                        "wordpress", self.charm_state, dummy_constraints)
805
 
                unit_state = yield service_state.add_unit_state()
806
 
                self.assertEqual((yield unit_state.get_public_address()), None)
807
 
                yield unit_state.set_public_address("example.foobar.com")
808
 
                yield self.assertEqual(
809
 
                        (yield unit_state.get_public_address()),
810
 
                         "example.foobar.com")
811
 
 
812
 
        @inlineCallbacks
813
 
        def test_get_set_private_address(self):
814
 
                service_state = yield self.service_state_manager.add_service_state(
815
 
                        "wordpress", self.charm_state, dummy_constraints)
816
 
                unit_state = yield service_state.add_unit_state()
817
 
                self.assertEqual((yield unit_state.get_private_address()), None)
818
 
                yield unit_state.set_private_address("example.local")
819
 
                yield self.assertEqual(
820
 
                        (yield unit_state.get_private_address()),
821
 
                         "example.local")
822
 
 
823
 
        @inlineCallbacks
824
 
        def test_get_service_unit_with_changing_state(self):
825
 
                """
826
 
                If a service is removed during operation, get_service_unit()
827
 
                should raise a nice error.
828
 
                """
829
 
                service_state = yield self.service_state_manager.add_service_state(
830
 
                        "wordpress", self.charm_state, dummy_constraints)
831
 
 
832
 
                yield self.remove_service(service_state.internal_id)
833
 
 
834
 
                d = service_state.get_unit_state("wordpress/0")
835
 
                yield self.assertFailure(d, StateChanged)
836
 
 
837
 
        @inlineCallbacks
838
 
        def test_get_service_unit_with_bad_service_name(self):
839
 
                """
840
 
                Service unit names contain a service name embedded into
841
 
                them.  The service name requested when calling get_unit_state()
842
 
                must match that of the object being used.
843
 
                """
844
 
                service_state0 = yield self.service_state_manager.add_service_state(
845
 
                        "wordpress", self.charm_state, dummy_constraints)
846
 
                service_state1 = yield self.service_state_manager.add_service_state(
847
 
                        "mysql", self.charm_state, dummy_constraints)
848
 
 
849
 
                # Add some state in a different service to make it a little
850
 
                # bit more prone to breaking in case of errors.
851
 
                yield service_state1.add_unit_state()
852
 
 
853
 
                try:
854
 
                        yield service_state0.get_unit_state("mysql/0")
855
 
                except BadServiceStateName, e:
856
 
                        self.assertEquals(e.expected_name, "wordpress")
857
 
                        self.assertEquals(e.obtained_name, "mysql")
858
 
                else:
859
 
                        self.fail("Error not raised")
860
 
 
861
 
        @inlineCallbacks
862
 
        def test_assign_unit_to_machine(self):
863
 
                service_state = yield self.service_state_manager.add_service_state(
864
 
                        "wordpress", self.charm_state, dummy_constraints)
865
 
                unit_state = yield service_state.add_unit_state()
866
 
                machine_state = yield self.machine_state_manager.add_machine_state(
867
 
                        series_constraints)
868
 
 
869
 
                yield unit_state.assign_to_machine(machine_state)
870
 
 
871
 
                topology = yield self.get_topology()
872
 
 
873
 
                self.assertEquals(
874
 
                        topology.get_service_unit_machine(service_state.internal_id,
875
 
                                                                                          unit_state.internal_id),
876
 
                        machine_state.internal_id)
877
 
 
878
 
        @inlineCallbacks
879
 
        def test_assign_unit_to_machine_with_changing_state(self):
880
 
                service_state = yield self.service_state_manager.add_service_state(
881
 
                        "wordpress", self.charm_state, dummy_constraints)
882
 
                unit_state = yield service_state.add_unit_state()
883
 
                machine_state = yield self.machine_state_manager.add_machine_state(
884
 
                        series_constraints)
885
 
 
886
 
                yield self.remove_service_unit(service_state.internal_id,
887
 
                                                                           unit_state.internal_id)
888
 
 
889
 
                d = unit_state.assign_to_machine(machine_state)
890
 
                yield self.assertFailure(d, StateChanged)
891
 
 
892
 
                yield self.remove_service(service_state.internal_id)
893
 
 
894
 
                d = unit_state.assign_to_machine(machine_state)
895
 
                yield self.assertFailure(d, StateChanged)
896
 
 
897
 
        @inlineCallbacks
898
 
        def test_unassign_unit_from_machine(self):
899
 
                service_state = yield self.service_state_manager.add_service_state(
900
 
                        "wordpress", self.charm_state, dummy_constraints)
901
 
                unit_state = yield service_state.add_unit_state()
902
 
                machine_state = yield self.machine_state_manager.add_machine_state(
903
 
                        series_constraints)
904
 
 
905
 
                yield unit_state.assign_to_machine(machine_state)
906
 
                yield unit_state.unassign_from_machine()
907
 
 
908
 
                topology = yield self.get_topology()
909
 
 
910
 
                self.assertEquals(
911
 
                        topology.get_service_unit_machine(service_state.internal_id,
912
 
                                                                                          unit_state.internal_id),
913
 
                        None)
914
 
 
915
 
        @inlineCallbacks
916
 
        def test_get_set_clear_resolved(self):
917
 
                """The a unit can be set to resolved to mark a future transition, with
918
 
                an optional retry flag."""
919
 
 
920
 
                unit_state = yield self.get_unit_state()
921
 
 
922
 
                self.assertIdentical((yield unit_state.get_resolved()), None)
923
 
                yield unit_state.set_resolved(NO_HOOKS)
924
 
 
925
 
                yield self.assertFailure(
926
 
                        unit_state.set_resolved(NO_HOOKS),
927
 
                        ServiceUnitResolvedAlreadyEnabled)
928
 
                yield self.assertEqual(
929
 
 
930
 
                                (yield unit_state.get_resolved()), {"retry": NO_HOOKS})
931
 
 
932
 
                yield unit_state.clear_resolved()
933
 
                self.assertIdentical((yield unit_state.get_resolved()), None)
934
 
                yield unit_state.clear_resolved()
935
 
 
936
 
                yield self.assertFailure(unit_state.set_resolved(None), ValueError)
937
 
 
938
 
        @inlineCallbacks
939
 
        def test_watch_resolved(self):
940
 
                """A unit resolved watch can be instituted on a permanent basis."""
941
 
                unit_state = yield self.get_unit_state()
942
 
 
943
 
                results = []
944
 
 
945
 
                def callback(value):
946
 
                        results.append(value)
947
 
 
948
 
                unit_state.watch_resolved(callback)
949
 
                yield unit_state.set_resolved(RETRY_HOOKS)
950
 
                yield unit_state.clear_resolved()
951
 
                yield unit_state.set_resolved(NO_HOOKS)
952
 
 
953
 
                yield self.poke_zk()
954
 
 
955
 
                self.assertEqual(len(results), 4)
956
 
                self.assertIdentical(results.pop(0), False)
957
 
                self.assertEqual(results.pop(0).type_name, "created")
958
 
                self.assertEqual(results.pop(0).type_name, "deleted")
959
 
                self.assertEqual(results.pop(0).type_name, "created")
960
 
 
961
 
                self.assertEqual(
962
 
                        (yield unit_state.get_resolved()),
963
 
                        {"retry": NO_HOOKS})
964
 
 
965
 
        @inlineCallbacks
966
 
        def test_watch_resolved_processes_current_state(self):
967
 
                """The watch method processes the current state before returning."""
968
 
                unit_state = yield self.get_unit_state()
969
 
 
970
 
                results = []
971
 
 
972
 
                @inlineCallbacks
973
 
                def callback(value):
974
 
                        results.append((yield unit_state.get_resolved()))
975
 
 
976
 
                yield unit_state.watch_resolved(callback)
977
 
                self.assertTrue(results)
978
 
 
979
 
        @inlineCallbacks
980
 
        def test_stop_watch_resolved(self):
981
 
                """A unit resolved watch can be instituted on a permanent basis.
982
 
 
983
 
                However the callback can raise StopWatcher at anytime to stop the watch
984
 
                """
985
 
                unit_state = yield self.get_unit_state()
986
 
 
987
 
                results = []
988
 
 
989
 
                def callback(value):
990
 
                        results.append(value)
991
 
                        if len(results) == 1:
992
 
                                raise StopWatcher()
993
 
                        if len(results) == 3:
994
 
                                raise StopWatcher()
995
 
 
996
 
                unit_state.watch_resolved(callback)
997
 
                yield unit_state.set_resolved(RETRY_HOOKS)
998
 
                yield unit_state.clear_resolved()
999
 
                yield self.poke_zk()
1000
 
 
1001
 
                unit_state.watch_resolved(callback)
1002
 
                yield unit_state.set_resolved(NO_HOOKS)
1003
 
                yield unit_state.clear_resolved()
1004
 
 
1005
 
                yield self.poke_zk()
1006
 
 
1007
 
                self.assertEqual(len(results), 3)
1008
 
                self.assertIdentical(results.pop(0), False)
1009
 
                self.assertIdentical(results.pop(0), False)
1010
 
                self.assertEqual(results.pop(0).type_name, "created")
1011
 
 
1012
 
                self.assertEqual(
1013
 
                        (yield unit_state.get_resolved()), None)
1014
 
 
1015
 
        @inlineCallbacks
1016
 
        def test_get_set_clear_relation_resolved(self):
1017
 
                """The a unit's realtions can be set to resolved to mark a
1018
 
                future transition, with an optional retry flag."""
1019
 
 
1020
 
                unit_state = yield self.get_unit_state()
1021
 
 
1022
 
                self.assertIdentical((yield unit_state.get_relation_resolved()), None)
1023
 
                yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
1024
 
 
1025
 
                # Trying to set a conflicting raises an error
1026
 
                yield self.assertFailure(
1027
 
                        unit_state.set_relation_resolved({"0": NO_HOOKS}),
1028
 
                        ServiceUnitRelationResolvedAlreadyEnabled)
1029
 
 
1030
 
                # Doing the same thing is fine
1031
 
                yield unit_state.set_relation_resolved({"0": RETRY_HOOKS}),
1032
 
 
1033
 
                # Its fine to put in new values
1034
 
                yield unit_state.set_relation_resolved({"21": RETRY_HOOKS})
1035
 
                yield self.assertEqual(
1036
 
                        (yield unit_state.get_relation_resolved()),
1037
 
                        {"0": RETRY_HOOKS, "21": RETRY_HOOKS})
1038
 
 
1039
 
                yield unit_state.clear_relation_resolved()
1040
 
                self.assertIdentical((yield unit_state.get_relation_resolved()), None)
1041
 
                yield unit_state.clear_relation_resolved()
1042
 
 
1043
 
                yield self.assertFailure(
1044
 
                        unit_state.set_relation_resolved(True), ValueError)
1045
 
                yield self.assertFailure(
1046
 
                        unit_state.set_relation_resolved(None), ValueError)
1047
 
 
1048
 
        @inlineCallbacks
1049
 
        def test_watch_relation_resolved(self):
1050
 
                """A unit resolved watch can be instituted on a permanent basis."""
1051
 
                unit_state = yield self.get_unit_state()
1052
 
 
1053
 
                results = []
1054
 
 
1055
 
                def callback(value):
1056
 
                        results.append(value)
1057
 
 
1058
 
                unit_state.watch_relation_resolved(callback)
1059
 
                yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
1060
 
                yield unit_state.clear_relation_resolved()
1061
 
                yield unit_state.set_relation_resolved({"0": NO_HOOKS})
1062
 
 
1063
 
                yield self.poke_zk()
1064
 
 
1065
 
                self.assertEqual(len(results), 4)
1066
 
                self.assertIdentical(results.pop(0), False)
1067
 
                self.assertEqual(results.pop(0).type_name, "created")
1068
 
                self.assertEqual(results.pop(0).type_name, "deleted")
1069
 
                self.assertEqual(results.pop(0).type_name, "created")
1070
 
 
1071
 
                self.assertEqual(
1072
 
                        (yield unit_state.get_relation_resolved()),
1073
 
                        {"0": NO_HOOKS})
1074
 
 
1075
 
        @inlineCallbacks
1076
 
        def test_watch_relation_resolved_processes_current_state(self):
1077
 
                """The watch method returns only after processing the current state."""
1078
 
                unit_state = yield self.get_unit_state()
1079
 
 
1080
 
                results = []
1081
 
 
1082
 
                @inlineCallbacks
1083
 
                def callback(value):
1084
 
                        results.append((yield unit_state.get_relation_resolved()))
1085
 
                yield unit_state.watch_relation_resolved(callback)
1086
 
                self.assertTrue(results)
1087
 
 
1088
 
        @inlineCallbacks
1089
 
        def test_stop_watch_relation_resolved(self):
1090
 
                """A unit resolved watch can be instituted on a permanent basis."""
1091
 
                unit_state = yield self.get_unit_state()
1092
 
 
1093
 
                results = []
1094
 
 
1095
 
                def callback(value):
1096
 
                        results.append(value)
1097
 
 
1098
 
                        if len(results) == 1:
1099
 
                                raise StopWatcher()
1100
 
 
1101
 
                        if len(results) == 3:
1102
 
                                raise StopWatcher()
1103
 
 
1104
 
                unit_state.watch_relation_resolved(callback)
1105
 
                yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
1106
 
                yield unit_state.clear_relation_resolved()
1107
 
                yield self.poke_zk()
1108
 
                self.assertEqual(len(results), 1)
1109
 
 
1110
 
                unit_state.watch_relation_resolved(callback)
1111
 
                yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
1112
 
                yield unit_state.clear_relation_resolved()
1113
 
                yield self.poke_zk()
1114
 
                self.assertEqual(len(results), 3)
1115
 
                self.assertIdentical(results.pop(0), False)
1116
 
                self.assertIdentical(results.pop(0), False)
1117
 
                self.assertEqual(results.pop(0).type_name, "created")
1118
 
 
1119
 
                self.assertEqual(
1120
 
                        (yield unit_state.get_relation_resolved()), None)
1121
 
 
1122
 
        @inlineCallbacks
1123
 
        def test_watch_resolved_slow_callback(self):
1124
 
                """A slow watch callback is still invoked serially."""
1125
 
                unit_state = yield self.get_unit_state()
1126
 
 
1127
 
                callbacks = [Deferred() for i in range(5)]
1128
 
                results = []
1129
 
                contents = []
1130
 
 
1131
 
                @inlineCallbacks
1132
 
                def watch(value):
1133
 
                        results.append(value)
1134
 
                        yield callbacks[len(results) - 1]
1135
 
                        contents.append((yield unit_state.get_resolved()))
1136
 
 
1137
 
                callbacks[0].callback(True)
1138
 
                yield unit_state.watch_resolved(watch)
1139
 
 
1140
 
                # These get collapsed into a single event
1141
 
                yield unit_state.set_resolved(RETRY_HOOKS)
1142
 
                yield unit_state.clear_resolved()
1143
 
                yield self.poke_zk()
1144
 
 
1145
 
                # Verify the callback hasn't completed
1146
 
                self.assertEqual(len(results), 2)
1147
 
                self.assertEqual(len(contents), 1)
1148
 
 
1149
 
                # Let it finish
1150
 
                callbacks[1].callback(True)
1151
 
                yield self.poke_zk()
1152
 
 
1153
 
                # Verify result counts
1154
 
                self.assertEqual(len(results), 3)
1155
 
                self.assertEqual(len(contents), 2)
1156
 
 
1157
 
                # Verify result values. Even though we have created event, the
1158
 
                # setting retrieved shows the hook is not enabled.
1159
 
                self.assertEqual(results[-1].type_name, "deleted")
1160
 
                self.assertEqual(contents[-1], None)
1161
 
 
1162
 
                yield unit_state.set_resolved(NO_HOOKS)
1163
 
                callbacks[2].callback(True)
1164
 
                yield self.poke_zk()
1165
 
 
1166
 
                self.assertEqual(len(results), 4)
1167
 
                self.assertEqual(contents[-1], {"retry": NO_HOOKS})
1168
 
 
1169
 
                # Clear out any pending activity.
1170
 
                yield self.poke_zk()
1171
 
 
1172
 
        @inlineCallbacks
1173
 
        def test_watch_relation_resolved_slow_callback(self):
1174
 
                """A slow watch callback is still invoked serially."""
1175
 
                unit_state = yield self.get_unit_state()
1176
 
 
1177
 
                callbacks = [Deferred() for i in range(5)]
1178
 
                results = []
1179
 
                contents = []
1180
 
 
1181
 
                @inlineCallbacks
1182
 
                def watch(value):
1183
 
                        results.append(value)
1184
 
                        yield callbacks[len(results) - 1]
1185
 
                        contents.append((yield unit_state.get_relation_resolved()))
1186
 
 
1187
 
                callbacks[0].callback(True)
1188
 
                yield unit_state.watch_relation_resolved(watch)
1189
 
 
1190
 
                # These get collapsed into a single event
1191
 
                yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
1192
 
                yield unit_state.clear_relation_resolved()
1193
 
                yield self.poke_zk()
1194
 
 
1195
 
                # Verify the callback hasn't completed
1196
 
                self.assertEqual(len(results), 2)
1197
 
                self.assertEqual(len(contents), 1)
1198
 
 
1199
 
                # Let it finish
1200
 
                callbacks[1].callback(True)
1201
 
                yield self.poke_zk()
1202
 
 
1203
 
                # Verify result counts
1204
 
                self.assertEqual(len(results), 3)
1205
 
                self.assertEqual(len(contents), 2)
1206
 
 
1207
 
                # Verify result values. Even though we have created event, the
1208
 
                # setting retrieved shows the hook is not enabled.
1209
 
                self.assertEqual(results[-1].type_name, "deleted")
1210
 
                self.assertEqual(contents[-1], None)
1211
 
 
1212
 
                yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
1213
 
                callbacks[2].callback(True)
1214
 
                yield self.poke_zk()
1215
 
 
1216
 
                self.assertEqual(len(results), 4)
1217
 
                self.assertEqual(contents[-1], {"0": RETRY_HOOKS})
1218
 
 
1219
 
                # Clear out any pending activity.
1220
 
                yield self.poke_zk()
1221
 
 
1222
 
        @inlineCallbacks
1223
 
        def test_set_and_clear_upgrade_flag(self):
1224
 
                """An upgrade flag can be set on a unit."""
1225
 
 
1226
 
                # Defaults to false
1227
 
                unit_state = yield self.get_unit_state()
1228
 
                upgrade_flag = yield unit_state.get_upgrade_flag()
1229
 
                self.assertEqual(upgrade_flag, False)
1230
 
 
1231
 
                # Can be set
1232
 
                yield unit_state.set_upgrade_flag()
1233
 
                upgrade_flag = yield unit_state.get_upgrade_flag()
1234
 
                self.assertEqual(upgrade_flag, {'force': False})
1235
 
 
1236
 
                # Attempting to set multiple times is an error if the values
1237
 
                # differ.
1238
 
                yield self.assertFailure(
1239
 
                        unit_state.set_upgrade_flag(force=True),
1240
 
                        ServiceUnitUpgradeAlreadyEnabled)
1241
 
                self.assertEqual(upgrade_flag, {'force': False})
1242
 
 
1243
 
                # Can be cleared
1244
 
                yield unit_state.clear_upgrade_flag()
1245
 
                upgrade_flag = yield unit_state.get_upgrade_flag()
1246
 
                self.assertEqual(upgrade_flag, False)
1247
 
 
1248
 
                # Can be cleared multiple times
1249
 
                yield unit_state.clear_upgrade_flag()
1250
 
                upgrade_flag = yield unit_state.get_upgrade_flag()
1251
 
                self.assertEqual(upgrade_flag, False)
1252
 
 
1253
 
                # A empty node present is not problematic
1254
 
                yield self.client.create(unit_state._upgrade_flag_path, "")
1255
 
                upgrade_flag = yield unit_state.get_upgrade_flag()
1256
 
                self.assertEqual(upgrade_flag, False)
1257
 
 
1258
 
                yield unit_state.set_upgrade_flag(force=True)
1259
 
                upgrade_flag = yield unit_state.get_upgrade_flag()
1260
 
                self.assertEqual(upgrade_flag, {"force": True})
1261
 
 
1262
 
        @inlineCallbacks
1263
 
        def test_watch_upgrade_flag_once(self):
1264
 
                """An upgrade watch can be set to notified of presence and changes."""
1265
 
                unit_state = yield self.get_unit_state()
1266
 
                yield unit_state.set_upgrade_flag()
1267
 
 
1268
 
                results = []
1269
 
 
1270
 
                def callback(value):
1271
 
                        results.append(value)
1272
 
 
1273
 
                unit_state.watch_upgrade_flag(callback, permanent=False)
1274
 
                yield unit_state.clear_upgrade_flag()
1275
 
                yield unit_state.set_upgrade_flag(force=True)
1276
 
                yield self.sleep(0.1)
1277
 
                yield self.poke_zk()
1278
 
                self.assertEqual(len(results), 2)
1279
 
                self.assertIdentical(results.pop(0), True)
1280
 
                self.assertIdentical(results.pop().type_name, "deleted")
1281
 
 
1282
 
                self.assertEqual(
1283
 
                        (yield unit_state.get_upgrade_flag()),
1284
 
                        {'force': True})
1285
 
 
1286
 
        @inlineCallbacks
1287
 
        def test_watch_upgrade_processes_current_state(self):
1288
 
                unit_state = yield self.get_unit_state()
1289
 
                results = []
1290
 
 
1291
 
                @inlineCallbacks
1292
 
                def callback(value):
1293
 
                        results.append((yield unit_state.get_upgrade_flag()))
1294
 
 
1295
 
                yield unit_state.watch_upgrade_flag(callback)
1296
 
                self.assertTrue(results)
1297
 
 
1298
 
        @inlineCallbacks
1299
 
        def test_watch_upgrade_flag_permanent(self):
1300
 
                """An upgrade watch can be instituted on a permanent basis."""
1301
 
                unit_state = yield self.get_unit_state()
1302
 
 
1303
 
                results = []
1304
 
 
1305
 
                def callback(value):
1306
 
                        results.append(value)
1307
 
 
1308
 
                yield unit_state.watch_upgrade_flag(callback)
1309
 
                self.assertTrue(results)
1310
 
                yield unit_state.set_upgrade_flag()
1311
 
                yield unit_state.clear_upgrade_flag()
1312
 
                yield unit_state.set_upgrade_flag()
1313
 
 
1314
 
                yield self.poke_zk()
1315
 
 
1316
 
                self.assertEqual(len(results), 4)
1317
 
                self.assertIdentical(results.pop(0), False)
1318
 
                self.assertIdentical(results.pop(0).type_name, "created")
1319
 
                self.assertIdentical(results.pop(0).type_name, "deleted")
1320
 
                self.assertIdentical(results.pop(0).type_name, "created")
1321
 
 
1322
 
                self.assertEqual(
1323
 
                        (yield unit_state.get_upgrade_flag()),
1324
 
                        {'force': False})
1325
 
 
1326
 
        @inlineCallbacks
1327
 
        def test_watch_upgrade_flag_waits_on_slow_callbacks(self):
1328
 
                """A slow watch callback is still invoked serially."""
1329
 
                unit_state = yield self.get_unit_state()
1330
 
 
1331
 
                callbacks = [Deferred() for i in range(5)]
1332
 
                results = []
1333
 
                contents = []
1334
 
 
1335
 
                @inlineCallbacks
1336
 
                def watch(value):
1337
 
                        results.append(value)
1338
 
                        yield callbacks[len(results) - 1]
1339
 
                        contents.append((yield unit_state.get_upgrade_flag()))
1340
 
 
1341
 
                yield callbacks[0].callback(True)
1342
 
                yield unit_state.watch_upgrade_flag(watch)
1343
 
 
1344
 
                # These get collapsed into a single event
1345
 
                yield unit_state.set_upgrade_flag()
1346
 
                yield unit_state.clear_upgrade_flag()
1347
 
                yield self.poke_zk()
1348
 
 
1349
 
                # Verify the callback hasn't completed
1350
 
                self.assertEqual(len(results), 2)
1351
 
                self.assertEqual(len(contents), 1)
1352
 
 
1353
 
                # Let it finish
1354
 
                callbacks[1].callback(True)
1355
 
                yield self.poke_zk()
1356
 
 
1357
 
                # Verify result counts
1358
 
                self.assertEqual(len(results), 3)
1359
 
                self.assertEqual(len(contents), 2)
1360
 
 
1361
 
                # Verify result values. Even though we have created event, the
1362
 
                # setting retrieved shows the hook is not enabled.
1363
 
                self.assertEqual(results[-1].type_name, "deleted")
1364
 
                self.assertEqual(contents[-1], False)
1365
 
 
1366
 
                yield unit_state.set_upgrade_flag()
1367
 
                yield self.poke_zk()
1368
 
 
1369
 
                # Verify the callback hasn't completed
1370
 
                self.assertEqual(len(contents), 2)
1371
 
 
1372
 
                callbacks[2].callback(True)
1373
 
                yield self.poke_zk()
1374
 
 
1375
 
                # Verify values.
1376
 
                self.assertEqual(len(contents), 3)
1377
 
                self.assertEqual(results[-1].type_name, "created")
1378
 
                self.assertEqual(contents[-1], {'force': False})
1379
 
 
1380
 
                # Clear out any pending activity.
1381
 
                yield self.poke_zk()
1382
 
 
1383
 
        @inlineCallbacks
1384
 
        def test_enable_debug_hook(self):
1385
 
                """Unit hook debugging can be enabled on the unit state."""
1386
 
                unit_state = yield self.get_unit_state()
1387
 
                enabled = yield unit_state.enable_hook_debug(["*"])
1388
 
                self.assertIdentical(enabled, True)
1389
 
                content, stat = yield self.client.get(
1390
 
                        "/units/%s/debug" % unit_state.internal_id)
1391
 
                data = yaml.load(content)
1392
 
                self.assertEqual(data, {"debug_hooks": ["*"]})
1393
 
 
1394
 
        @inlineCallbacks
1395
 
        def test_enable_debug_multiple_named_hooks(self):
1396
 
                """Unit hook debugging can be enabled for multiple hooks."""
1397
 
                unit_state = yield self.get_unit_state()
1398
 
                enabled = yield unit_state.enable_hook_debug(
1399
 
                        ["db-relation-broken", "db-relation-changed"])
1400
 
 
1401
 
                self.assertIdentical(enabled, True)
1402
 
                content, stat = yield self.client.get(
1403
 
                        "/units/%s/debug" % unit_state.internal_id)
1404
 
                data = yaml.load(content)
1405
 
                self.assertEqual(
1406
 
                        data,
1407
 
                        {"debug_hooks": ["db-relation-broken", "db-relation-changed"]})
1408
 
 
1409
 
        @inlineCallbacks
1410
 
        def test_enable_debug_all_and_named_is_error(self):
1411
 
                """Unit hook debugging can be enabled for multiple hooks,
1412
 
                but only if they are all named hooks."""
1413
 
                unit_state = yield self.get_unit_state()
1414
 
 
1415
 
                error = yield self.assertFailure(
1416
 
                        unit_state.enable_hook_debug(["*", "db-relation-changed"]),
1417
 
                        ValueError)
1418
 
                self.assertEquals(
1419
 
                        str(error),
1420
 
                        "Ambigious to debug all hooks and named hooks "
1421
 
                        "['*', 'db-relation-changed']")
1422
 
 
1423
 
        @inlineCallbacks
1424
 
        def test_enable_debug_requires_sequence(self):
1425
 
                """The enable hook debug only accepts a sequences of names.
1426
 
                """
1427
 
                unit_state = yield self.get_unit_state()
1428
 
 
1429
 
                error = yield self.assertFailure(
1430
 
                        unit_state.enable_hook_debug(None),
1431
 
                        AssertionError)
1432
 
                self.assertEquals(str(error), "Hook names must be a list: got None")
1433
 
 
1434
 
        @inlineCallbacks
1435
 
        def test_enable_named_debug_hook(self):
1436
 
                """Unit hook debugging can be enabled on for a named hook."""
1437
 
                unit_state = yield self.get_unit_state()
1438
 
                enabled = yield unit_state.enable_hook_debug(
1439
 
                        ["db-relation-changed"])
1440
 
                self.assertIdentical(enabled, True)
1441
 
                content, stat = yield self.client.get(
1442
 
                        "/units/%s/debug" % unit_state.internal_id)
1443
 
                data = yaml.load(content)
1444
 
                self.assertEqual(data, {"debug_hooks": ["db-relation-changed"]})
1445
 
 
1446
 
        @inlineCallbacks
1447
 
        def test_enable_debug_hook_pre_existing(self):
1448
 
                """Attempting to enable debug on a unit state already being debugged
1449
 
                raises an exception.
1450
 
                """
1451
 
                unit_state = yield self.get_unit_state()
1452
 
                yield unit_state.enable_hook_debug(["*"])
1453
 
                error = yield self.assertFailure(unit_state.enable_hook_debug(["*"]),
1454
 
                                                                                 ServiceUnitDebugAlreadyEnabled)
1455
 
                self.assertEquals(
1456
 
                        str(error), "Service unit 'wordpress/0' is already in debug mode.")
1457
 
 
1458
 
        @inlineCallbacks
1459
 
        def test_enable_debug_hook_lifetime(self):
1460
 
                """A debug hook setting is only active for the lifetime of the client
1461
 
                that created it.
1462
 
                """
1463
 
                unit_state = yield self.get_unit_state()
1464
 
                yield unit_state.enable_hook_debug(["*"])
1465
 
                exists = yield self.client.exists(
1466
 
                        "/units/%s/debug" % unit_state.internal_id)
1467
 
                self.assertTrue(exists)
1468
 
                yield self.client.close()
1469
 
                self.client = self.get_zookeeper_client()
1470
 
                yield self.client.connect()
1471
 
                exists = yield self.client.exists(
1472
 
                        "/units/%s/debug" % unit_state.internal_id)
1473
 
                self.assertFalse(exists)
1474
 
 
1475
 
        @inlineCallbacks
1476
 
        def test_watch_debug_hook_once(self):
1477
 
                """A watch can be set to notified of presence and changes."""
1478
 
                unit_state = yield self.get_unit_state()
1479
 
                yield unit_state.enable_hook_debug(["*"])
1480
 
 
1481
 
                results = []
1482
 
 
1483
 
                def callback(value):
1484
 
                        results.append(value)
1485
 
 
1486
 
                yield unit_state.watch_hook_debug(callback, permanent=False)
1487
 
                yield unit_state.clear_hook_debug()
1488
 
                yield unit_state.enable_hook_debug(["*"])
1489
 
                yield self.poke_zk()
1490
 
                self.assertEqual(len(results), 2)
1491
 
                self.assertIdentical(results.pop(0), True)
1492
 
                self.assertIdentical(results.pop().type_name, "deleted")
1493
 
 
1494
 
                self.assertEqual(
1495
 
                        (yield unit_state.get_hook_debug()),
1496
 
                        {"debug_hooks": ["*"]})
1497
 
 
1498
 
        @inlineCallbacks
1499
 
        def test_watch_debug_hook_processes_current_state(self):
1500
 
                """A hook debug watch can be instituted on a permanent basis."""
1501
 
                unit_state = yield self.get_unit_state()
1502
 
 
1503
 
                results = []
1504
 
 
1505
 
                @inlineCallbacks
1506
 
                def callback(value):
1507
 
                        results.append((yield unit_state.get_hook_debug()))
1508
 
 
1509
 
                yield unit_state.watch_hook_debug(callback)
1510
 
                self.assertTrue(results)
1511
 
 
1512
 
        @inlineCallbacks
1513
 
        def test_watch_debug_hook_permanent(self):
1514
 
                """A hook debug watch can be instituted on a permanent basis."""
1515
 
                unit_state = yield self.get_unit_state()
1516
 
 
1517
 
                results = []
1518
 
 
1519
 
                def callback(value):
1520
 
                        results.append(value)
1521
 
 
1522
 
                yield unit_state.watch_hook_debug(callback)
1523
 
                yield unit_state.enable_hook_debug(["*"])
1524
 
                yield unit_state.clear_hook_debug()
1525
 
                yield unit_state.enable_hook_debug(["*"])
1526
 
 
1527
 
                yield self.poke_zk()
1528
 
 
1529
 
                self.assertEqual(len(results), 4)
1530
 
                self.assertIdentical(results.pop(0), False)
1531
 
                self.assertIdentical(results.pop(0).type_name, "created")
1532
 
                self.assertIdentical(results.pop(0).type_name, "deleted")
1533
 
                self.assertIdentical(results.pop(0).type_name, "created")
1534
 
 
1535
 
                self.assertEqual(
1536
 
                        (yield unit_state.get_hook_debug()),
1537
 
                        {"debug_hooks": ["*"]})
1538
 
 
1539
 
        @inlineCallbacks
1540
 
        def test_watch_debug_hook_waits_on_slow_callbacks(self):
1541
 
                """A slow watch callback is still invoked serially."""
1542
 
 
1543
 
                unit_state = yield self.get_unit_state()
1544
 
 
1545
 
                callbacks = [Deferred() for i in range(5)]
1546
 
                results = []
1547
 
                contents = []
1548
 
 
1549
 
                @inlineCallbacks
1550
 
                def watch(value):
1551
 
                        results.append(value)
1552
 
                        yield callbacks[len(results) - 1]
1553
 
                        contents.append((yield unit_state.get_hook_debug()))
1554
 
 
1555
 
                callbacks[0].callback(True)  # Finish the current state processing
1556
 
                yield unit_state.watch_hook_debug(watch)
1557
 
 
1558
 
                # These get collapsed into a single event
1559
 
                yield unit_state.enable_hook_debug(["*"])
1560
 
                yield unit_state.clear_hook_debug()
1561
 
                yield self.poke_zk()
1562
 
 
1563
 
                # Verify the callback hasn't completed
1564
 
                self.assertEqual(len(results), 2)
1565
 
                self.assertEqual(len(contents), 1)
1566
 
 
1567
 
                # Let it finish
1568
 
                callbacks[1].callback(True)
1569
 
                yield self.poke_zk()
1570
 
 
1571
 
                # Verify result counts
1572
 
                self.assertEqual(len(results), 3)
1573
 
                self.assertEqual(len(contents), 2)
1574
 
 
1575
 
                # Verify result values. Even though we have created event, the
1576
 
                # setting retrieved shows the hook is not enabled.
1577
 
                self.assertEqual(results[-1].type_name, "deleted")
1578
 
                self.assertEqual(contents[-1], None)
1579
 
 
1580
 
                yield unit_state.enable_hook_debug(["*"])
1581
 
                yield self.poke_zk()
1582
 
 
1583
 
                # Verify the callback hasn't completed
1584
 
                self.assertEqual(len(contents), 2)
1585
 
 
1586
 
                callbacks[2].callback(True)
1587
 
                yield self.poke_zk()
1588
 
 
1589
 
                # Verify values.
1590
 
                self.assertEqual(len(contents), 3)
1591
 
                self.assertEqual(results[-1].type_name, "created")
1592
 
                self.assertEqual(contents[-1], {"debug_hooks": ["*"]})
1593
 
 
1594
 
                # Clear out any pending activity.
1595
 
                yield self.poke_zk()
1596
 
 
1597
 
        @inlineCallbacks
1598
 
        def test_service_unit_agent(self):
1599
 
                """A service unit state has an associated unit agent."""
1600
 
                service_state = yield self.service_state_manager.add_service_state(
1601
 
                        "wordpress", self.charm_state, dummy_constraints)
1602
 
                unit_state = yield service_state.add_unit_state()
1603
 
                exists_d, watch_d = unit_state.watch_agent()
1604
 
                exists = yield exists_d
1605
 
                self.assertFalse(exists)
1606
 
                yield unit_state.connect_agent()
1607
 
                event = yield watch_d
1608
 
                self.assertEqual(event.type_name, "created")
1609
 
                self.assertEqual(event.path,
1610
 
                                                 "/units/%s/agent" % unit_state.internal_id)
1611
 
 
1612
 
        @inlineCallbacks
1613
 
        def test_get_charm_id(self):
1614
 
                """A service unit knows its charm id"""
1615
 
                service_state = yield self.service_state_manager.add_service_state(
1616
 
                        "wordpress", self.charm_state, dummy_constraints)
1617
 
                unit_state = yield service_state.add_unit_state()
1618
 
                unit_charm = yield unit_state.get_charm_id()
1619
 
                service_charm = yield service_state.get_charm_id()
1620
 
                self.assertTrue(
1621
 
                        (self.charm_state.id == unit_charm == service_charm))
1622
 
 
1623
 
        @inlineCallbacks
1624
 
        def test_set_charm_id(self):
1625
 
                """A service unit charm can be set and is validated when set."""
1626
 
                service_state = yield self.service_state_manager.add_service_state(
1627
 
                        "wordpress", self.charm_state, dummy_constraints)
1628
 
                unit_state = yield service_state.add_unit_state()
1629
 
                yield self.assertFailure(
1630
 
                        unit_state.set_charm_id("abc"), CharmURLError)
1631
 
                yield self.assertFailure(
1632
 
                        unit_state.set_charm_id("abc:foobar-a"), CharmURLError)
1633
 
                yield self.assertFailure(
1634
 
                        unit_state.set_charm_id(None), CharmURLError)
1635
 
                charm_id = "local:series/name-1"
1636
 
                yield unit_state.set_charm_id(charm_id)
1637
 
                value = yield unit_state.get_charm_id()
1638
 
                self.assertEqual(charm_id, value)
1639
 
 
1640
 
        @inlineCallbacks
1641
 
        def test_add_unit_state_combines_constraints(self):
1642
 
                """Constraints are inherited both from juju defaults and service"""
1643
 
                service_state = yield self.service_state_manager.add_service_state(
1644
 
                        "wordpress", self.charm_state,
1645
 
                        dummy_cs.parse(["arch=arm", "mem=1G"]))
1646
 
                unit_state = yield service_state.add_unit_state()
1647
 
                constraints = yield unit_state.get_constraints()
1648
 
                expected = {
1649
 
                        "arch": "arm", "cpu": 1, "mem": 1024,
1650
 
                        "provider-type": "dummy", "ubuntu-series": "series"}
1651
 
                self.assertEquals(constraints, expected)
1652
 
 
1653
 
        @inlineCallbacks
1654
 
        def test_unassign_unit_from_machine_without_being_assigned(self):
1655
 
                """
1656
 
                When unassigning a machine from a unit, it is possible that
1657
 
                the machine has not been previously assigned, or that it
1658
 
                was assigned but the state changed beneath us.  In either
1659
 
                case, the end state is the intended state, so we simply
1660
 
                move forward without any errors here, to avoid having to
1661
 
                handle the extra complexity of dealing with the concurrency
1662
 
                problems.
1663
 
                """
1664
 
                service_state = yield self.service_state_manager.add_service_state(
1665
 
                        "wordpress", self.charm_state, dummy_constraints)
1666
 
                unit_state = yield service_state.add_unit_state()
1667
 
 
1668
 
                yield unit_state.unassign_from_machine()
1669
 
 
1670
 
                topology = yield self.get_topology()
1671
 
                self.assertEquals(
1672
 
                        topology.get_service_unit_machine(service_state.internal_id,
1673
 
                                                                                          unit_state.internal_id),
1674
 
                        None)
1675
 
 
1676
 
                machine_id = yield unit_state.get_assigned_machine_id()
1677
 
                self.assertEqual(machine_id, None)
1678
 
 
1679
 
        @inlineCallbacks
1680
 
        def test_assign_unit_to_machine_again_fails(self):
1681
 
                """
1682
 
                Trying to assign a machine to an already assigned unit
1683
 
                should fail, unless we're assigning to precisely the same
1684
 
                machine, in which case it's no big deal.
1685
 
                """
1686
 
                service_state = yield self.service_state_manager.add_service_state(
1687
 
                        "wordpress", self.charm_state, dummy_constraints)
1688
 
                unit_state = yield service_state.add_unit_state()
1689
 
                machine_state0 = yield self.machine_state_manager.add_machine_state(
1690
 
                        series_constraints)
1691
 
                machine_state1 = yield self.machine_state_manager.add_machine_state(
1692
 
                        series_constraints)
1693
 
 
1694
 
                yield unit_state.assign_to_machine(machine_state0)
1695
 
 
1696
 
                # Assigning again to the same machine is a NOOP, so nothing
1697
 
                # terrible should happen if we let it go through.
1698
 
                yield unit_state.assign_to_machine(machine_state0)
1699
 
 
1700
 
                try:
1701
 
                        yield unit_state.assign_to_machine(machine_state1)
1702
 
                except ServiceUnitStateMachineAlreadyAssigned, e:
1703
 
                        self.assertEquals(e.unit_name, "wordpress/0")
1704
 
                else:
1705
 
                        self.fail("Error not raised")
1706
 
 
1707
 
                machine_id = yield unit_state.get_assigned_machine_id()
1708
 
                self.assertEqual(machine_id, 0)
1709
 
 
1710
 
        @inlineCallbacks
1711
 
        def test_unassign_unit_from_machine_with_changing_state(self):
1712
 
                service_state = yield self.service_state_manager.add_service_state(
1713
 
                        "wordpress", self.charm_state, dummy_constraints)
1714
 
                unit_state = yield service_state.add_unit_state()
1715
 
 
1716
 
                yield self.remove_service_unit(service_state.internal_id,
1717
 
                                                                           unit_state.internal_id)
1718
 
 
1719
 
                d = unit_state.unassign_from_machine()
1720
 
                yield self.assertFailure(d, StateChanged)
1721
 
 
1722
 
                d = unit_state.get_assigned_machine_id()
1723
 
                yield self.assertFailure(d, StateChanged)
1724
 
 
1725
 
                yield self.remove_service(service_state.internal_id)
1726
 
 
1727
 
                d = unit_state.unassign_from_machine()
1728
 
                yield self.assertFailure(d, StateChanged)
1729
 
 
1730
 
                d = unit_state.get_assigned_machine_id()
1731
 
                yield self.assertFailure(d, StateChanged)
1732
 
 
1733
 
        @inlineCallbacks
1734
 
        def test_assign_unit_to_unused_machine(self):
1735
 
                """Verify that unused machines can be assigned to when their machine
1736
 
                constraints match the service unit's."""
1737
 
                yield self.machine_state_manager.add_machine_state(
1738
 
                        series_constraints)
1739
 
                mysql_service_state = yield self.add_service_from_charm("mysql")
1740
 
                mysql_unit_state = yield mysql_service_state.add_unit_state()
1741
 
                mysql_machine_state = yield \
1742
 
                        self.machine_state_manager.add_machine_state(series_constraints)
1743
 
                yield mysql_unit_state.assign_to_machine(mysql_machine_state)
1744
 
                yield self.service_state_manager.remove_service_state(
1745
 
                        mysql_service_state)
1746
 
                wordpress_service_state = yield self.add_service_from_charm(
1747
 
                        "wordpress")
1748
 
                wordpress_unit_state = yield wordpress_service_state.add_unit_state()
1749
 
                yield wordpress_unit_state.assign_to_unused_machine()
1750
 
                self.assertEqual(
1751
 
                        (yield self.get_topology()).get_machines(),
1752
 
                        ["machine-0000000000", "machine-0000000001"])
1753
 
                yield self.assert_machine_assignments("wordpress", [1])
1754
 
 
1755
 
        @inlineCallbacks
1756
 
        def test_assign_unit_to_unused_machine_bad_constraints(self):
1757
 
                """Verify that unused machines do not get allocated service units with
1758
 
                non-matching constraints."""
1759
 
                yield self.machine_state_manager.add_machine_state(
1760
 
                        series_constraints)
1761
 
                mysql_service_state = yield self.add_service_from_charm("mysql")
1762
 
                mysql_unit_state = yield mysql_service_state.add_unit_state()
1763
 
                mysql_machine_state = yield \
1764
 
                        self.machine_state_manager.add_machine_state(
1765
 
                                series_constraints)
1766
 
                yield mysql_unit_state.assign_to_machine(mysql_machine_state)
1767
 
                yield self.service_state_manager.remove_service_state(
1768
 
                        mysql_service_state)
1769
 
                other_constraints = dummy_cs.parse(["arch=arm"])
1770
 
                wordpress_service_state = yield self.add_service_from_charm(
1771
 
                        "wordpress", constraints=other_constraints.with_series("series"))
1772
 
                wordpress_unit_state = yield wordpress_service_state.add_unit_state()
1773
 
                yield self.assertFailure(
1774
 
                        wordpress_unit_state.assign_to_unused_machine(),
1775
 
                        NoUnusedMachines)
1776
 
                self.assertEqual(
1777
 
                        (yield self.get_topology()).get_machines(),
1778
 
                        ["machine-0000000000", "machine-0000000001"])
1779
 
                yield self.assert_machine_assignments("wordpress", [None])
1780
 
 
1781
 
        @inlineCallbacks
1782
 
        def test_assign_unit_to_unused_machine_with_changing_state_service(self):
1783
 
                """Verify `StateChanged` raised if service is manipulated during reuse.
1784
 
                """
1785
 
                yield self.machine_state_manager.add_machine_state(
1786
 
                        series_constraints)
1787
 
                mysql_service_state = yield self.add_service_from_charm("mysql")
1788
 
                mysql_unit_state = yield mysql_service_state.add_unit_state()
1789
 
                mysql_machine_state = yield self.machine_state_manager.add_machine_state(
1790
 
                        series_constraints)
1791
 
                yield mysql_unit_state.assign_to_machine(mysql_machine_state)
1792
 
                yield self.service_state_manager.remove_service_state(
1793
 
                        mysql_service_state)
1794
 
                wordpress_service_state = yield self.add_service_from_charm(
1795
 
                        "wordpress")
1796
 
                wordpress_unit_state = yield wordpress_service_state.add_unit_state()
1797
 
                yield self.remove_service(wordpress_service_state.internal_id)
1798
 
                yield self.assertFailure(
1799
 
                        wordpress_unit_state.assign_to_unused_machine(), StateChanged)
1800
 
 
1801
 
        @inlineCallbacks
1802
 
        def test_assign_unit_to_unused_machine_with_changing_state_service_unit(self):
1803
 
                "Verify `StateChanged` raised if unit is manipulated during reuse."""
1804
 
                yield self.machine_state_manager.add_machine_state(
1805
 
                        series_constraints)
1806
 
                mysql_service_state = yield self.add_service_from_charm("mysql")
1807
 
                mysql_unit_state = yield mysql_service_state.add_unit_state()
1808
 
                mysql_machine_state = yield self.machine_state_manager.add_machine_state(
1809
 
                        series_constraints)
1810
 
                yield mysql_unit_state.assign_to_machine(mysql_machine_state)
1811
 
                yield self.service_state_manager.remove_service_state(
1812
 
                        mysql_service_state)
1813
 
                wordpress_service_state = yield self.add_service_from_charm(
1814
 
                        "wordpress")
1815
 
                wordpress_unit_state = yield wordpress_service_state.add_unit_state()
1816
 
                yield self.remove_service_unit(
1817
 
                        wordpress_service_state.internal_id,
1818
 
                        wordpress_unit_state.internal_id)
1819
 
                yield self.assertFailure(
1820
 
                        wordpress_unit_state.assign_to_unused_machine(), StateChanged)
1821
 
 
1822
 
        @inlineCallbacks
1823
 
        def test_assign_unit_to_unused_machine_only_machine_zero(self):
1824
 
                """Verify when the only available machine is machine 0"""
1825
 
                yield self.machine_state_manager.add_machine_state(
1826
 
                        series_constraints)
1827
 
                wordpress_service_state = yield self.add_service_from_charm(
1828
 
                        "wordpress")
1829
 
                wordpress_unit_state = yield wordpress_service_state.add_unit_state()
1830
 
                yield self.assertFailure(
1831
 
                        wordpress_unit_state.assign_to_unused_machine(),
1832
 
                        NoUnusedMachines)
1833
 
 
1834
 
        @inlineCallbacks
1835
 
        def test_assign_unit_to_unused_machine_none_available(self):
1836
 
                """Verify when there are no unused machines"""
1837
 
                yield self.machine_state_manager.add_machine_state(
1838
 
                        series_constraints)
1839
 
                mysql_service_state = yield self.add_service_from_charm("mysql")
1840
 
                mysql_unit_state = yield mysql_service_state.add_unit_state()
1841
 
                mysql_machine_state = yield self.machine_state_manager.add_machine_state(
1842
 
                        series_constraints)
1843
 
                yield mysql_unit_state.assign_to_machine(mysql_machine_state)
1844
 
                yield self.assert_machine_assignments("mysql", [1])
1845
 
                wordpress_service_state = yield self.add_service_from_charm(
1846
 
                        "wordpress")
1847
 
                wordpress_unit_state = yield wordpress_service_state.add_unit_state()
1848
 
                yield self.assertFailure(
1849
 
                        wordpress_unit_state.assign_to_unused_machine(),
1850
 
                        NoUnusedMachines)
1851
 
 
1852
 
        @inlineCallbacks
1853
 
        def test_watch_relations_processes_current_state(self):
1854
 
                """
1855
 
                The watch method returns only after processing initial state.
1856
 
 
1857
 
                Note the callback is only invoked if there are changes
1858
 
                requiring processing.
1859
 
                """
1860
 
                service_state = yield self.add_service("wordpress")
1861
 
                yield self.add_relation(
1862
 
                        "rel-type", "global", [service_state, "name", "role"])
1863
 
 
1864
 
                results = []
1865
 
 
1866
 
                def callback(*args):
1867
 
                        results.append(True)
1868
 
 
1869
 
                yield service_state.watch_relation_states(callback)
1870
 
                self.assertTrue(results)
1871
 
 
1872
 
        @inlineCallbacks
1873
 
        def test_watch_relations_when_being_created(self):
1874
 
                """
1875
 
                We can watch relations before we have any.
1876
 
                """
1877
 
                service_state = yield self.add_service("wordpress")
1878
 
 
1879
 
                wait_callback = [Deferred() for i in range(5)]
1880
 
                calls = []
1881
 
 
1882
 
                def watch_relations(old_relations, new_relations):
1883
 
                        calls.append((old_relations, new_relations))
1884
 
                        wait_callback[len(calls) - 1].callback(True)
1885
 
 
1886
 
                # Start watching
1887
 
                service_state.watch_relation_states(watch_relations)
1888
 
 
1889
 
                # Callback is still untouched
1890
 
                self.assertEquals(calls, [])
1891
 
 
1892
 
                # add a service relation and wait for the callback
1893
 
                relation_state = yield self.add_relation(
1894
 
                        "rel-type", "global", [service_state, "name", "role"])
1895
 
                yield wait_callback[1]
1896
 
 
1897
 
                # verify the result
1898
 
                self.assertEquals(len(calls), 2)
1899
 
                old_relations, new_relations = calls[1]
1900
 
                self.assertFalse(old_relations)
1901
 
                self.assertEquals(new_relations[0].relation_role, "role")
1902
 
 
1903
 
                # add a new relation with the service assigned to it.
1904
 
                relation_state2 = yield self.add_relation(
1905
 
                        "rel-type2", "global", [service_state, "app", "server"])
1906
 
                yield wait_callback[2]
1907
 
 
1908
 
                self.assertEquals(len(calls), 3)
1909
 
                old_relations, new_relations = calls[2]
1910
 
                self.assertEquals([r.internal_relation_id for r in old_relations],
1911
 
                                                  [relation_state.internal_id])
1912
 
                self.assertEquals([r.internal_relation_id for r in new_relations],
1913
 
                                                  [relation_state.internal_id,
1914
 
                                                   relation_state2.internal_id])
1915
 
 
1916
 
        @inlineCallbacks
1917
 
        def test_watch_relations_may_defer(self):
1918
 
                """
1919
 
                The watch relations callback may return a deferred so that
1920
 
                it performs some its logic asynchronously. In this case, it must
1921
 
                not be called a second time before its postponed logic is finished
1922
 
                completely.
1923
 
                """
1924
 
                wait_callback = [Deferred() for i in range(5)]
1925
 
                finish_callback = [Deferred() for i in range(5)]
1926
 
 
1927
 
                calls = []
1928
 
 
1929
 
                def watch_relations(old_relations, new_relations):
1930
 
                        calls.append((old_relations, new_relations))
1931
 
                        wait_callback[len(calls) - 1].callback(True)
1932
 
                        return finish_callback[len(calls) - 1]
1933
 
 
1934
 
                service_state = yield self.add_service("s-1")
1935
 
                service_state.watch_relation_states(watch_relations)
1936
 
 
1937
 
                # Shouldn't have any callbacks yet.
1938
 
                self.assertEquals(calls, [])
1939
 
 
1940
 
                # Assign to a relation.
1941
 
                yield self.add_relation("rel-type", "global",
1942
 
                                                                (service_state, "name", "role"))
1943
 
 
1944
 
                # Hold off until callback is started.
1945
 
                yield wait_callback[0]
1946
 
 
1947
 
                # Assign to another relation.
1948
 
                yield self.add_relation("rel-type", "global",
1949
 
                                                  (service_state, "name2", "role"))
1950
 
 
1951
 
                # Give a chance for something bad to happen.
1952
 
                yield self.sleep(0.3)
1953
 
 
1954
 
                # Ensure we still have a single call.
1955
 
                self.assertEquals(len(calls), 1)
1956
 
 
1957
 
                # Allow the first call to be completed, and wait on the
1958
 
                # next one.
1959
 
                finish_callback[0].callback(None)
1960
 
                yield wait_callback[1]
1961
 
                finish_callback[1].callback(None)
1962
 
 
1963
 
                # We should have the second change now.
1964
 
                self.assertEquals(len(calls), 2)
1965
 
 
1966
 
        @inlineCallbacks
1967
 
        def test_get_relation_endpoints_service_name(self):
1968
 
                """Test getting endpoints with descriptor ``<service name>``"""
1969
 
                yield self.add_service_from_charm("wordpress")
1970
 
                self.assertEqual(
1971
 
                        (yield self.service_state_manager.get_relation_endpoints(
1972
 
                        "wordpress")),
1973
 
                        [RelationEndpoint("wordpress", "varnish", "cache", "client"),
1974
 
                        RelationEndpoint("wordpress", "mysql", "db", "client"),
1975
 
                        RelationEndpoint("wordpress", "http", "url", "server"),
1976
 
                        RelationEndpoint("wordpress", "juju-info", "juju-info", "server")])
1977
 
                yield self.add_service_from_charm("riak")
1978
 
                self.assertEqual(
1979
 
                        (yield self.service_state_manager.get_relation_endpoints(
1980
 
                        "riak")),
1981
 
                        [RelationEndpoint("riak", "http", "admin", "server"),
1982
 
                        RelationEndpoint("riak", "http", "endpoint", "server"),
1983
 
                        RelationEndpoint("riak", "riak", "ring", "peer"),
1984
 
                        RelationEndpoint("riak", "juju-info", "juju-info", "server")])
1985
 
 
1986
 
        @inlineCallbacks
1987
 
        def test_get_relation_endpoints_service_name_relation_name(self):
1988
 
                """Test getting endpoints with ``<service name:relation name>``"""
1989
 
                yield self.add_service_from_charm("wordpress")
1990
 
                self.assertEqual(
1991
 
                        (yield self.service_state_manager.get_relation_endpoints(
1992
 
                        "wordpress:url")),
1993
 
                        [RelationEndpoint("wordpress", "http", "url", "server"),
1994
 
                        RelationEndpoint("wordpress", "juju-info", "juju-info", "server")])
1995
 
                self.assertEqual(
1996
 
                        (yield self.service_state_manager.get_relation_endpoints(
1997
 
                        "wordpress:db")),
1998
 
                        [RelationEndpoint("wordpress", "mysql", "db", "client"),
1999
 
                        RelationEndpoint("wordpress", "juju-info", "juju-info", "server")])
2000
 
                self.assertEqual(
2001
 
                        (yield self.service_state_manager.get_relation_endpoints(
2002
 
                        "wordpress:cache")),
2003
 
                        [RelationEndpoint("wordpress", "varnish", "cache", "client"),
2004
 
                        RelationEndpoint("wordpress", "juju-info", "juju-info", "server")])
2005
 
                yield self.add_service_from_charm("riak")
2006
 
                self.assertEqual(
2007
 
                        (yield self.service_state_manager.get_relation_endpoints(
2008
 
                        "riak:ring")),
2009
 
                        [RelationEndpoint("riak", "riak", "ring", "peer"),
2010
 
                        RelationEndpoint("riak", "juju-info", "juju-info", "server")])
2011
 
 
2012
 
        @inlineCallbacks
2013
 
        def test_descriptor_for_services_without_charms(self):
2014
 
                """Test with services that have no corresponding charms defined"""
2015
 
                yield self.add_service("nocharm")
2016
 
                # verify we get the implicit interface
2017
 
                self.assertEqual(
2018
 
                        (yield self.service_state_manager.get_relation_endpoints(
2019
 
                                "nocharm")),
2020
 
                                [RelationEndpoint("nocharm",
2021
 
                                "juju-info", "juju-info", "server")])
2022
 
 
2023
 
                self.assertEqual(
2024
 
                        (yield self.service_state_manager.get_relation_endpoints(
2025
 
                                "nocharm:nonsense")),
2026
 
                                [RelationEndpoint("nocharm",
2027
 
                                "juju-info", "juju-info", "server")])
2028
 
 
2029
 
        @inlineCallbacks
2030
 
        def test_descriptor_for_missing_service(self):
2031
 
                """Test with a service that is not in the topology"""
2032
 
                yield self.assertFailure(
2033
 
                        self.service_state_manager.get_relation_endpoints(
2034
 
                                "notadded"),
2035
 
                        ServiceStateNotFound)
2036
 
 
2037
 
        @inlineCallbacks
2038
 
        def test_bad_descriptors(self):
2039
 
                """Test that the descriptors meet the minimum naming standards"""
2040
 
                yield self.assertFailure(
2041
 
                        self.service_state_manager.get_relation_endpoints("a:b:c"),
2042
 
                        BadDescriptor)
2043
 
                yield self.assertFailure(
2044
 
                        self.service_state_manager.get_relation_endpoints(""),
2045
 
                        BadDescriptor)
2046
 
 
2047
 
        @inlineCallbacks
2048
 
        def test_join_descriptors_service_name(self):
2049
 
                """Test descriptor of the form ``<service name>`"""
2050
 
                yield self.add_service_from_charm("wordpress")
2051
 
                yield self.add_service_from_charm("mysql")
2052
 
                self.assertEqual(
2053
 
                        (yield self.service_state_manager.join_descriptors(
2054
 
                                "wordpress", "mysql")),
2055
 
                        [(RelationEndpoint("wordpress", "mysql", "db", "client"),
2056
 
                          RelationEndpoint("mysql", "mysql", "server", "server"))])
2057
 
                # symmetric - note the pair has rotated
2058
 
                self.assertEqual(
2059
 
                        (yield self.service_state_manager.join_descriptors(
2060
 
                                "mysql", "wordpress")),
2061
 
                        [(RelationEndpoint("mysql", "mysql", "server", "server"),
2062
 
                          RelationEndpoint("wordpress", "mysql", "db", "client"))])
2063
 
                yield self.add_service_from_charm("varnish")
2064
 
                self.assertEqual(
2065
 
                        (yield self.service_state_manager.join_descriptors(
2066
 
                                "wordpress", "varnish")),
2067
 
                        [(RelationEndpoint("wordpress", "varnish", "cache", "client"),
2068
 
                          RelationEndpoint("varnish", "varnish", "webcache", "server"))])
2069
 
 
2070
 
        @inlineCallbacks
2071
 
        def test_join_descriptors_service_name_relation_name(self):
2072
 
                """Test joining descriptors ``<service name:relation name>``"""
2073
 
                yield self.add_service_from_charm("wordpress")
2074
 
                yield self.add_service_from_charm("mysql")
2075
 
                self.assertEqual(
2076
 
                        (yield self.service_state_manager.join_descriptors(
2077
 
                                "wordpress:db", "mysql")),
2078
 
                        [(RelationEndpoint("wordpress", "mysql", "db", "client"),
2079
 
                          RelationEndpoint("mysql", "mysql", "server", "server"))])
2080
 
                self.assertEqual(
2081
 
                        (yield self.service_state_manager.join_descriptors(
2082
 
                                "mysql:server", "wordpress")),
2083
 
                        [(RelationEndpoint("mysql", "mysql", "server", "server"),
2084
 
                          RelationEndpoint("wordpress", "mysql", "db", "client"))])
2085
 
                self.assertEqual(
2086
 
                        (yield self.service_state_manager.join_descriptors(
2087
 
                                "mysql:server", "wordpress:db")),
2088
 
                        [(RelationEndpoint("mysql", "mysql", "server", "server"),
2089
 
                          RelationEndpoint("wordpress", "mysql", "db", "client"))])
2090
 
 
2091
 
                yield self.add_service_from_charm("varnish")
2092
 
                self.assertEqual(
2093
 
                        (yield self.service_state_manager.join_descriptors(
2094
 
                                "wordpress:cache", "varnish")),
2095
 
                        [(RelationEndpoint("wordpress", "varnish", "cache", "client"),
2096
 
                          RelationEndpoint("varnish", "varnish", "webcache", "server"))])
2097
 
                self.assertEqual(
2098
 
                        (yield self.service_state_manager.join_descriptors(
2099
 
                                "wordpress:cache", "varnish:webcache")),
2100
 
                        [(RelationEndpoint("wordpress", "varnish", "cache", "client"),
2101
 
                          RelationEndpoint("varnish", "varnish", "webcache", "server"))])
2102
 
 
2103
 
        @inlineCallbacks
2104
 
        def test_join_peer_descriptors(self):
2105
 
                """Test joining of peer relation descriptors"""
2106
 
                yield self.add_service_from_charm("riak")
2107
 
                self.assertEqual(
2108
 
                        (yield self.service_state_manager.join_descriptors(
2109
 
                                "riak", "riak")),
2110
 
                        [(RelationEndpoint("riak", "riak", "ring", "peer"),
2111
 
                          RelationEndpoint("riak", "riak", "ring", "peer"))])
2112
 
                self.assertEqual(
2113
 
                        (yield self.service_state_manager.join_descriptors(
2114
 
                                "riak:ring", "riak")),
2115
 
                        [(RelationEndpoint("riak", "riak", "ring", "peer"),
2116
 
                          RelationEndpoint("riak", "riak", "ring", "peer"))])
2117
 
                self.assertEqual(
2118
 
                        (yield self.service_state_manager.join_descriptors(
2119
 
                                "riak:ring", "riak:ring")),
2120
 
                        [(RelationEndpoint("riak", "riak", "ring", "peer"),
2121
 
                          RelationEndpoint("riak", "riak", "ring", "peer"))])
2122
 
                self.assertEqual(
2123
 
                        (yield self.service_state_manager.join_descriptors(
2124
 
                                "riak:no-ring", "riak:ring")),
2125
 
                        [])
2126
 
 
2127
 
        @inlineCallbacks
2128
 
        def test_join_descriptors_no_common_relation(self):
2129
 
                """Test joining of descriptors that do not share a relation"""
2130
 
                yield self.add_service_from_charm("mysql")
2131
 
                yield self.add_service_from_charm("riak")
2132
 
                yield self.add_service_from_charm("wordpress")
2133
 
                yield self.add_service_from_charm("varnish")
2134
 
                self.assertEqual(
2135
 
                        (yield self.service_state_manager.join_descriptors(
2136
 
                                "mysql", "riak")),
2137
 
                        [])
2138
 
                self.assertEqual(
2139
 
                        (yield self.service_state_manager.join_descriptors(
2140
 
                                "mysql:server", "riak:ring")),
2141
 
                        [])
2142
 
                self.assertEqual(
2143
 
                        (yield self.service_state_manager.join_descriptors(
2144
 
                                "varnish", "mysql")),
2145
 
                        [])
2146
 
                self.assertEqual(
2147
 
                        (yield self.service_state_manager.join_descriptors(
2148
 
                                "riak:ring", "riak:admin")),
2149
 
                        [])
2150
 
                self.assertEqual(
2151
 
                        (yield self.service_state_manager.join_descriptors(
2152
 
                                "riak", "wordpress")),
2153
 
                        [])
2154
 
 
2155
 
        @inlineCallbacks
2156
 
        def test_join_descriptors_no_service_state(self):
2157
 
                """Test joining of nonexistent services"""
2158
 
                yield self.add_service_from_charm("wordpress")
2159
 
                yield self.assertFailure(
2160
 
                        self.service_state_manager.join_descriptors("wordpress", "nosuch"),
2161
 
                        ServiceStateNotFound)
2162
 
                yield self.assertFailure(
2163
 
                        self.service_state_manager.join_descriptors("notyet", "nosuch"),
2164
 
                        ServiceStateNotFound)
2165
 
 
2166
 
        @inlineCallbacks
2167
 
        def test_watch_services_initial_callback(self):
2168
 
                """Watch service processes initial state before returning.
2169
 
 
2170
 
                Note the callback is only executed if there is some meaningful state
2171
 
                change.
2172
 
                """
2173
 
                results = []
2174
 
 
2175
 
                def callback(*args):
2176
 
                        results.append(True)
2177
 
 
2178
 
                yield self.service_state_manager.watch_service_states(callback)
2179
 
                yield self.add_service("wordpress")
2180
 
                yield self.poke_zk()
2181
 
                self.assertTrue(results)
2182
 
 
2183
 
        @inlineCallbacks
2184
 
        def test_watch_services_when_being_created(self):
2185
 
                """
2186
 
                It should be possible to start watching services even
2187
 
                before they are created.  In this case, the callback will
2188
 
                be made when it's actually introduced.
2189
 
                """
2190
 
                wait_callback = [Deferred() for i in range(10)]
2191
 
 
2192
 
                calls = []
2193
 
 
2194
 
                def watch_services(old_services, new_services):
2195
 
                        calls.append((old_services, new_services))
2196
 
                        wait_callback[len(calls) - 1].callback(True)
2197
 
 
2198
 
                # Start watching.
2199
 
                self.service_state_manager.watch_service_states(watch_services)
2200
 
 
2201
 
                # Callback is still untouched.
2202
 
                self.assertEquals(calls, [])
2203
 
 
2204
 
                # Add a service, and wait for callback.
2205
 
                yield self.add_service("wordpress")
2206
 
                yield wait_callback[0]
2207
 
 
2208
 
                # The first callback must have been fired, and it must have None
2209
 
                # as the first argument because that's the first service seen.
2210
 
                self.assertEquals(len(calls), 1)
2211
 
                old_services, new_services = calls[0]
2212
 
                self.assertEquals(old_services, set())
2213
 
                self.assertEquals(new_services, set(["wordpress"]))
2214
 
 
2215
 
                # Add a service again.
2216
 
                yield self.add_service("mysql")
2217
 
                yield wait_callback[1]
2218
 
 
2219
 
                # Now the watch callback must have been fired with two
2220
 
                # different service sets.  The old one, and the new one.
2221
 
                self.assertEquals(len(calls), 2)
2222
 
                old_services, new_services = calls[1]
2223
 
                self.assertEquals(old_services, set(["wordpress"]))
2224
 
                self.assertEquals(new_services, set(["mysql", "wordpress"]))
2225
 
 
2226
 
        @inlineCallbacks
2227
 
        def test_watch_services_may_defer(self):
2228
 
                """
2229
 
                The watch services callback may return a deferred so that it
2230
 
                performs some of its logic asynchronously. In this case, it
2231
 
                must not be called a second time before its postponed logic
2232
 
                is finished completely.
2233
 
                """
2234
 
                wait_callback = [Deferred() for i in range(10)]
2235
 
                finish_callback = [Deferred() for i in range(10)]
2236
 
 
2237
 
                calls = []
2238
 
 
2239
 
                def watch_services(old_services, new_services):
2240
 
                        calls.append((old_services, new_services))
2241
 
                        wait_callback[len(calls) - 1].callback(True)
2242
 
                        return finish_callback[len(calls) - 1]
2243
 
 
2244
 
                # Start watching.
2245
 
                self.service_state_manager.watch_service_states(watch_services)
2246
 
 
2247
 
                # Create the service.
2248
 
                yield self.add_service("wordpress")
2249
 
 
2250
 
                # Hold off until callback is started.
2251
 
                yield wait_callback[0]
2252
 
 
2253
 
                # Add another service.
2254
 
                yield self.add_service("mysql")
2255
 
 
2256
 
                # Ensure we still have a single call.
2257
 
                self.assertEquals(len(calls), 1)
2258
 
 
2259
 
                # Allow the first call to be completed, and wait on the
2260
 
                # next one.
2261
 
                finish_callback[0].callback(None)
2262
 
                yield wait_callback[1]
2263
 
                finish_callback[1].callback(None)
2264
 
 
2265
 
                # We should have the second change now.
2266
 
                self.assertEquals(len(calls), 2)
2267
 
                old_services, new_services = calls[1]
2268
 
                self.assertEquals(old_services, set(["wordpress"]))
2269
 
                self.assertEquals(new_services, set(["mysql", "wordpress"]))
2270
 
 
2271
 
        @inlineCallbacks
2272
 
        def test_watch_services_with_changing_topology(self):
2273
 
                """
2274
 
                If the topology changes in an unrelated way, the services
2275
 
                watch callback should not be called with two equal
2276
 
                arguments.
2277
 
                """
2278
 
                wait_callback = [Deferred() for i in range(10)]
2279
 
 
2280
 
                calls = []
2281
 
 
2282
 
                def watch_services(old_services, new_services):
2283
 
                        calls.append((old_services, new_services))
2284
 
                        wait_callback[len(calls) - 1].callback(True)
2285
 
 
2286
 
                # Start watching.
2287
 
                self.service_state_manager.watch_service_states(watch_services)
2288
 
 
2289
 
                # Callback is still untouched.
2290
 
                self.assertEquals(calls, [])
2291
 
 
2292
 
                # Add a service, and wait for callback.
2293
 
                yield self.add_service("wordpress")
2294
 
                yield wait_callback[0]
2295
 
 
2296
 
                # Now change the topology in an unrelated way.
2297
 
                yield self.machine_state_manager.add_machine_state(
2298
 
                        series_constraints)
2299
 
 
2300
 
                # Add a service again.
2301
 
                yield self.add_service("mysql")
2302
 
                yield wait_callback[1]
2303
 
 
2304
 
                # But it *shouldn't* have happened.
2305
 
                self.assertEquals(len(calls), 2)
2306
 
 
2307
 
        @inlineCallbacks
2308
 
        def test_watch_service_units_initial_callback(self):
2309
 
                """Watch service unit processes initial state before returning.
2310
 
 
2311
 
                Note the callback is only executed if there is some meaningful state
2312
 
                change.
2313
 
                """
2314
 
                results = []
2315
 
 
2316
 
                def callback(*args):
2317
 
                        results.append(True)
2318
 
 
2319
 
                service_state = yield self.add_service("wordpress")
2320
 
                yield service_state.watch_service_unit_states(callback)
2321
 
                yield service_state.add_unit_state()
2322
 
                yield self.poke_zk()
2323
 
                self.assertTrue(results)
2324
 
 
2325
 
        @inlineCallbacks
2326
 
        def test_watch_service_units_when_being_created(self):
2327
 
                """
2328
 
                It should be possible to start watching service units even
2329
 
                before they are created.  In this case, the callback will be
2330
 
                made when it's actually introduced.
2331
 
                """
2332
 
                wait_callback = [Deferred() for i in range(10)]
2333
 
 
2334
 
                calls = []
2335
 
 
2336
 
                def watch_service_units(old_service_units, new_service_units):
2337
 
                        calls.append((old_service_units, new_service_units))
2338
 
                        wait_callback[len(calls) - 1].callback(True)
2339
 
 
2340
 
                # Start watching.
2341
 
                service_state = yield self.add_service("wordpress")
2342
 
                service_state.watch_service_unit_states(watch_service_units)
2343
 
 
2344
 
                # Callback is still untouched.
2345
 
                self.assertEquals(calls, [])
2346
 
 
2347
 
                # Add a service unit, and wait for callback.
2348
 
                yield service_state.add_unit_state()
2349
 
 
2350
 
                yield wait_callback[0]
2351
 
 
2352
 
                # The first callback must have been fired, and it must have None
2353
 
                # as the first argument because that's the first service seen.
2354
 
                self.assertEquals(len(calls), 1)
2355
 
                old_service_units, new_service_units = calls[0]
2356
 
                self.assertEquals(old_service_units, set())
2357
 
                self.assertEquals(new_service_units, set(["wordpress/0"]))
2358
 
 
2359
 
                # Add another service unit.
2360
 
                yield service_state.add_unit_state()
2361
 
                yield wait_callback[1]
2362
 
 
2363
 
                # Now the watch callback must have been fired with two
2364
 
                # different service sets.  The old one, and the new one.
2365
 
                self.assertEquals(len(calls), 2)
2366
 
                old_service_units, new_service_units = calls[1]
2367
 
                self.assertEquals(old_service_units, set(["wordpress/0"]))
2368
 
                self.assertEquals(new_service_units, set(["wordpress/0", "wordpress/1"]))
2369
 
 
2370
 
        @inlineCallbacks
2371
 
        def test_watch_service_units_may_defer(self):
2372
 
                """
2373
 
                The watch service units callback may return a deferred so that
2374
 
                it performs some of its logic asynchronously.  In this case,
2375
 
                it must not be called a second time before its postponed logic
2376
 
                is finished completely.
2377
 
                """
2378
 
                wait_callback = [Deferred() for i in range(10)]
2379
 
                finish_callback = [Deferred() for i in range(10)]
2380
 
 
2381
 
                calls = []
2382
 
 
2383
 
                def watch_service_units(old_service_units, new_service_units):
2384
 
                        calls.append((old_service_units, new_service_units))
2385
 
                        wait_callback[len(calls) - 1].callback(True)
2386
 
                        return finish_callback[len(calls) - 1]
2387
 
 
2388
 
                # Start watching.
2389
 
                service_state = yield self.add_service("wordpress")
2390
 
                service_state.watch_service_unit_states(watch_service_units)
2391
 
 
2392
 
                # Create the service unit.
2393
 
                yield service_state.add_unit_state()
2394
 
 
2395
 
                # Hold off until callback is started.
2396
 
                yield wait_callback[0]
2397
 
 
2398
 
                # Add another service unit.
2399
 
                yield service_state.add_unit_state()
2400
 
 
2401
 
                # Ensure we still have a single call.
2402
 
                self.assertEquals(len(calls), 1)
2403
 
 
2404
 
                # Allow the first call to be completed, and wait on the
2405
 
                # next one.
2406
 
                finish_callback[0].callback(None)
2407
 
                yield wait_callback[1]
2408
 
                finish_callback[1].callback(None)
2409
 
 
2410
 
                # We should have the second change now.
2411
 
                self.assertEquals(len(calls), 2)
2412
 
                old_service_units, new_service_units = calls[1]
2413
 
                self.assertEquals(old_service_units, set(["wordpress/0"]))
2414
 
                self.assertEquals(
2415
 
                        new_service_units, set(["wordpress/0", "wordpress/1"]))
2416
 
 
2417
 
        @inlineCallbacks
2418
 
        def test_watch_service_units_with_changing_topology(self):
2419
 
                """
2420
 
                If the topology changes in an unrelated way, the services
2421
 
                watch callback should not be called with two equal
2422
 
                arguments.
2423
 
                """
2424
 
                wait_callback = [Deferred() for i in range(10)]
2425
 
 
2426
 
                calls = []
2427
 
 
2428
 
                def watch_service_units(old_service_units, new_service_units):
2429
 
                        calls.append((old_service_units, new_service_units))
2430
 
                        wait_callback[len(calls) - 1].callback(True)
2431
 
 
2432
 
                # Start watching.
2433
 
                service_state = yield self.add_service("wordpress")
2434
 
                service_state.watch_service_unit_states(watch_service_units)
2435
 
 
2436
 
                # Callback is still untouched.
2437
 
                self.assertEquals(calls, [])
2438
 
 
2439
 
                # Add a service, and wait for callback.
2440
 
                yield service_state.add_unit_state()
2441
 
                yield wait_callback[0]
2442
 
 
2443
 
                # Now change the topology in an unrelated way.
2444
 
                yield self.machine_state_manager.add_machine_state(
2445
 
                        series_constraints)
2446
 
 
2447
 
                # Add a service again.
2448
 
                yield service_state.add_unit_state()
2449
 
                yield wait_callback[1]
2450
 
 
2451
 
                # But it *shouldn't* have happened.
2452
 
                self.assertEquals(len(calls), 2)
2453
 
 
2454
 
        @inlineCallbacks
2455
 
        def test_service_config_get_set(self):
2456
 
                """Validate that we can set and get service config options."""
2457
 
                wordpress = yield self.add_service_from_charm("wordpress")
2458
 
 
2459
 
                # attempt to get the initialized service state
2460
 
                config = yield wordpress.get_config()
2461
 
 
2462
 
                # the initial state is empty
2463
 
                self.assertEqual(config, {"blog-title": "My Title"})
2464
 
 
2465
 
                # behaves as a normal dict
2466
 
                self.assertRaises(KeyError, config.__getitem__, "missing")
2467
 
 
2468
 
                # various ways to set state
2469
 
                config.update(dict(alpha="beta", one="two"))
2470
 
                config["another"] = "value"
2471
 
 
2472
 
                # write the values
2473
 
                yield config.write()
2474
 
 
2475
 
                # we should be able to read the config and see the same values
2476
 
                # (in this case it would be the cached object)
2477
 
                config2 = yield wordpress.get_config()
2478
 
                self.assertEqual(config2, {"alpha": "beta",
2479
 
                                                                   "one": "two",
2480
 
                                                                   "another": "value",
2481
 
                                                                   "blog-title": "My Title"})
2482
 
 
2483
 
                # now set a non-string value and recover it
2484
 
                config2["number"] = 1
2485
 
                config2["one"] = None
2486
 
                yield config2.write()
2487
 
 
2488
 
                yield config.read()
2489
 
                self.assertEquals(config["number"], 1)
2490
 
                self.assertEquals(config["one"], None)
2491
 
 
2492
 
        @inlineCallbacks
2493
 
        def test_service_config_get_returns_new(self):
2494
 
                """Validate that we can set and get service config options."""
2495
 
                wordpress = yield self.add_service_from_charm("wordpress")
2496
 
 
2497
 
                # attempt to get the initialized service state
2498
 
                config = yield wordpress.get_config()
2499
 
                config.update({"foo": "bar"})
2500
 
                # Defaults come through
2501
 
                self.assertEqual(config, {"foo": "bar", "blog-title": "My Title"})
2502
 
 
2503
 
                config2 = yield wordpress.get_config()
2504
 
                self.assertEqual(config2, {"blog-title": "My Title"})
2505
 
 
2506
 
                yield config.write()
2507
 
                self.assertEqual(config, {"foo": "bar", "blog-title": "My Title"})
2508
 
 
2509
 
                # Config2 is still empty (a different YAML State), with charm defaults.
2510
 
                self.assertEqual(config2, {"blog-title": "My Title"})
2511
 
 
2512
 
                yield config2.read()
2513
 
                self.assertEqual(config, {"foo": "bar", "blog-title": "My Title"})
2514
 
 
2515
 
                # The default was never written to storage.
2516
 
                data, stat = yield self.client.get(
2517
 
                        "/services/%s/config" % wordpress.internal_id)
2518
 
                self.assertEqual(yaml.load(data), {"foo": "bar"})
2519
 
 
2520
 
        @inlineCallbacks
2521
 
        def test_get_charm_state(self):
2522
 
                wordpress = yield self.add_service_from_charm("wordpress")
2523
 
                charm = yield wordpress.get_charm_state()
2524
 
 
2525
 
                self.assertEqual(charm.name, "wordpress")
2526
 
                metadata = yield charm.get_metadata()
2527
 
                self.assertEqual(metadata.summary, "Blog engine")
 
165
    @inlineCallbacks
 
166
    def test_add_service(self):
 
167
        """
 
168
        Adding a service state should register it in zookeeper,
 
169
        including the requested charm id.
 
170
        """
 
171
        yield self.service_state_manager.add_service_state(
 
172
                "wordpress", self.charm_state, dummy_constraints)
 
173
        yield self.service_state_manager.add_service_state(
 
174
                "mysql", self.charm_state, dummy_constraints)
 
175
        children = yield self.client.get_children("/services")
 
176
 
 
177
        self.assertEquals(sorted(children), [
 
178
            "service-0000000000", "service-0000000001"])
 
179
 
 
180
        content, stat = yield self.client.get("/services/service-0000000000")
 
181
        details = yaml.load(content)
 
182
        self.assertTrue(details)
 
183
        self.assertEquals(details.get("charm"), "local:series/dummy-1")
 
184
        self.assertFalse(isinstance(details.get("charm"), unicode))
 
185
        self.assertEquals(details.get("constraints"), series_constraints.data)
 
186
 
 
187
        topology = yield self.get_topology()
 
188
        self.assertEquals(topology.find_service_with_name("wordpress"),
 
189
                                          "service-0000000000")
 
190
        self.assertEquals(topology.find_service_with_name("mysql"),
 
191
                                          "service-0000000001")
 
192
 
 
193
    @inlineCallbacks
 
194
    def test_add_service_with_duplicated_name(self):
 
195
        """
 
196
        If a service is added with a duplicated name, a meaningful
 
197
        error should be raised.
 
198
        """
 
199
        yield self.service_state_manager.add_service_state(
 
200
                "wordpress", self.charm_state, dummy_constraints)
 
201
 
 
202
        try:
 
203
            yield self.service_state_manager.add_service_state(
 
204
                    "wordpress", self.charm_state, dummy_constraints)
 
205
        except ServiceStateNameInUse, e:
 
206
            self.assertEquals(e.service_name, "wordpress")
 
207
        else:
 
208
            self.fail("Error not raised")
 
209
 
 
210
    @inlineCallbacks
 
211
    def test_get_service_and_check_attributes(self):
 
212
        """
 
213
        Getting a service state should be possible, and the service
 
214
        state identification should be available through its
 
215
        attributes.
 
216
        """
 
217
        yield self.service_state_manager.add_service_state(
 
218
                "wordpress", self.charm_state, dummy_constraints)
 
219
        service_state = yield self.service_state_manager.get_service_state(
 
220
                "wordpress")
 
221
        self.assertEquals(service_state.service_name, "wordpress")
 
222
        self.assertEquals(service_state.internal_id, "service-0000000000")
 
223
 
 
224
    @inlineCallbacks
 
225
    def test_get_service_not_found(self):
 
226
        """
 
227
        Getting a service state which is not available should errback
 
228
        a meaningful error.
 
229
        """
 
230
        try:
 
231
            yield self.service_state_manager.get_service_state("wordpress")
 
232
        except ServiceStateNotFound, e:
 
233
            self.assertEquals(e.service_name, "wordpress")
 
234
        else:
 
235
            self.fail("Error not raised")
 
236
 
 
237
    def test_get_unit_state(self):
 
238
        """A unit state can be retrieved by name from the service manager."""
 
239
        self.assertFailure(self.service_state_manager.get_unit_state(
 
240
                "wordpress/1"), ServiceStateNotFound)
 
241
 
 
242
        self.assertFailure(self.service_state_manager.get_unit_state(
 
243
                "wordpress1"), ServiceUnitStateNotFound)
 
244
 
 
245
        wordpress_state = yield self.service_state_manager.add_service_state(
 
246
                "wordpress", self.charm_state, dummy_constraints)
 
247
 
 
248
        self.assertFailure(self.service_state_manager.get_unit_state(
 
249
                "wordpress/1"), ServiceUnitStateNotFound)
 
250
 
 
251
        wordpress_unit = wordpress_state.add_unit_state()
 
252
 
 
253
        unit_state = yield self.service_state_manager.get_unit_state(
 
254
                        "wordpress/1")
 
255
 
 
256
        self.assertEqual(unit_state.internal_id, wordpress_unit.internal_id)
 
257
 
 
258
    @inlineCallbacks
 
259
    def test_get_service_charm_id(self):
 
260
        """
 
261
        The service state should make its respective charm id available.
 
262
        """
 
263
        yield self.service_state_manager.add_service_state(
 
264
                "wordpress", self.charm_state, dummy_constraints)
 
265
        service_state = yield self.service_state_manager.get_service_state(
 
266
                "wordpress")
 
267
        charm_id = yield service_state.get_charm_id()
 
268
        self.assertEquals(charm_id, "local:series/dummy-1")
 
269
 
 
270
    @inlineCallbacks
 
271
    def test_set_service_charm_id(self):
 
272
        """
 
273
        The service state should allow its charm id to be set.
 
274
        """
 
275
        yield self.service_state_manager.add_service_state(
 
276
                "wordpress", self.charm_state, dummy_constraints)
 
277
        service_state = yield self.service_state_manager.get_service_state(
 
278
                "wordpress")
 
279
        yield service_state.set_charm_id("local:series/dummy-2")
 
280
        charm_id = yield service_state.get_charm_id()
 
281
        self.assertEquals(charm_id, "local:series/dummy-2")
 
282
 
 
283
    @inlineCallbacks
 
284
    def test_get_service_constraints(self):
 
285
        """The service state should make constraints available"""
 
286
        initial_constraints = dummy_cs.parse(["cpu=256", "arch=amd64"])
 
287
        yield self.service_state_manager.add_service_state(
 
288
                "wordpress", self.charm_state, initial_constraints)
 
289
        service_state = yield self.service_state_manager.get_service_state(
 
290
                "wordpress")
 
291
        constraints = yield service_state.get_constraints()
 
292
        self.assertEquals(
 
293
                constraints, initial_constraints.with_series("series"))
 
294
 
 
295
    @inlineCallbacks
 
296
    def test_get_service_constraints_inherits(self):
 
297
        """The service constraints should be combined with the environment's"""
 
298
        yield self.push_env_constraints("arch=arm", "cpu=32")
 
299
        service_constraints = dummy_cs.parse(["cpu=256"])
 
300
        yield self.service_state_manager.add_service_state(
 
301
                "wordpress", self.charm_state, service_constraints)
 
302
        service_state = yield self.service_state_manager.get_service_state(
 
303
                "wordpress")
 
304
        constraints = yield service_state.get_constraints()
 
305
        expected_base = dummy_cs.parse(["arch=arm", "cpu=256"])
 
306
        self.assertEquals(constraints, expected_base.with_series("series"))
 
307
 
 
308
    @inlineCallbacks
 
309
    def test_get_missing_service_constraints(self):
 
310
        """
 
311
        Nodes created before the constraints mechanism was added should have
 
312
        empty constraints.
 
313
        """
 
314
        yield self.client.delete("/constraints")
 
315
        yield self.service_state_manager.add_service_state(
 
316
                "wordpress", self.charm_state, dummy_constraints)
 
317
        service_state = yield self.service_state_manager.get_service_state(
 
318
                "wordpress")
 
319
        path = "/services/" + service_state.internal_id
 
320
        node = YAMLState(self.client, path)
 
321
        yield node.read()
 
322
        del node["constraints"]
 
323
        yield node.write()
 
324
        constraints = yield service_state.get_constraints()
 
325
        self.assertEquals(constraints.data, {})
 
326
 
 
327
    @inlineCallbacks
 
328
    def test_get_missing_unit_constraints(self):
 
329
        """
 
330
        Nodes created before the constraints mechanism was added should have
 
331
        empty constraints.
 
332
        """
 
333
        yield self.service_state_manager.add_service_state(
 
334
                "wordpress", self.charm_state, dummy_constraints)
 
335
        service_state = yield self.service_state_manager.get_service_state(
 
336
                "wordpress")
 
337
        unit_state = yield service_state.add_unit_state()
 
338
        path = "/units/" + unit_state.internal_id
 
339
        node = YAMLState(self.client, path)
 
340
        yield node.read()
 
341
        del node["constraints"]
 
342
        yield node.write()
 
343
        constraints = yield unit_state.get_constraints()
 
344
        self.assertEquals(constraints.data, {})
 
345
 
 
346
    @inlineCallbacks
 
347
    def test_set_service_constraints(self):
 
348
        """The service state should make constraints available for change"""
 
349
        initial_constraints = dummy_cs.parse(["cpu=256", "arch=amd64"])
 
350
        yield self.service_state_manager.add_service_state(
 
351
                "wordpress", self.charm_state, initial_constraints)
 
352
        service_state = yield self.service_state_manager.get_service_state(
 
353
                "wordpress")
 
354
        new_constraints = dummy_cs.parse(["mem=2G", "arch=arm"])
 
355
        yield service_state.set_constraints(new_constraints)
 
356
        retrieved_constraints = yield service_state.get_constraints()
 
357
        self.assertEquals(
 
358
                retrieved_constraints, new_constraints.with_series("series"))
 
359
 
 
360
    @inlineCallbacks
 
361
    def test_remove_service_state(self):
 
362
        """
 
363
        A service state can be removed along with its relations, units,
 
364
        and zookeeper state.
 
365
        """
 
366
        service_state = yield self.service_state_manager.add_service_state(
 
367
                "wordpress", self.charm_state, dummy_constraints)
 
368
 
 
369
        relation_state = yield self.add_relation(
 
370
                "rel-type2", "global", [service_state, "app", "server"])
 
371
 
 
372
        unit_state = yield service_state.add_unit_state()
 
373
        machine_state = yield self.machine_state_manager.add_machine_state(
 
374
                series_constraints)
 
375
        yield unit_state.assign_to_machine(machine_state)
 
376
 
 
377
        yield self.service_state_manager.remove_service_state(service_state)
 
378
 
 
379
        topology = yield self.get_topology()
 
380
        self.assertFalse(topology.has_relation(relation_state.internal_id))
 
381
        self.assertFalse(topology.has_service(service_state.internal_id))
 
382
        self.assertFalse(topology.get_service_units_in_machine(
 
383
            machine_state.internal_id))
 
384
 
 
385
        exists = yield self.client.exists(
 
386
                "/services/%s" % service_state.internal_id)
 
387
        self.assertFalse(exists)
 
388
 
 
389
    @inlineCallbacks
 
390
    def test_add_service_unit_and_check_attributes(self):
 
391
        """
 
392
        A service state should enable adding a new service unit
 
393
        under it, and again the unit should offer attributes allowing
 
394
        its identification.
 
395
        """
 
396
        service_state0 = yield self.service_state_manager.add_service_state(
 
397
                "wordpress", self.charm_state, dummy_constraints)
 
398
        service_state1 = yield self.service_state_manager.add_service_state(
 
399
                "mysql", self.charm_state, dummy_constraints)
 
400
 
 
401
        unit_state0 = yield service_state0.add_unit_state()
 
402
        unit_state1 = yield service_state1.add_unit_state()
 
403
        unit_state2 = yield service_state0.add_unit_state()
 
404
        unit_state3 = yield service_state1.add_unit_state()
 
405
 
 
406
        children = yield self.client.get_children("/units")
 
407
        self.assertEquals(sorted(children), [
 
408
            "unit-0000000000", "unit-0000000001",
 
409
            "unit-0000000002", "unit-0000000003"])
 
410
 
 
411
        self.assertEquals(unit_state0.service_name, "wordpress")
 
412
        self.assertEquals(unit_state0.internal_id, "unit-0000000000")
 
413
        self.assertEquals(unit_state0.unit_name, "wordpress/0")
 
414
 
 
415
        self.assertEquals(unit_state1.service_name, "mysql")
 
416
        self.assertEquals(unit_state1.internal_id, "unit-0000000001")
 
417
        self.assertEquals(unit_state1.unit_name, "mysql/0")
 
418
 
 
419
        self.assertEquals(unit_state2.service_name, "wordpress")
 
420
        self.assertEquals(unit_state2.internal_id, "unit-0000000002")
 
421
        self.assertEquals(unit_state2.unit_name, "wordpress/1")
 
422
 
 
423
        self.assertEquals(unit_state3.service_name, "mysql")
 
424
        self.assertEquals(unit_state3.internal_id, "unit-0000000003")
 
425
        self.assertEquals(unit_state3.unit_name, "mysql/1")
 
426
 
 
427
        topology = yield self.get_topology()
 
428
 
 
429
        self.assertTrue(
 
430
            topology.has_service_unit("service-0000000000",
 
431
                                      "unit-0000000000"))
 
432
        self.assertTrue(
 
433
            topology.has_service_unit("service-0000000001",
 
434
                                      "unit-0000000001"))
 
435
        self.assertTrue(
 
436
            topology.has_service_unit("service-0000000000",
 
437
                                      "unit-0000000002"))
 
438
        self.assertTrue(
 
439
            topology.has_service_unit("service-0000000001",
 
440
                                      "unit-0000000003"))
 
441
 
 
442
    def get_presence_path(
 
443
            self, relation_state, relation_role, unit_state, container=None):
 
444
        container = container.internal_id if container else None
 
445
        presence_path = "/".join(filter(None, [
 
446
                "/relations",
 
447
                relation_state.internal_id,
 
448
                container,
 
449
                relation_role,
 
450
                unit_state.internal_id]))
 
451
        return presence_path
 
452
 
 
453
    @inlineCallbacks
 
454
    def test_add_service_unit_with_container(self):
 
455
        """
 
456
        Validate adding units with containers specified and recovering that.
 
457
        """
 
458
        mysql_ep = RelationEndpoint("mysql", "juju-info", "juju-info",
 
459
                                    "server", "global")
 
460
        logging_ep = RelationEndpoint("logging", "juju-info", "juju-info",
 
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)
 
479
 
 
480
        self.assertEquals((yield unit_state1.get_container()), None)
 
481
        self.assertEquals((yield unit_state0.get_container()), unit_state1)
 
482
        self.assertEquals((yield unit_state2.get_container()), unit_state3)
 
483
        self.assertEquals((yield unit_state3.get_container()), None)
 
484
 
 
485
        for unit_state in (unit_state1, unit_state3):
 
486
            yield unit_state.set_private_address(
 
487
                    "%s.example.com" % (
 
488
                            unit_state.unit_name.replace("/", "-")))
 
489
 
 
490
        # construct the proper relation state
 
491
        mystate = pick_attr(service_states, relation_role="server")
 
492
        logstate = pick_attr(service_states, relation_role="client")
 
493
        yield logstate.add_unit_state(unit_state0)
 
494
        yield logstate.add_unit_state(unit_state2)
 
495
 
 
496
        yield mystate.add_unit_state(unit_state1)
 
497
        yield mystate.add_unit_state(unit_state3)
 
498
 
 
499
        @inlineCallbacks
 
500
        def verify_container(relation_state, service_relation_state,
 
501
                                                 unit_state, container):
 
502
            presence_path = self.get_presence_path(
 
503
                    relation_state,
 
504
                    service_relation_state.relation_role,
 
505
                    unit_state,
 
506
                    container)
 
507
 
 
508
            content, stat = yield self.client.get(presence_path)
 
509
            self.assertTrue(stat)
 
510
            self.assertEqual(content, '')
 
511
            # verify the node data on the relation role nodes
 
512
            role_path = os.path.dirname(presence_path)
 
513
            # role path
 
514
            content, stat = yield self.client.get(role_path)
 
515
            self.assertTrue(stat)
 
516
            node_info = yaml.load(content)
 
517
 
 
518
            self.assertEqual(
 
519
                    node_info["name"],
 
520
                    service_relation_state.relation_name)
 
521
            self.assertEqual(
 
522
                    node_info["role"],
 
523
                    service_relation_state.relation_role)
 
524
 
 
525
            settings_path = os.path.dirname(
 
526
                    os.path.dirname(presence_path)) + "/settings/" + \
 
527
                            unit_state.internal_id
 
528
            content, stat = yield self.client.get(settings_path)
 
529
            self.assertTrue(stat)
 
530
            settings_info = yaml.load(content)
 
531
 
 
532
            # Verify that private address was set
 
533
            # we verify the content elsewhere
 
534
            self.assertTrue(settings_info["private-address"])
 
535
 
 
536
        # verify all the units are constructed as expected
 
537
        # first the client roles with another container
 
538
        yield verify_container(relation_state, logstate,
 
539
                               unit_state0, unit_state1)
 
540
 
 
541
        yield verify_container(relation_state, logstate,
 
542
                               unit_state2, unit_state3)
 
543
 
 
544
        # and now the principals (which are their own relation containers)
 
545
        yield verify_container(relation_state, logstate,
 
546
                               unit_state0, unit_state1)
 
547
 
 
548
        yield verify_container(relation_state, mystate,
 
549
                               unit_state1, unit_state1)
 
550
 
 
551
    @inlineCallbacks
 
552
    def test_get_container_no_principal(self):
 
553
        """Get container should handle no principal."""
 
554
        mysql_ep = RelationEndpoint("mysql", "juju-info", "juju-info",
 
555
                                    "server", "global")
 
556
        logging_ep = RelationEndpoint("logging", "juju-info", "juju-info",
 
557
                                      "client", "container")
 
558
 
 
559
        logging_charm_state = yield self.get_subordinate_charm()
 
560
        self.assertTrue(logging_charm_state.is_subordinate())
 
561
        log_state = yield self.service_state_manager.add_service_state(
 
562
                "logging", logging_charm_state, dummy_constraints)
 
563
        mysql_state = yield self.service_state_manager.add_service_state(
 
564
                "mysql", self.charm_state, dummy_constraints)
 
565
 
 
566
        relation_state, service_states = (yield
 
567
                self.relation_state_manager.add_relation_state(
 
568
                        mysql_ep, logging_ep))
 
569
 
 
570
        unit_state1 = yield mysql_state.add_unit_state()
 
571
        unit_state0 = yield log_state.add_unit_state(container=unit_state1)
 
572
 
 
573
        unit_state3 = yield mysql_state.add_unit_state()
 
574
        unit_state2 = yield log_state.add_unit_state(container=unit_state3)
 
575
 
 
576
        self.assertEquals((yield unit_state1.get_container()), None)
 
577
        self.assertEquals((yield unit_state0.get_container()), unit_state1)
 
578
        self.assertEquals((yield unit_state2.get_container()), unit_state3)
 
579
        self.assertEquals((yield unit_state3.get_container()), None)
 
580
 
 
581
        # now remove a principal node and test again
 
582
 
 
583
        yield mysql_state.remove_unit_state(unit_state1)
 
584
        container = yield unit_state0.get_container()
 
585
        self.assertEquals(container, None)
 
586
 
 
587
        # the other pair is still fine
 
588
        self.assertEquals((yield unit_state2.get_container()), unit_state3)
 
589
        self.assertEquals((yield unit_state3.get_container()), None)
 
590
 
 
591
 
 
592
    @inlineCallbacks
 
593
    def test_add_service_unit_with_changing_state(self):
 
594
        """
 
595
        When adding a service unit, there's a chance that the
 
596
        service will go away mid-way through.  Rather than blowing
 
597
        up randomly, a nice error should be raised.
 
598
        """
 
599
        service_state = yield self.service_state_manager.add_service_state(
 
600
                "wordpress", self.charm_state, dummy_constraints)
 
601
 
 
602
        yield self.remove_service(service_state.internal_id)
 
603
 
 
604
        d = service_state.add_unit_state()
 
605
        yield self.assertFailure(d, StateChanged)
 
606
 
 
607
    @inlineCallbacks
 
608
    def test_get_unit_names(self):
 
609
        """A service's units names are retrievable."""
 
610
        service_state = yield self.service_state_manager.add_service_state(
 
611
                "wordpress", self.charm_state, dummy_constraints)
 
612
 
 
613
        expected_names = []
 
614
        for i in range(3):
 
615
            unit_state = yield service_state.add_unit_state()
 
616
            expected_names.append(unit_state.unit_name)
 
617
 
 
618
        unit_names = yield service_state.get_unit_names()
 
619
        self.assertEqual(unit_names, expected_names)
 
620
 
 
621
    @inlineCallbacks
 
622
    def test_remove_service_unit(self):
 
623
        """Removing a service unit removes all state associated.
 
624
        """
 
625
        service_state = yield self.service_state_manager.add_service_state(
 
626
                "wordpress", self.charm_state, dummy_constraints)
 
627
 
 
628
        unit_state = yield service_state.add_unit_state()
 
629
 
 
630
        # Assign to a machine
 
631
        machine_state = yield self.machine_state_manager.add_machine_state(
 
632
                series_constraints)
 
633
        yield unit_state.assign_to_machine(machine_state)
 
634
        # Connect a unit agent
 
635
        yield unit_state.connect_agent()
 
636
 
 
637
        # Now try and destroy it.
 
638
        yield service_state.remove_unit_state(unit_state)
 
639
 
 
640
        # Verify destruction.
 
641
        topology = yield self.get_topology()
 
642
        self.assertTrue(topology.has_service(service_state.internal_id))
 
643
        self.assertFalse(topology.has_service_unit(
 
644
            service_state.internal_id, unit_state.internal_id))
 
645
 
 
646
        exists = yield self.client.exists("/units/%s" % unit_state.internal_id)
 
647
        self.assertFalse(exists)
 
648
 
 
649
    def test_remove_service_unit_nonexistant(self):
 
650
        """Removing a non existant service unit, is fine."""
 
651
 
 
652
        service_state = yield self.service_state_manager.add_service_state(
 
653
                "wordpress", self.charm_state, dummy_constraints)
 
654
        unit_state = yield service_state.add_unit_state()
 
655
        yield service_state.remove_unit_state(unit_state)
 
656
        yield service_state.remove_unit_state(unit_state)
 
657
 
 
658
    @inlineCallbacks
 
659
    def test_get_all_service_states(self):
 
660
        services = yield self.service_state_manager.get_all_service_states()
 
661
        self.assertFalse(services)
 
662
 
 
663
        yield self.service_state_manager.add_service_state(
 
664
                "wordpress", self.charm_state, dummy_constraints)
 
665
        services = yield self.service_state_manager.get_all_service_states()
 
666
        self.assertEquals(len(services), 1)
 
667
 
 
668
        yield self.service_state_manager.add_service_state(
 
669
                "mysql", self.charm_state, dummy_constraints)
 
670
        services = yield self.service_state_manager.get_all_service_states()
 
671
        self.assertEquals(len(services), 2)
 
672
 
 
673
    @inlineCallbacks
 
674
    def test_get_service_unit(self):
 
675
        """
 
676
        Getting back service units should be possible using the
 
677
        user-oriented id.
 
678
        """
 
679
        service_state0 = yield self.service_state_manager.add_service_state(
 
680
                "wordpress", self.charm_state, dummy_constraints)
 
681
        service_state1 = yield self.service_state_manager.add_service_state(
 
682
                "mysql", self.charm_state, dummy_constraints)
 
683
 
 
684
        yield service_state0.add_unit_state()
 
685
        yield service_state1.add_unit_state()
 
686
        yield service_state0.add_unit_state()
 
687
        yield service_state1.add_unit_state()
 
688
 
 
689
        unit_state0 = yield service_state0.get_unit_state("wordpress/0")
 
690
        unit_state1 = yield service_state1.get_unit_state("mysql/0")
 
691
        unit_state2 = yield service_state0.get_unit_state("wordpress/1")
 
692
        unit_state3 = yield service_state1.get_unit_state("mysql/1")
 
693
 
 
694
        self.assertEquals(unit_state0.internal_id, "unit-0000000000")
 
695
        self.assertEquals(unit_state1.internal_id, "unit-0000000001")
 
696
        self.assertEquals(unit_state2.internal_id, "unit-0000000002")
 
697
        self.assertEquals(unit_state3.internal_id, "unit-0000000003")
 
698
 
 
699
        self.assertEquals(unit_state0.unit_name, "wordpress/0")
 
700
        self.assertEquals(unit_state1.unit_name, "mysql/0")
 
701
        self.assertEquals(unit_state2.unit_name, "wordpress/1")
 
702
        self.assertEquals(unit_state3.unit_name, "mysql/1")
 
703
 
 
704
    @inlineCallbacks
 
705
    def test_get_all_unit_states(self):
 
706
        service_state0 = yield self.service_state_manager.add_service_state(
 
707
                "wordpress", self.charm_state, dummy_constraints)
 
708
        service_state1 = yield self.service_state_manager.add_service_state(
 
709
                "mysql", self.charm_state, dummy_constraints)
 
710
 
 
711
        yield service_state0.add_unit_state()
 
712
        yield service_state1.add_unit_state()
 
713
        yield service_state0.add_unit_state()
 
714
        yield service_state1.add_unit_state()
 
715
 
 
716
        unit_state0 = yield service_state0.get_unit_state("wordpress/0")
 
717
        unit_state1 = yield service_state1.get_unit_state("mysql/0")
 
718
        unit_state2 = yield service_state0.get_unit_state("wordpress/1")
 
719
        unit_state3 = yield service_state1.get_unit_state("mysql/1")
 
720
 
 
721
        wordpress_units = yield service_state0.get_all_unit_states()
 
722
        self.assertEquals(
 
723
                set(wordpress_units), set((unit_state0, unit_state2)))
 
724
 
 
725
        mysql_units = yield service_state1.get_all_unit_states()
 
726
        self.assertEquals(set(mysql_units), set((unit_state1, unit_state3)))
 
727
 
 
728
    @inlineCallbacks
 
729
    def test_get_all_unit_states_with_changing_state(self):
 
730
        """
 
731
        When getting the service unit states, there's a chance that
 
732
        the service will go away mid-way through.  Rather than blowing
 
733
        up randomly, a nice error should be raised.
 
734
        """
 
735
        service_state = yield self.service_state_manager.add_service_state(
 
736
                "wordpress", self.charm_state, dummy_constraints)
 
737
        yield service_state.add_unit_state()
 
738
        unit_state = (yield service_state.get_all_unit_states())[0]
 
739
        self.assertEqual(unit_state.unit_name, "wordpress/0")
 
740
        yield self.remove_service(service_state.internal_id)
 
741
        yield self.assertFailure(
 
742
                service_state.get_all_unit_states(), StateChanged)
 
743
 
 
744
    @inlineCallbacks
 
745
    def test_set_functions(self):
 
746
        wordpress = yield self.service_state_manager.add_service_state(
 
747
                "wordpress", self.charm_state, dummy_constraints)
 
748
        mysql = yield self.service_state_manager.add_service_state(
 
749
                "mysql", self.charm_state, dummy_constraints)
 
750
 
 
751
        s1 = yield self.service_state_manager.get_service_state(
 
752
                "wordpress")
 
753
        s2 = yield self.service_state_manager.get_service_state(
 
754
                "mysql")
 
755
        self.assertEquals(hash(s1), hash(wordpress))
 
756
        self.assertEquals(hash(s2), hash(mysql))
 
757
 
 
758
        self.assertNotEqual(s1, object())
 
759
        self.assertNotEqual(s1, s2)
 
760
 
 
761
        self.assertEquals(s1, wordpress)
 
762
        self.assertEquals(s2, mysql)
 
763
 
 
764
        us0 = yield wordpress.add_unit_state()
 
765
        us1 = yield wordpress.add_unit_state()
 
766
 
 
767
        unit_state0 = yield wordpress.get_unit_state("wordpress/0")
 
768
        unit_state1 = yield wordpress.get_unit_state("wordpress/1")
 
769
 
 
770
        self.assertEquals(us0, unit_state0)
 
771
        self.assertEquals(us1, unit_state1)
 
772
        self.assertEquals(hash(us1), hash(unit_state1))
 
773
 
 
774
        self.assertNotEqual(us0, object())
 
775
        self.assertNotEqual(us0, us1)
 
776
 
 
777
    @inlineCallbacks
 
778
    def test_get_service_unit_not_found(self):
 
779
        """
 
780
        Attempting to retrieve a non-existent service unit should
 
781
        result in an errback.
 
782
        """
 
783
        service_state0 = yield self.service_state_manager.add_service_state(
 
784
                "wordpress", self.charm_state, dummy_constraints)
 
785
        service_state1 = yield self.service_state_manager.add_service_state(
 
786
                "mysql", self.charm_state, dummy_constraints)
 
787
 
 
788
        # Add some state in a different service to make it a little
 
789
        # bit more prone to breaking in case of errors.
 
790
        yield service_state1.add_unit_state()
 
791
 
 
792
        try:
 
793
            yield service_state0.get_unit_state("wordpress/0")
 
794
        except ServiceUnitStateNotFound, e:
 
795
            self.assertEquals(e.unit_name, "wordpress/0")
 
796
        else:
 
797
            self.fail("Error not raised")
 
798
 
 
799
    @inlineCallbacks
 
800
    def test_get_set_public_address(self):
 
801
        service_state = yield self.service_state_manager.add_service_state(
 
802
                "wordpress", self.charm_state, dummy_constraints)
 
803
        unit_state = yield service_state.add_unit_state()
 
804
        self.assertEqual((yield unit_state.get_public_address()), None)
 
805
        yield unit_state.set_public_address("example.foobar.com")
 
806
        yield self.assertEqual(
 
807
                (yield unit_state.get_public_address()),
 
808
                 "example.foobar.com")
 
809
 
 
810
    @inlineCallbacks
 
811
    def test_get_set_private_address(self):
 
812
        service_state = yield self.service_state_manager.add_service_state(
 
813
                "wordpress", self.charm_state, dummy_constraints)
 
814
        unit_state = yield service_state.add_unit_state()
 
815
        self.assertEqual((yield unit_state.get_private_address()), None)
 
816
        yield unit_state.set_private_address("example.local")
 
817
        yield self.assertEqual(
 
818
                (yield unit_state.get_private_address()),
 
819
                 "example.local")
 
820
 
 
821
    @inlineCallbacks
 
822
    def test_get_service_unit_with_changing_state(self):
 
823
        """
 
824
        If a service is removed during operation, get_service_unit()
 
825
        should raise a nice error.
 
826
        """
 
827
        service_state = yield self.service_state_manager.add_service_state(
 
828
                "wordpress", self.charm_state, dummy_constraints)
 
829
 
 
830
        yield self.remove_service(service_state.internal_id)
 
831
 
 
832
        d = service_state.get_unit_state("wordpress/0")
 
833
        yield self.assertFailure(d, StateChanged)
 
834
 
 
835
    @inlineCallbacks
 
836
    def test_get_service_unit_with_bad_service_name(self):
 
837
        """
 
838
        Service unit names contain a service name embedded into
 
839
        them.  The service name requested when calling get_unit_state()
 
840
        must match that of the object being used.
 
841
        """
 
842
        service_state0 = yield self.service_state_manager.add_service_state(
 
843
                "wordpress", self.charm_state, dummy_constraints)
 
844
        service_state1 = yield self.service_state_manager.add_service_state(
 
845
                "mysql", self.charm_state, dummy_constraints)
 
846
 
 
847
        # Add some state in a different service to make it a little
 
848
        # bit more prone to breaking in case of errors.
 
849
        yield service_state1.add_unit_state()
 
850
 
 
851
        try:
 
852
            yield service_state0.get_unit_state("mysql/0")
 
853
        except BadServiceStateName, e:
 
854
            self.assertEquals(e.expected_name, "wordpress")
 
855
            self.assertEquals(e.obtained_name, "mysql")
 
856
        else:
 
857
            self.fail("Error not raised")
 
858
 
 
859
    @inlineCallbacks
 
860
    def test_assign_unit_to_machine(self):
 
861
        service_state = yield self.service_state_manager.add_service_state(
 
862
                "wordpress", self.charm_state, dummy_constraints)
 
863
        unit_state = yield service_state.add_unit_state()
 
864
        machine_state = yield self.machine_state_manager.add_machine_state(
 
865
                series_constraints)
 
866
 
 
867
        yield unit_state.assign_to_machine(machine_state)
 
868
 
 
869
        topology = yield self.get_topology()
 
870
 
 
871
        self.assertEquals(
 
872
                topology.get_service_unit_machine(service_state.internal_id,
 
873
                                                  unit_state.internal_id),
 
874
                machine_state.internal_id)
 
875
 
 
876
    @inlineCallbacks
 
877
    def test_assign_unit_to_machine_with_changing_state(self):
 
878
        service_state = yield self.service_state_manager.add_service_state(
 
879
                "wordpress", self.charm_state, dummy_constraints)
 
880
        unit_state = yield service_state.add_unit_state()
 
881
        machine_state = yield self.machine_state_manager.add_machine_state(
 
882
                series_constraints)
 
883
 
 
884
        yield self.remove_service_unit(service_state.internal_id,
 
885
                                       unit_state.internal_id)
 
886
 
 
887
        d = unit_state.assign_to_machine(machine_state)
 
888
        yield self.assertFailure(d, StateChanged)
 
889
 
 
890
        yield self.remove_service(service_state.internal_id)
 
891
 
 
892
        d = unit_state.assign_to_machine(machine_state)
 
893
        yield self.assertFailure(d, StateChanged)
 
894
 
 
895
    @inlineCallbacks
 
896
    def test_unassign_unit_from_machine(self):
 
897
        service_state = yield self.service_state_manager.add_service_state(
 
898
                "wordpress", self.charm_state, dummy_constraints)
 
899
        unit_state = yield service_state.add_unit_state()
 
900
        machine_state = yield self.machine_state_manager.add_machine_state(
 
901
                series_constraints)
 
902
 
 
903
        yield unit_state.assign_to_machine(machine_state)
 
904
        yield unit_state.unassign_from_machine()
 
905
 
 
906
        topology = yield self.get_topology()
 
907
 
 
908
        self.assertEquals(topology.get_service_unit_machine(
 
909
            service_state.internal_id, unit_state.internal_id), None)
 
910
 
 
911
    @inlineCallbacks
 
912
    def test_get_set_clear_resolved(self):
 
913
        """The a unit can be set to resolved to mark a future transition, with
 
914
        an optional retry flag."""
 
915
 
 
916
        unit_state = yield self.get_unit_state()
 
917
 
 
918
        self.assertIdentical((yield unit_state.get_resolved()), None)
 
919
        yield unit_state.set_resolved(NO_HOOKS)
 
920
 
 
921
        yield self.assertFailure(
 
922
                unit_state.set_resolved(NO_HOOKS),
 
923
                ServiceUnitResolvedAlreadyEnabled)
 
924
        yield self.assertEqual((yield unit_state.get_resolved()),
 
925
                               {"retry": NO_HOOKS})
 
926
 
 
927
        yield unit_state.clear_resolved()
 
928
        self.assertIdentical((yield unit_state.get_resolved()), None)
 
929
        yield unit_state.clear_resolved()
 
930
 
 
931
        yield self.assertFailure(unit_state.set_resolved(None), ValueError)
 
932
 
 
933
    @inlineCallbacks
 
934
    def test_watch_resolved(self):
 
935
        """A unit resolved watch can be instituted on a permanent basis."""
 
936
        unit_state = yield self.get_unit_state()
 
937
 
 
938
        results = []
 
939
 
 
940
        def callback(value):
 
941
            results.append(value)
 
942
 
 
943
        unit_state.watch_resolved(callback)
 
944
        yield unit_state.set_resolved(RETRY_HOOKS)
 
945
        yield unit_state.clear_resolved()
 
946
        yield unit_state.set_resolved(NO_HOOKS)
 
947
 
 
948
        yield self.poke_zk()
 
949
 
 
950
        self.assertEqual(len(results), 4)
 
951
        self.assertIdentical(results.pop(0), False)
 
952
        self.assertEqual(results.pop(0).type_name, "created")
 
953
        self.assertEqual(results.pop(0).type_name, "deleted")
 
954
        self.assertEqual(results.pop(0).type_name, "created")
 
955
 
 
956
        self.assertEqual(
 
957
                (yield unit_state.get_resolved()),
 
958
                {"retry": NO_HOOKS})
 
959
 
 
960
    @inlineCallbacks
 
961
    def test_watch_resolved_processes_current_state(self):
 
962
        """The watch method processes the current state before returning."""
 
963
        unit_state = yield self.get_unit_state()
 
964
 
 
965
        results = []
 
966
 
 
967
        @inlineCallbacks
 
968
        def callback(value):
 
969
            results.append((yield unit_state.get_resolved()))
 
970
 
 
971
        yield unit_state.watch_resolved(callback)
 
972
        self.assertTrue(results)
 
973
 
 
974
    @inlineCallbacks
 
975
    def test_stop_watch_resolved(self):
 
976
        """A unit resolved watch can be instituted on a permanent basis.
 
977
 
 
978
        However the callback can raise StopWatcher at anytime to stop the watch
 
979
        """
 
980
        unit_state = yield self.get_unit_state()
 
981
 
 
982
        results = []
 
983
 
 
984
        def callback(value):
 
985
            results.append(value)
 
986
            if len(results) == 1:
 
987
                raise StopWatcher()
 
988
            if len(results) == 3:
 
989
                raise StopWatcher()
 
990
 
 
991
        unit_state.watch_resolved(callback)
 
992
        yield unit_state.set_resolved(RETRY_HOOKS)
 
993
        yield unit_state.clear_resolved()
 
994
        yield self.poke_zk()
 
995
 
 
996
        unit_state.watch_resolved(callback)
 
997
        yield unit_state.set_resolved(NO_HOOKS)
 
998
        yield unit_state.clear_resolved()
 
999
 
 
1000
        yield self.poke_zk()
 
1001
 
 
1002
        self.assertEqual(len(results), 3)
 
1003
        self.assertIdentical(results.pop(0), False)
 
1004
        self.assertIdentical(results.pop(0), False)
 
1005
        self.assertEqual(results.pop(0).type_name, "created")
 
1006
 
 
1007
        self.assertEqual(
 
1008
                (yield unit_state.get_resolved()), None)
 
1009
 
 
1010
    @inlineCallbacks
 
1011
    def test_get_set_clear_relation_resolved(self):
 
1012
        """The a unit's realtions can be set to resolved to mark a
 
1013
        future transition, with an optional retry flag."""
 
1014
 
 
1015
        unit_state = yield self.get_unit_state()
 
1016
 
 
1017
        self.assertIdentical((yield unit_state.get_relation_resolved()), None)
 
1018
        yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
 
1019
 
 
1020
        # Trying to set a conflicting raises an error
 
1021
        yield self.assertFailure(
 
1022
                unit_state.set_relation_resolved({"0": NO_HOOKS}),
 
1023
                ServiceUnitRelationResolvedAlreadyEnabled)
 
1024
 
 
1025
        # Doing the same thing is fine
 
1026
        yield unit_state.set_relation_resolved({"0": RETRY_HOOKS}),
 
1027
 
 
1028
        # Its fine to put in new values
 
1029
        yield unit_state.set_relation_resolved({"21": RETRY_HOOKS})
 
1030
        yield self.assertEqual(
 
1031
                (yield unit_state.get_relation_resolved()),
 
1032
                {"0": RETRY_HOOKS, "21": RETRY_HOOKS})
 
1033
 
 
1034
        yield unit_state.clear_relation_resolved()
 
1035
        self.assertIdentical((yield unit_state.get_relation_resolved()), None)
 
1036
        yield unit_state.clear_relation_resolved()
 
1037
 
 
1038
        yield self.assertFailure(
 
1039
                unit_state.set_relation_resolved(True), ValueError)
 
1040
        yield self.assertFailure(
 
1041
                unit_state.set_relation_resolved(None), ValueError)
 
1042
 
 
1043
    @inlineCallbacks
 
1044
    def test_watch_relation_resolved(self):
 
1045
        """A unit resolved watch can be instituted on a permanent basis."""
 
1046
        unit_state = yield self.get_unit_state()
 
1047
 
 
1048
        results = []
 
1049
 
 
1050
        def callback(value):
 
1051
            results.append(value)
 
1052
 
 
1053
        unit_state.watch_relation_resolved(callback)
 
1054
        yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
 
1055
        yield unit_state.clear_relation_resolved()
 
1056
        yield unit_state.set_relation_resolved({"0": NO_HOOKS})
 
1057
 
 
1058
        yield self.poke_zk()
 
1059
 
 
1060
        self.assertEqual(len(results), 4)
 
1061
        self.assertIdentical(results.pop(0), False)
 
1062
        self.assertEqual(results.pop(0).type_name, "created")
 
1063
        self.assertEqual(results.pop(0).type_name, "deleted")
 
1064
        self.assertEqual(results.pop(0).type_name, "created")
 
1065
 
 
1066
        self.assertEqual(
 
1067
                (yield unit_state.get_relation_resolved()),
 
1068
                {"0": NO_HOOKS})
 
1069
 
 
1070
    @inlineCallbacks
 
1071
    def test_watch_relation_resolved_processes_current_state(self):
 
1072
        """The watch method returns only after processing the current state."""
 
1073
        unit_state = yield self.get_unit_state()
 
1074
 
 
1075
        results = []
 
1076
 
 
1077
        @inlineCallbacks
 
1078
        def callback(value):
 
1079
            results.append((yield unit_state.get_relation_resolved()))
 
1080
        yield unit_state.watch_relation_resolved(callback)
 
1081
        self.assertTrue(results)
 
1082
 
 
1083
    @inlineCallbacks
 
1084
    def test_stop_watch_relation_resolved(self):
 
1085
        """A unit resolved watch can be instituted on a permanent basis."""
 
1086
        unit_state = yield self.get_unit_state()
 
1087
 
 
1088
        results = []
 
1089
 
 
1090
        def callback(value):
 
1091
            results.append(value)
 
1092
 
 
1093
            if len(results) == 1:
 
1094
                raise StopWatcher()
 
1095
 
 
1096
            if len(results) == 3:
 
1097
                raise StopWatcher()
 
1098
 
 
1099
        unit_state.watch_relation_resolved(callback)
 
1100
        yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
 
1101
        yield unit_state.clear_relation_resolved()
 
1102
        yield self.poke_zk()
 
1103
        self.assertEqual(len(results), 1)
 
1104
 
 
1105
        unit_state.watch_relation_resolved(callback)
 
1106
        yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
 
1107
        yield unit_state.clear_relation_resolved()
 
1108
        yield self.poke_zk()
 
1109
        self.assertEqual(len(results), 3)
 
1110
        self.assertIdentical(results.pop(0), False)
 
1111
        self.assertIdentical(results.pop(0), False)
 
1112
        self.assertEqual(results.pop(0).type_name, "created")
 
1113
 
 
1114
        self.assertEqual(
 
1115
                (yield unit_state.get_relation_resolved()), None)
 
1116
 
 
1117
    @inlineCallbacks
 
1118
    def test_watch_resolved_slow_callback(self):
 
1119
        """A slow watch callback is still invoked serially."""
 
1120
        unit_state = yield self.get_unit_state()
 
1121
 
 
1122
        callbacks = [Deferred() for i in range(5)]
 
1123
        results = []
 
1124
        contents = []
 
1125
 
 
1126
        @inlineCallbacks
 
1127
        def watch(value):
 
1128
            results.append(value)
 
1129
            yield callbacks[len(results) - 1]
 
1130
            contents.append((yield unit_state.get_resolved()))
 
1131
 
 
1132
        callbacks[0].callback(True)
 
1133
        yield unit_state.watch_resolved(watch)
 
1134
 
 
1135
        # These get collapsed into a single event
 
1136
        yield unit_state.set_resolved(RETRY_HOOKS)
 
1137
        yield unit_state.clear_resolved()
 
1138
        yield self.poke_zk()
 
1139
 
 
1140
        # Verify the callback hasn't completed
 
1141
        self.assertEqual(len(results), 2)
 
1142
        self.assertEqual(len(contents), 1)
 
1143
 
 
1144
        # Let it finish
 
1145
        callbacks[1].callback(True)
 
1146
        yield self.poke_zk()
 
1147
 
 
1148
        # Verify result counts
 
1149
        self.assertEqual(len(results), 3)
 
1150
        self.assertEqual(len(contents), 2)
 
1151
 
 
1152
        # Verify result values. Even though we have created event, the
 
1153
        # setting retrieved shows the hook is not enabled.
 
1154
        self.assertEqual(results[-1].type_name, "deleted")
 
1155
        self.assertEqual(contents[-1], None)
 
1156
 
 
1157
        yield unit_state.set_resolved(NO_HOOKS)
 
1158
        callbacks[2].callback(True)
 
1159
        yield self.poke_zk()
 
1160
 
 
1161
        self.assertEqual(len(results), 4)
 
1162
        self.assertEqual(contents[-1], {"retry": NO_HOOKS})
 
1163
 
 
1164
        # Clear out any pending activity.
 
1165
        yield self.poke_zk()
 
1166
 
 
1167
    @inlineCallbacks
 
1168
    def test_watch_relation_resolved_slow_callback(self):
 
1169
        """A slow watch callback is still invoked serially."""
 
1170
        unit_state = yield self.get_unit_state()
 
1171
 
 
1172
        callbacks = [Deferred() for i in range(5)]
 
1173
        results = []
 
1174
        contents = []
 
1175
 
 
1176
        @inlineCallbacks
 
1177
        def watch(value):
 
1178
            results.append(value)
 
1179
            yield callbacks[len(results) - 1]
 
1180
            contents.append((yield unit_state.get_relation_resolved()))
 
1181
 
 
1182
        callbacks[0].callback(True)
 
1183
        yield unit_state.watch_relation_resolved(watch)
 
1184
 
 
1185
        # These get collapsed into a single event
 
1186
        yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
 
1187
        yield unit_state.clear_relation_resolved()
 
1188
        yield self.poke_zk()
 
1189
 
 
1190
        # Verify the callback hasn't completed
 
1191
        self.assertEqual(len(results), 2)
 
1192
        self.assertEqual(len(contents), 1)
 
1193
 
 
1194
        # Let it finish
 
1195
        callbacks[1].callback(True)
 
1196
        yield self.poke_zk()
 
1197
 
 
1198
        # Verify result counts
 
1199
        self.assertEqual(len(results), 3)
 
1200
        self.assertEqual(len(contents), 2)
 
1201
 
 
1202
        # Verify result values. Even though we have created event, the
 
1203
        # setting retrieved shows the hook is not enabled.
 
1204
        self.assertEqual(results[-1].type_name, "deleted")
 
1205
        self.assertEqual(contents[-1], None)
 
1206
 
 
1207
        yield unit_state.set_relation_resolved({"0": RETRY_HOOKS})
 
1208
        callbacks[2].callback(True)
 
1209
        yield self.poke_zk()
 
1210
 
 
1211
        self.assertEqual(len(results), 4)
 
1212
        self.assertEqual(contents[-1], {"0": RETRY_HOOKS})
 
1213
 
 
1214
        # Clear out any pending activity.
 
1215
        yield self.poke_zk()
 
1216
 
 
1217
    @inlineCallbacks
 
1218
    def test_set_and_clear_upgrade_flag(self):
 
1219
        """An upgrade flag can be set on a unit."""
 
1220
 
 
1221
        # Defaults to false
 
1222
        unit_state = yield self.get_unit_state()
 
1223
        upgrade_flag = yield unit_state.get_upgrade_flag()
 
1224
        self.assertEqual(upgrade_flag, False)
 
1225
 
 
1226
        # Can be set
 
1227
        yield unit_state.set_upgrade_flag()
 
1228
        upgrade_flag = yield unit_state.get_upgrade_flag()
 
1229
        self.assertEqual(upgrade_flag, {"force": False})
 
1230
 
 
1231
        # Attempting to set multiple times is an error if the values
 
1232
        # differ.
 
1233
        yield self.assertFailure(
 
1234
                unit_state.set_upgrade_flag(force=True),
 
1235
                ServiceUnitUpgradeAlreadyEnabled)
 
1236
        self.assertEqual(upgrade_flag, {"force": False})
 
1237
 
 
1238
        # Can be cleared
 
1239
        yield unit_state.clear_upgrade_flag()
 
1240
        upgrade_flag = yield unit_state.get_upgrade_flag()
 
1241
        self.assertEqual(upgrade_flag, False)
 
1242
 
 
1243
        # Can be cleared multiple times
 
1244
        yield unit_state.clear_upgrade_flag()
 
1245
        upgrade_flag = yield unit_state.get_upgrade_flag()
 
1246
        self.assertEqual(upgrade_flag, False)
 
1247
 
 
1248
        # A empty node present is not problematic
 
1249
        yield self.client.create(unit_state._upgrade_flag_path, "")
 
1250
        upgrade_flag = yield unit_state.get_upgrade_flag()
 
1251
        self.assertEqual(upgrade_flag, False)
 
1252
 
 
1253
        yield unit_state.set_upgrade_flag(force=True)
 
1254
        upgrade_flag = yield unit_state.get_upgrade_flag()
 
1255
        self.assertEqual(upgrade_flag, {"force": True})
 
1256
 
 
1257
    @inlineCallbacks
 
1258
    def test_watch_upgrade_flag_once(self):
 
1259
        """An upgrade watch can be set to notified of presence and changes."""
 
1260
        unit_state = yield self.get_unit_state()
 
1261
        yield unit_state.set_upgrade_flag()
 
1262
 
 
1263
        results = []
 
1264
 
 
1265
        def callback(value):
 
1266
            results.append(value)
 
1267
 
 
1268
        unit_state.watch_upgrade_flag(callback, permanent=False)
 
1269
        yield unit_state.clear_upgrade_flag()
 
1270
        yield unit_state.set_upgrade_flag(force=True)
 
1271
        yield self.sleep(0.1)
 
1272
        yield self.poke_zk()
 
1273
        self.assertEqual(len(results), 2)
 
1274
        self.assertIdentical(results.pop(0), True)
 
1275
        self.assertIdentical(results.pop().type_name, "deleted")
 
1276
 
 
1277
        self.assertEqual(
 
1278
                (yield unit_state.get_upgrade_flag()),
 
1279
                {"force": True})
 
1280
 
 
1281
    @inlineCallbacks
 
1282
    def test_watch_upgrade_processes_current_state(self):
 
1283
        unit_state = yield self.get_unit_state()
 
1284
        results = []
 
1285
 
 
1286
        @inlineCallbacks
 
1287
        def callback(value):
 
1288
            results.append((yield unit_state.get_upgrade_flag()))
 
1289
 
 
1290
        yield unit_state.watch_upgrade_flag(callback)
 
1291
        self.assertTrue(results)
 
1292
 
 
1293
    @inlineCallbacks
 
1294
    def test_watch_upgrade_flag_permanent(self):
 
1295
        """An upgrade watch can be instituted on a permanent basis."""
 
1296
        unit_state = yield self.get_unit_state()
 
1297
 
 
1298
        results = []
 
1299
 
 
1300
        def callback(value):
 
1301
            results.append(value)
 
1302
 
 
1303
        yield unit_state.watch_upgrade_flag(callback)
 
1304
        self.assertTrue(results)
 
1305
        yield unit_state.set_upgrade_flag()
 
1306
        yield unit_state.clear_upgrade_flag()
 
1307
        yield unit_state.set_upgrade_flag()
 
1308
 
 
1309
        yield self.poke_zk()
 
1310
 
 
1311
        self.assertEqual(len(results), 4)
 
1312
        self.assertIdentical(results.pop(0), False)
 
1313
        self.assertIdentical(results.pop(0).type_name, "created")
 
1314
        self.assertIdentical(results.pop(0).type_name, "deleted")
 
1315
        self.assertIdentical(results.pop(0).type_name, "created")
 
1316
 
 
1317
        self.assertEqual(
 
1318
                (yield unit_state.get_upgrade_flag()),
 
1319
                {"force": False})
 
1320
 
 
1321
    @inlineCallbacks
 
1322
    def test_watch_upgrade_flag_waits_on_slow_callbacks(self):
 
1323
        """A slow watch callback is still invoked serially."""
 
1324
        unit_state = yield self.get_unit_state()
 
1325
 
 
1326
        callbacks = [Deferred() for i in range(5)]
 
1327
        results = []
 
1328
        contents = []
 
1329
 
 
1330
        @inlineCallbacks
 
1331
        def watch(value):
 
1332
            results.append(value)
 
1333
            yield callbacks[len(results) - 1]
 
1334
            contents.append((yield unit_state.get_upgrade_flag()))
 
1335
 
 
1336
        yield callbacks[0].callback(True)
 
1337
        yield unit_state.watch_upgrade_flag(watch)
 
1338
 
 
1339
        # These get collapsed into a single event
 
1340
        yield unit_state.set_upgrade_flag()
 
1341
        yield unit_state.clear_upgrade_flag()
 
1342
        yield self.poke_zk()
 
1343
 
 
1344
        # Verify the callback hasn't completed
 
1345
        self.assertEqual(len(results), 2)
 
1346
        self.assertEqual(len(contents), 1)
 
1347
 
 
1348
        # Let it finish
 
1349
        callbacks[1].callback(True)
 
1350
        yield self.poke_zk()
 
1351
 
 
1352
        # Verify result counts
 
1353
        self.assertEqual(len(results), 3)
 
1354
        self.assertEqual(len(contents), 2)
 
1355
 
 
1356
        # Verify result values. Even though we have created event, the
 
1357
        # setting retrieved shows the hook is not enabled.
 
1358
        self.assertEqual(results[-1].type_name, "deleted")
 
1359
        self.assertEqual(contents[-1], False)
 
1360
 
 
1361
        yield unit_state.set_upgrade_flag()
 
1362
        yield self.poke_zk()
 
1363
 
 
1364
        # Verify the callback hasn't completed
 
1365
        self.assertEqual(len(contents), 2)
 
1366
 
 
1367
        callbacks[2].callback(True)
 
1368
        yield self.poke_zk()
 
1369
 
 
1370
        # Verify values.
 
1371
        self.assertEqual(len(contents), 3)
 
1372
        self.assertEqual(results[-1].type_name, "created")
 
1373
        self.assertEqual(contents[-1], {"force": False})
 
1374
 
 
1375
        # Clear out any pending activity.
 
1376
        yield self.poke_zk()
 
1377
 
 
1378
    @inlineCallbacks
 
1379
    def test_enable_debug_hook(self):
 
1380
        """Unit hook debugging can be enabled on the unit state."""
 
1381
        unit_state = yield self.get_unit_state()
 
1382
        enabled = yield unit_state.enable_hook_debug(["*"])
 
1383
        self.assertIdentical(enabled, True)
 
1384
        content, stat = yield self.client.get(
 
1385
                "/units/%s/debug" % unit_state.internal_id)
 
1386
        data = yaml.load(content)
 
1387
        self.assertEqual(data, {"debug_hooks": ["*"]})
 
1388
 
 
1389
    @inlineCallbacks
 
1390
    def test_enable_debug_multiple_named_hooks(self):
 
1391
        """Unit hook debugging can be enabled for multiple hooks."""
 
1392
        unit_state = yield self.get_unit_state()
 
1393
        enabled = yield unit_state.enable_hook_debug(
 
1394
                ["db-relation-broken", "db-relation-changed"])
 
1395
 
 
1396
        self.assertIdentical(enabled, True)
 
1397
        content, stat = yield self.client.get(
 
1398
                "/units/%s/debug" % unit_state.internal_id)
 
1399
        data = yaml.load(content)
 
1400
        self.assertEqual(data, {"debug_hooks":
 
1401
                            ["db-relation-broken", "db-relation-changed"]})
 
1402
 
 
1403
    @inlineCallbacks
 
1404
    def test_enable_debug_all_and_named_is_error(self):
 
1405
        """Unit hook debugging can be enabled for multiple hooks,
 
1406
        but only if they are all named hooks."""
 
1407
        unit_state = yield self.get_unit_state()
 
1408
 
 
1409
        error = yield self.assertFailure(
 
1410
                unit_state.enable_hook_debug(["*", "db-relation-changed"]),
 
1411
                ValueError)
 
1412
        self.assertEquals(
 
1413
                str(error),
 
1414
                "Ambigious to debug all hooks and named hooks "
 
1415
                "['*', 'db-relation-changed']")
 
1416
 
 
1417
    @inlineCallbacks
 
1418
    def test_enable_debug_requires_sequence(self):
 
1419
        """The enable hook debug only accepts a sequences of names.
 
1420
        """
 
1421
        unit_state = yield self.get_unit_state()
 
1422
 
 
1423
        error = yield self.assertFailure(
 
1424
                unit_state.enable_hook_debug(None),
 
1425
                AssertionError)
 
1426
        self.assertEquals(str(error), "Hook names must be a list: got None")
 
1427
 
 
1428
    @inlineCallbacks
 
1429
    def test_enable_named_debug_hook(self):
 
1430
        """Unit hook debugging can be enabled on for a named hook."""
 
1431
        unit_state = yield self.get_unit_state()
 
1432
        enabled = yield unit_state.enable_hook_debug(
 
1433
                ["db-relation-changed"])
 
1434
        self.assertIdentical(enabled, True)
 
1435
        content, stat = yield self.client.get(
 
1436
                "/units/%s/debug" % unit_state.internal_id)
 
1437
        data = yaml.load(content)
 
1438
        self.assertEqual(data, {"debug_hooks": ["db-relation-changed"]})
 
1439
 
 
1440
    @inlineCallbacks
 
1441
    def test_enable_debug_hook_pre_existing(self):
 
1442
        """Attempting to enable debug on a unit state already being debugged
 
1443
        raises an exception.
 
1444
        """
 
1445
        unit_state = yield self.get_unit_state()
 
1446
        yield unit_state.enable_hook_debug(["*"])
 
1447
        error = yield self.assertFailure(unit_state.enable_hook_debug(["*"]),
 
1448
                                         ServiceUnitDebugAlreadyEnabled)
 
1449
        self.assertEquals(str(error),
 
1450
                        "Service unit 'wordpress/0' is already in debug mode.")
 
1451
 
 
1452
    @inlineCallbacks
 
1453
    def test_enable_debug_hook_lifetime(self):
 
1454
        """A debug hook setting is only active for the lifetime of the client
 
1455
        that created it.
 
1456
        """
 
1457
        unit_state = yield self.get_unit_state()
 
1458
        yield unit_state.enable_hook_debug(["*"])
 
1459
        exists = yield self.client.exists(
 
1460
                "/units/%s/debug" % unit_state.internal_id)
 
1461
        self.assertTrue(exists)
 
1462
        yield self.client.close()
 
1463
        self.client = self.get_zookeeper_client()
 
1464
        yield self.client.connect()
 
1465
        exists = yield self.client.exists(
 
1466
                "/units/%s/debug" % unit_state.internal_id)
 
1467
        self.assertFalse(exists)
 
1468
 
 
1469
    @inlineCallbacks
 
1470
    def test_watch_debug_hook_once(self):
 
1471
        """A watch can be set to notified of presence and changes."""
 
1472
        unit_state = yield self.get_unit_state()
 
1473
        yield unit_state.enable_hook_debug(["*"])
 
1474
 
 
1475
        results = []
 
1476
 
 
1477
        def callback(value):
 
1478
            results.append(value)
 
1479
 
 
1480
        yield unit_state.watch_hook_debug(callback, permanent=False)
 
1481
        yield unit_state.clear_hook_debug()
 
1482
        yield unit_state.enable_hook_debug(["*"])
 
1483
        yield self.poke_zk()
 
1484
        self.assertEqual(len(results), 2)
 
1485
        self.assertIdentical(results.pop(0), True)
 
1486
        self.assertIdentical(results.pop().type_name, "deleted")
 
1487
 
 
1488
        self.assertEqual(
 
1489
                (yield unit_state.get_hook_debug()),
 
1490
                {"debug_hooks": ["*"]})
 
1491
 
 
1492
    @inlineCallbacks
 
1493
    def test_watch_debug_hook_processes_current_state(self):
 
1494
        """A hook debug watch can be instituted on a permanent basis."""
 
1495
        unit_state = yield self.get_unit_state()
 
1496
 
 
1497
        results = []
 
1498
 
 
1499
        @inlineCallbacks
 
1500
        def callback(value):
 
1501
            results.append((yield unit_state.get_hook_debug()))
 
1502
 
 
1503
        yield unit_state.watch_hook_debug(callback)
 
1504
        self.assertTrue(results)
 
1505
 
 
1506
    @inlineCallbacks
 
1507
    def test_watch_debug_hook_permanent(self):
 
1508
        """A hook debug watch can be instituted on a permanent basis."""
 
1509
        unit_state = yield self.get_unit_state()
 
1510
 
 
1511
        results = []
 
1512
 
 
1513
        def callback(value):
 
1514
            results.append(value)
 
1515
 
 
1516
        yield unit_state.watch_hook_debug(callback)
 
1517
        yield unit_state.enable_hook_debug(["*"])
 
1518
        yield unit_state.clear_hook_debug()
 
1519
        yield unit_state.enable_hook_debug(["*"])
 
1520
 
 
1521
        yield self.poke_zk()
 
1522
 
 
1523
        self.assertEqual(len(results), 4)
 
1524
        self.assertIdentical(results.pop(0), False)
 
1525
        self.assertIdentical(results.pop(0).type_name, "created")
 
1526
        self.assertIdentical(results.pop(0).type_name, "deleted")
 
1527
        self.assertIdentical(results.pop(0).type_name, "created")
 
1528
 
 
1529
        self.assertEqual(
 
1530
                (yield unit_state.get_hook_debug()),
 
1531
                {"debug_hooks": ["*"]})
 
1532
 
 
1533
    @inlineCallbacks
 
1534
    def test_watch_debug_hook_waits_on_slow_callbacks(self):
 
1535
        """A slow watch callback is still invoked serially."""
 
1536
 
 
1537
        unit_state = yield self.get_unit_state()
 
1538
 
 
1539
        callbacks = [Deferred() for i in range(5)]
 
1540
        results = []
 
1541
        contents = []
 
1542
 
 
1543
        @inlineCallbacks
 
1544
        def watch(value):
 
1545
            results.append(value)
 
1546
            yield callbacks[len(results) - 1]
 
1547
            contents.append((yield unit_state.get_hook_debug()))
 
1548
 
 
1549
        callbacks[0].callback(True)  # Finish the current state processing
 
1550
        yield unit_state.watch_hook_debug(watch)
 
1551
 
 
1552
        # These get collapsed into a single event
 
1553
        yield unit_state.enable_hook_debug(["*"])
 
1554
        yield unit_state.clear_hook_debug()
 
1555
        yield self.poke_zk()
 
1556
 
 
1557
        # Verify the callback hasn't completed
 
1558
        self.assertEqual(len(results), 2)
 
1559
        self.assertEqual(len(contents), 1)
 
1560
 
 
1561
        # Let it finish
 
1562
        callbacks[1].callback(True)
 
1563
        yield self.poke_zk()
 
1564
 
 
1565
        # Verify result counts
 
1566
        self.assertEqual(len(results), 3)
 
1567
        self.assertEqual(len(contents), 2)
 
1568
 
 
1569
        # Verify result values. Even though we have created event, the
 
1570
        # setting retrieved shows the hook is not enabled.
 
1571
        self.assertEqual(results[-1].type_name, "deleted")
 
1572
        self.assertEqual(contents[-1], None)
 
1573
 
 
1574
        yield unit_state.enable_hook_debug(["*"])
 
1575
        yield self.poke_zk()
 
1576
 
 
1577
        # Verify the callback hasn't completed
 
1578
        self.assertEqual(len(contents), 2)
 
1579
 
 
1580
        callbacks[2].callback(True)
 
1581
        yield self.poke_zk()
 
1582
 
 
1583
        # Verify values.
 
1584
        self.assertEqual(len(contents), 3)
 
1585
        self.assertEqual(results[-1].type_name, "created")
 
1586
        self.assertEqual(contents[-1], {"debug_hooks": ["*"]})
 
1587
 
 
1588
        # Clear out any pending activity.
 
1589
        yield self.poke_zk()
 
1590
 
 
1591
    @inlineCallbacks
 
1592
    def test_service_unit_agent(self):
 
1593
        """A service unit state has an associated unit agent."""
 
1594
        service_state = yield self.service_state_manager.add_service_state(
 
1595
                "wordpress", self.charm_state, dummy_constraints)
 
1596
        unit_state = yield service_state.add_unit_state()
 
1597
        exists_d, watch_d = unit_state.watch_agent()
 
1598
        exists = yield exists_d
 
1599
        self.assertFalse(exists)
 
1600
        yield unit_state.connect_agent()
 
1601
        event = yield watch_d
 
1602
        self.assertEqual(event.type_name, "created")
 
1603
        self.assertEqual(event.path,
 
1604
                                         "/units/%s/agent" % unit_state.internal_id)
 
1605
 
 
1606
    @inlineCallbacks
 
1607
    def test_get_charm_id(self):
 
1608
        """A service unit knows its charm id"""
 
1609
        service_state = yield self.service_state_manager.add_service_state(
 
1610
                "wordpress", self.charm_state, dummy_constraints)
 
1611
        unit_state = yield service_state.add_unit_state()
 
1612
        unit_charm = yield unit_state.get_charm_id()
 
1613
        service_charm = yield service_state.get_charm_id()
 
1614
        self.assertTrue(
 
1615
                (self.charm_state.id == unit_charm == service_charm))
 
1616
 
 
1617
    @inlineCallbacks
 
1618
    def test_set_charm_id(self):
 
1619
        """A service unit charm can be set and is validated when set."""
 
1620
        service_state = yield self.service_state_manager.add_service_state(
 
1621
                "wordpress", self.charm_state, dummy_constraints)
 
1622
        unit_state = yield service_state.add_unit_state()
 
1623
        yield self.assertFailure(
 
1624
                unit_state.set_charm_id("abc"), CharmURLError)
 
1625
        yield self.assertFailure(
 
1626
                unit_state.set_charm_id("abc:foobar-a"), CharmURLError)
 
1627
        yield self.assertFailure(
 
1628
                unit_state.set_charm_id(None), CharmURLError)
 
1629
        charm_id = "local:series/name-1"
 
1630
        yield unit_state.set_charm_id(charm_id)
 
1631
        value = yield unit_state.get_charm_id()
 
1632
        self.assertEqual(charm_id, value)
 
1633
 
 
1634
    @inlineCallbacks
 
1635
    def test_add_unit_state_combines_constraints(self):
 
1636
        """Constraints are inherited both from juju defaults and service"""
 
1637
        service_state = yield self.service_state_manager.add_service_state(
 
1638
                "wordpress", self.charm_state,
 
1639
                dummy_cs.parse(["arch=arm", "mem=1G"]))
 
1640
        unit_state = yield service_state.add_unit_state()
 
1641
        constraints = yield unit_state.get_constraints()
 
1642
        expected = {
 
1643
                "arch": "arm", "cpu": 1, "mem": 1024,
 
1644
                "provider-type": "dummy", "ubuntu-series": "series"}
 
1645
        self.assertEquals(constraints, expected)
 
1646
 
 
1647
    @inlineCallbacks
 
1648
    def test_unassign_unit_from_machine_without_being_assigned(self):
 
1649
        """
 
1650
        When unassigning a machine from a unit, it is possible that
 
1651
        the machine has not been previously assigned, or that it
 
1652
        was assigned but the state changed beneath us.  In either
 
1653
        case, the end state is the intended state, so we simply
 
1654
        move forward without any errors here, to avoid having to
 
1655
        handle the extra complexity of dealing with the concurrency
 
1656
        problems.
 
1657
        """
 
1658
        service_state = yield self.service_state_manager.add_service_state(
 
1659
                "wordpress", self.charm_state, dummy_constraints)
 
1660
        unit_state = yield service_state.add_unit_state()
 
1661
 
 
1662
        yield unit_state.unassign_from_machine()
 
1663
 
 
1664
        topology = yield self.get_topology()
 
1665
        self.assertEquals(topology.get_service_unit_machine(
 
1666
            service_state.internal_id, unit_state.internal_id), None)
 
1667
 
 
1668
        machine_id = yield unit_state.get_assigned_machine_id()
 
1669
        self.assertEqual(machine_id, None)
 
1670
 
 
1671
    @inlineCallbacks
 
1672
    def test_assign_unit_to_machine_again_fails(self):
 
1673
        """
 
1674
        Trying to assign a machine to an already assigned unit
 
1675
        should fail, unless we're assigning to precisely the same
 
1676
        machine, in which case it's no big deal.
 
1677
        """
 
1678
        service_state = yield self.service_state_manager.add_service_state(
 
1679
                "wordpress", self.charm_state, dummy_constraints)
 
1680
        unit_state = yield service_state.add_unit_state()
 
1681
        machine_state0 = yield self.machine_state_manager.add_machine_state(
 
1682
                series_constraints)
 
1683
        machine_state1 = yield self.machine_state_manager.add_machine_state(
 
1684
                series_constraints)
 
1685
 
 
1686
        yield unit_state.assign_to_machine(machine_state0)
 
1687
 
 
1688
        # Assigning again to the same machine is a NOOP, so nothing
 
1689
        # terrible should happen if we let it go through.
 
1690
        yield unit_state.assign_to_machine(machine_state0)
 
1691
 
 
1692
        try:
 
1693
            yield unit_state.assign_to_machine(machine_state1)
 
1694
        except ServiceUnitStateMachineAlreadyAssigned, e:
 
1695
            self.assertEquals(e.unit_name, "wordpress/0")
 
1696
        else:
 
1697
            self.fail("Error not raised")
 
1698
 
 
1699
        machine_id = yield unit_state.get_assigned_machine_id()
 
1700
        self.assertEqual(machine_id, 0)
 
1701
 
 
1702
    @inlineCallbacks
 
1703
    def test_unassign_unit_from_machine_with_changing_state(self):
 
1704
        service_state = yield self.service_state_manager.add_service_state(
 
1705
                "wordpress", self.charm_state, dummy_constraints)
 
1706
        unit_state = yield service_state.add_unit_state()
 
1707
 
 
1708
        yield self.remove_service_unit(service_state.internal_id,
 
1709
                                       unit_state.internal_id)
 
1710
 
 
1711
        d = unit_state.unassign_from_machine()
 
1712
        yield self.assertFailure(d, StateChanged)
 
1713
 
 
1714
        d = unit_state.get_assigned_machine_id()
 
1715
        yield self.assertFailure(d, StateChanged)
 
1716
 
 
1717
        yield self.remove_service(service_state.internal_id)
 
1718
 
 
1719
        d = unit_state.unassign_from_machine()
 
1720
        yield self.assertFailure(d, StateChanged)
 
1721
 
 
1722
        d = unit_state.get_assigned_machine_id()
 
1723
        yield self.assertFailure(d, StateChanged)
 
1724
 
 
1725
    @inlineCallbacks
 
1726
    def test_assign_unit_to_unused_machine(self):
 
1727
        """Verify that unused machines can be assigned to when their machine
 
1728
        constraints match the service unit's."""
 
1729
        yield self.machine_state_manager.add_machine_state(
 
1730
                series_constraints)
 
1731
        mysql_service_state = yield self.add_service_from_charm("mysql")
 
1732
        mysql_unit_state = yield mysql_service_state.add_unit_state()
 
1733
        mysql_machine_state = yield \
 
1734
                self.machine_state_manager.add_machine_state(series_constraints)
 
1735
        yield mysql_unit_state.assign_to_machine(mysql_machine_state)
 
1736
        yield self.service_state_manager.remove_service_state(
 
1737
                mysql_service_state)
 
1738
        wordpress_service_state = yield self.add_service_from_charm(
 
1739
                "wordpress")
 
1740
        wordpress_unit_state = yield wordpress_service_state.add_unit_state()
 
1741
        yield wordpress_unit_state.assign_to_unused_machine()
 
1742
        self.assertEqual(
 
1743
                (yield self.get_topology()).get_machines(),
 
1744
                ["machine-0000000000", "machine-0000000001"])
 
1745
        yield self.assert_machine_assignments("wordpress", [1])
 
1746
 
 
1747
    @inlineCallbacks
 
1748
    def test_assign_unit_to_unused_machine_bad_constraints(self):
 
1749
        """Verify that unused machines do not get allocated service units with
 
1750
        non-matching constraints."""
 
1751
        yield self.machine_state_manager.add_machine_state(
 
1752
                series_constraints)
 
1753
        mysql_service_state = yield self.add_service_from_charm("mysql")
 
1754
        mysql_unit_state = yield mysql_service_state.add_unit_state()
 
1755
        mysql_machine_state = yield \
 
1756
                self.machine_state_manager.add_machine_state(
 
1757
                        series_constraints)
 
1758
        yield mysql_unit_state.assign_to_machine(mysql_machine_state)
 
1759
        yield self.service_state_manager.remove_service_state(
 
1760
                mysql_service_state)
 
1761
        other_constraints = dummy_cs.parse(["arch=arm"])
 
1762
        wordpress_service_state = yield self.add_service_from_charm(
 
1763
                "wordpress", constraints=other_constraints.with_series("series"))
 
1764
        wordpress_unit_state = yield wordpress_service_state.add_unit_state()
 
1765
        yield self.assertFailure(
 
1766
                wordpress_unit_state.assign_to_unused_machine(),
 
1767
                NoUnusedMachines)
 
1768
        self.assertEqual(
 
1769
                (yield self.get_topology()).get_machines(),
 
1770
                ["machine-0000000000", "machine-0000000001"])
 
1771
        yield self.assert_machine_assignments("wordpress", [None])
 
1772
 
 
1773
    @inlineCallbacks
 
1774
    def test_assign_unit_to_unused_machine_with_changing_state_service(self):
 
1775
        """Verify `StateChanged` raised if service is manipulated during reuse.
 
1776
        """
 
1777
        yield self.machine_state_manager.add_machine_state(
 
1778
                series_constraints)
 
1779
        mysql_service_state = yield self.add_service_from_charm("mysql")
 
1780
        mysql_unit_state = yield mysql_service_state.add_unit_state()
 
1781
        mysql_machine_state = yield self.machine_state_manager.add_machine_state(
 
1782
                series_constraints)
 
1783
        yield mysql_unit_state.assign_to_machine(mysql_machine_state)
 
1784
        yield self.service_state_manager.remove_service_state(
 
1785
                mysql_service_state)
 
1786
        wordpress_service_state = yield self.add_service_from_charm(
 
1787
                "wordpress")
 
1788
        wordpress_unit_state = yield wordpress_service_state.add_unit_state()
 
1789
        yield self.remove_service(wordpress_service_state.internal_id)
 
1790
        yield self.assertFailure(
 
1791
                wordpress_unit_state.assign_to_unused_machine(), StateChanged)
 
1792
 
 
1793
    @inlineCallbacks
 
1794
    def test_assign_unit_to_unused_machine_with_changing_state_service_unit(self):
 
1795
        "Verify `StateChanged` raised if unit is manipulated during reuse."
 
1796
        yield self.machine_state_manager.add_machine_state(
 
1797
                series_constraints)
 
1798
        mysql_service_state = yield self.add_service_from_charm("mysql")
 
1799
        mysql_unit_state = yield mysql_service_state.add_unit_state()
 
1800
        mysql_machine_state = yield self.machine_state_manager.add_machine_state(
 
1801
                series_constraints)
 
1802
        yield mysql_unit_state.assign_to_machine(mysql_machine_state)
 
1803
        yield self.service_state_manager.remove_service_state(
 
1804
                mysql_service_state)
 
1805
        wordpress_service_state = yield self.add_service_from_charm(
 
1806
                "wordpress")
 
1807
        wordpress_unit_state = yield wordpress_service_state.add_unit_state()
 
1808
        yield self.remove_service_unit(
 
1809
                wordpress_service_state.internal_id,
 
1810
                wordpress_unit_state.internal_id)
 
1811
        yield self.assertFailure(
 
1812
                wordpress_unit_state.assign_to_unused_machine(), StateChanged)
 
1813
 
 
1814
    @inlineCallbacks
 
1815
    def test_assign_unit_to_unused_machine_only_machine_zero(self):
 
1816
        """Verify when the only available machine is machine 0"""
 
1817
        yield self.machine_state_manager.add_machine_state(
 
1818
                series_constraints)
 
1819
        wordpress_service_state = yield self.add_service_from_charm(
 
1820
                "wordpress")
 
1821
        wordpress_unit_state = yield wordpress_service_state.add_unit_state()
 
1822
        yield self.assertFailure(
 
1823
                wordpress_unit_state.assign_to_unused_machine(),
 
1824
                NoUnusedMachines)
 
1825
 
 
1826
    @inlineCallbacks
 
1827
    def test_assign_unit_to_unused_machine_none_available(self):
 
1828
        """Verify when there are no unused machines"""
 
1829
        yield self.machine_state_manager.add_machine_state(
 
1830
                series_constraints)
 
1831
        mysql_service_state = yield self.add_service_from_charm("mysql")
 
1832
        mysql_unit_state = yield mysql_service_state.add_unit_state()
 
1833
        mysql_machine_state = yield self.machine_state_manager.add_machine_state(
 
1834
                series_constraints)
 
1835
        yield mysql_unit_state.assign_to_machine(mysql_machine_state)
 
1836
        yield self.assert_machine_assignments("mysql", [1])
 
1837
        wordpress_service_state = yield self.add_service_from_charm(
 
1838
                "wordpress")
 
1839
        wordpress_unit_state = yield wordpress_service_state.add_unit_state()
 
1840
        yield self.assertFailure(
 
1841
                wordpress_unit_state.assign_to_unused_machine(),
 
1842
                NoUnusedMachines)
 
1843
 
 
1844
    @inlineCallbacks
 
1845
    def test_watch_relations_processes_current_state(self):
 
1846
        """
 
1847
        The watch method returns only after processing initial state.
 
1848
 
 
1849
        Note the callback is only invoked if there are changes
 
1850
        requiring processing.
 
1851
        """
 
1852
        service_state = yield self.add_service("wordpress")
 
1853
        yield self.add_relation(
 
1854
                "rel-type", "global", [service_state, "name", "role"])
 
1855
 
 
1856
        results = []
 
1857
 
 
1858
        def callback(*args):
 
1859
            results.append(True)
 
1860
 
 
1861
        yield service_state.watch_relation_states(callback)
 
1862
        self.assertTrue(results)
 
1863
 
 
1864
    @inlineCallbacks
 
1865
    def test_watch_relations_when_being_created(self):
 
1866
        """
 
1867
        We can watch relations before we have any.
 
1868
        """
 
1869
        service_state = yield self.add_service("wordpress")
 
1870
 
 
1871
        wait_callback = [Deferred() for i in range(5)]
 
1872
        calls = []
 
1873
 
 
1874
        def watch_relations(old_relations, new_relations):
 
1875
            calls.append((old_relations, new_relations))
 
1876
            wait_callback[len(calls) - 1].callback(True)
 
1877
 
 
1878
        # Start watching
 
1879
        service_state.watch_relation_states(watch_relations)
 
1880
 
 
1881
        # Callback is still untouched
 
1882
        self.assertEquals(calls, [])
 
1883
 
 
1884
        # add a service relation and wait for the callback
 
1885
        relation_state = yield self.add_relation(
 
1886
                "rel-type", "global", [service_state, "name", "role"])
 
1887
        yield wait_callback[1]
 
1888
 
 
1889
        # verify the result
 
1890
        self.assertEquals(len(calls), 2)
 
1891
        old_relations, new_relations = calls[1]
 
1892
        self.assertFalse(old_relations)
 
1893
        self.assertEquals(new_relations[0].relation_role, "role")
 
1894
 
 
1895
        # add a new relation with the service assigned to it.
 
1896
        relation_state2 = yield self.add_relation(
 
1897
                "rel-type2", "global", [service_state, "app", "server"])
 
1898
        yield wait_callback[2]
 
1899
 
 
1900
        self.assertEquals(len(calls), 3)
 
1901
        old_relations, new_relations = calls[2]
 
1902
        self.assertEquals([r.internal_relation_id for r in old_relations],
 
1903
                                          [relation_state.internal_id])
 
1904
        self.assertEquals([r.internal_relation_id for r in new_relations],
 
1905
                                          [relation_state.internal_id,
 
1906
                                           relation_state2.internal_id])
 
1907
 
 
1908
    @inlineCallbacks
 
1909
    def test_watch_relations_may_defer(self):
 
1910
        """
 
1911
        The watch relations callback may return a deferred so that
 
1912
        it performs some its logic asynchronously. In this case, it must
 
1913
        not be called a second time before its postponed logic is finished
 
1914
        completely.
 
1915
        """
 
1916
        wait_callback = [Deferred() for i in range(5)]
 
1917
        finish_callback = [Deferred() for i in range(5)]
 
1918
 
 
1919
        calls = []
 
1920
 
 
1921
        def watch_relations(old_relations, new_relations):
 
1922
            calls.append((old_relations, new_relations))
 
1923
            wait_callback[len(calls) - 1].callback(True)
 
1924
            return finish_callback[len(calls) - 1]
 
1925
 
 
1926
        service_state = yield self.add_service("s-1")
 
1927
        service_state.watch_relation_states(watch_relations)
 
1928
 
 
1929
        # Shouldn't have any callbacks yet.
 
1930
        self.assertEquals(calls, [])
 
1931
 
 
1932
        # Assign to a relation.
 
1933
        yield self.add_relation("rel-type", "global",
 
1934
                                (service_state, "name", "role"))
 
1935
 
 
1936
        # Hold off until callback is started.
 
1937
        yield wait_callback[0]
 
1938
 
 
1939
        # Assign to another relation.
 
1940
        yield self.add_relation("rel-type", "global",
 
1941
                                (service_state, "name2", "role"))
 
1942
 
 
1943
        # Give a chance for something bad to happen.
 
1944
        yield self.sleep(0.3)
 
1945
 
 
1946
        # Ensure we still have a single call.
 
1947
        self.assertEquals(len(calls), 1)
 
1948
 
 
1949
        # Allow the first call to be completed, and wait on the
 
1950
        # next one.
 
1951
        finish_callback[0].callback(None)
 
1952
        yield wait_callback[1]
 
1953
        finish_callback[1].callback(None)
 
1954
 
 
1955
        # We should have the second change now.
 
1956
        self.assertEquals(len(calls), 2)
 
1957
 
 
1958
    @inlineCallbacks
 
1959
    def test_get_relation_endpoints_service_name(self):
 
1960
        """Test getting endpoints with descriptor ``<service name>``"""
 
1961
        yield self.add_service_from_charm("wordpress")
 
1962
        self.assertEqual(
 
1963
                (yield self.service_state_manager.get_relation_endpoints(
 
1964
                "wordpress")),
 
1965
                [RelationEndpoint("wordpress", "varnish", "cache", "client"),
 
1966
                RelationEndpoint("wordpress", "mysql", "db", "client"),
 
1967
                RelationEndpoint("wordpress", "http", "url", "server"),
 
1968
                RelationEndpoint("wordpress", "juju-info", "juju-info", "server")])
 
1969
        yield self.add_service_from_charm("riak")
 
1970
        self.assertEqual(
 
1971
                (yield self.service_state_manager.get_relation_endpoints(
 
1972
                "riak")),
 
1973
                [RelationEndpoint("riak", "http", "admin", "server"),
 
1974
                RelationEndpoint("riak", "http", "endpoint", "server"),
 
1975
                RelationEndpoint("riak", "riak", "ring", "peer"),
 
1976
                RelationEndpoint("riak", "juju-info", "juju-info", "server")])
 
1977
 
 
1978
    @inlineCallbacks
 
1979
    def test_get_relation_endpoints_service_name_relation_name(self):
 
1980
        """Test getting endpoints with ``<service name:relation name>``"""
 
1981
        yield self.add_service_from_charm("wordpress")
 
1982
        self.assertEqual(
 
1983
            (yield self.service_state_manager.get_relation_endpoints(
 
1984
                "wordpress:url")),
 
1985
            [RelationEndpoint("wordpress", "http", "url", "server"),
 
1986
             RelationEndpoint("wordpress", "juju-info", "juju-info", "server")])
 
1987
        self.assertEqual(
 
1988
            (yield self.service_state_manager.get_relation_endpoints(
 
1989
                "wordpress:db")),
 
1990
            [RelationEndpoint("wordpress", "mysql", "db", "client"),
 
1991
             RelationEndpoint("wordpress", "juju-info", "juju-info", "server")])
 
1992
        self.assertEqual(
 
1993
            (yield self.service_state_manager.get_relation_endpoints(
 
1994
                "wordpress:cache")),
 
1995
            [RelationEndpoint("wordpress", "varnish", "cache", "client"),
 
1996
             RelationEndpoint("wordpress", "juju-info", "juju-info", "server")])
 
1997
        yield self.add_service_from_charm("riak")
 
1998
        self.assertEqual(
 
1999
            (yield self.service_state_manager.get_relation_endpoints(
 
2000
                "riak:ring")),
 
2001
            [RelationEndpoint("riak", "riak", "ring", "peer"),
 
2002
             RelationEndpoint("riak", "juju-info", "juju-info", "server")])
 
2003
 
 
2004
    @inlineCallbacks
 
2005
    def test_descriptor_for_services_without_charms(self):
 
2006
        """Test with services that have no corresponding charms defined"""
 
2007
        yield self.add_service("nocharm")
 
2008
        # verify we get the implicit interface
 
2009
        self.assertEqual(
 
2010
                (yield self.service_state_manager.get_relation_endpoints(
 
2011
                        "nocharm")),
 
2012
                        [RelationEndpoint("nocharm",
 
2013
                        "juju-info", "juju-info", "server")])
 
2014
 
 
2015
        self.assertEqual(
 
2016
                (yield self.service_state_manager.get_relation_endpoints(
 
2017
                        "nocharm:nonsense")),
 
2018
                        [RelationEndpoint("nocharm",
 
2019
                        "juju-info", "juju-info", "server")])
 
2020
 
 
2021
    @inlineCallbacks
 
2022
    def test_descriptor_for_missing_service(self):
 
2023
        """Test with a service that is not in the topology"""
 
2024
        yield self.assertFailure(
 
2025
                self.service_state_manager.get_relation_endpoints("notadded"),
 
2026
                ServiceStateNotFound)
 
2027
 
 
2028
    @inlineCallbacks
 
2029
    def test_bad_descriptors(self):
 
2030
        """Test that the descriptors meet the minimum naming standards"""
 
2031
        yield self.assertFailure(
 
2032
                self.service_state_manager.get_relation_endpoints("a:b:c"),
 
2033
                BadDescriptor)
 
2034
        yield self.assertFailure(
 
2035
                self.service_state_manager.get_relation_endpoints(""),
 
2036
                BadDescriptor)
 
2037
 
 
2038
    @inlineCallbacks
 
2039
    def test_join_descriptors_service_name(self):
 
2040
        """Test descriptor of the form ``<service name>`"""
 
2041
        yield self.add_service_from_charm("wordpress")
 
2042
        yield self.add_service_from_charm("mysql")
 
2043
        self.assertEqual(
 
2044
                (yield self.service_state_manager.join_descriptors(
 
2045
                        "wordpress", "mysql")),
 
2046
                [(RelationEndpoint("wordpress", "mysql", "db", "client"),
 
2047
                  RelationEndpoint("mysql", "mysql", "server", "server"))])
 
2048
        # symmetric - note the pair has rotated
 
2049
        self.assertEqual(
 
2050
                (yield self.service_state_manager.join_descriptors(
 
2051
                        "mysql", "wordpress")),
 
2052
                [(RelationEndpoint("mysql", "mysql", "server", "server"),
 
2053
                  RelationEndpoint("wordpress", "mysql", "db", "client"))])
 
2054
        yield self.add_service_from_charm("varnish")
 
2055
        self.assertEqual(
 
2056
                (yield self.service_state_manager.join_descriptors(
 
2057
                        "wordpress", "varnish")),
 
2058
                [(RelationEndpoint("wordpress", "varnish", "cache", "client"),
 
2059
                  RelationEndpoint("varnish", "varnish", "webcache", "server"))])
 
2060
 
 
2061
    @inlineCallbacks
 
2062
    def test_join_descriptors_service_name_relation_name(self):
 
2063
        """Test joining descriptors ``<service name:relation name>``"""
 
2064
        yield self.add_service_from_charm("wordpress")
 
2065
        yield self.add_service_from_charm("mysql")
 
2066
        self.assertEqual(
 
2067
                (yield self.service_state_manager.join_descriptors(
 
2068
                        "wordpress:db", "mysql")),
 
2069
                [(RelationEndpoint("wordpress", "mysql", "db", "client"),
 
2070
                  RelationEndpoint("mysql", "mysql", "server", "server"))])
 
2071
        self.assertEqual(
 
2072
                (yield self.service_state_manager.join_descriptors(
 
2073
                        "mysql:server", "wordpress")),
 
2074
                [(RelationEndpoint("mysql", "mysql", "server", "server"),
 
2075
                  RelationEndpoint("wordpress", "mysql", "db", "client"))])
 
2076
        self.assertEqual(
 
2077
                (yield self.service_state_manager.join_descriptors(
 
2078
                        "mysql:server", "wordpress:db")),
 
2079
                [(RelationEndpoint("mysql", "mysql", "server", "server"),
 
2080
                  RelationEndpoint("wordpress", "mysql", "db", "client"))])
 
2081
 
 
2082
        yield self.add_service_from_charm("varnish")
 
2083
        self.assertEqual(
 
2084
                (yield self.service_state_manager.join_descriptors(
 
2085
                        "wordpress:cache", "varnish")),
 
2086
                [(RelationEndpoint("wordpress", "varnish", "cache", "client"),
 
2087
                  RelationEndpoint("varnish", "varnish", "webcache", "server"))])
 
2088
        self.assertEqual(
 
2089
                (yield self.service_state_manager.join_descriptors(
 
2090
                        "wordpress:cache", "varnish:webcache")),
 
2091
                [(RelationEndpoint("wordpress", "varnish", "cache", "client"),
 
2092
                  RelationEndpoint("varnish", "varnish", "webcache", "server"))])
 
2093
 
 
2094
    @inlineCallbacks
 
2095
    def test_join_peer_descriptors(self):
 
2096
        """Test joining of peer relation descriptors"""
 
2097
        yield self.add_service_from_charm("riak")
 
2098
        self.assertEqual(
 
2099
                (yield self.service_state_manager.join_descriptors(
 
2100
                        "riak", "riak")),
 
2101
                [(RelationEndpoint("riak", "riak", "ring", "peer"),
 
2102
                  RelationEndpoint("riak", "riak", "ring", "peer"))])
 
2103
        self.assertEqual(
 
2104
                (yield self.service_state_manager.join_descriptors(
 
2105
                        "riak:ring", "riak")),
 
2106
                [(RelationEndpoint("riak", "riak", "ring", "peer"),
 
2107
                  RelationEndpoint("riak", "riak", "ring", "peer"))])
 
2108
        self.assertEqual(
 
2109
                (yield self.service_state_manager.join_descriptors(
 
2110
                        "riak:ring", "riak:ring")),
 
2111
                [(RelationEndpoint("riak", "riak", "ring", "peer"),
 
2112
                  RelationEndpoint("riak", "riak", "ring", "peer"))])
 
2113
        self.assertEqual(
 
2114
                (yield self.service_state_manager.join_descriptors(
 
2115
                        "riak:no-ring", "riak:ring")),
 
2116
                [])
 
2117
 
 
2118
    @inlineCallbacks
 
2119
    def test_join_descriptors_no_common_relation(self):
 
2120
        """Test joining of descriptors that do not share a relation"""
 
2121
        yield self.add_service_from_charm("mysql")
 
2122
        yield self.add_service_from_charm("riak")
 
2123
        yield self.add_service_from_charm("wordpress")
 
2124
        yield self.add_service_from_charm("varnish")
 
2125
        self.assertEqual((yield self.service_state_manager.join_descriptors(
 
2126
            "mysql", "riak")), [])
 
2127
        self.assertEqual((yield self.service_state_manager.join_descriptors(
 
2128
            "mysql:server", "riak:ring")), [])
 
2129
        self.assertEqual((yield self.service_state_manager.join_descriptors(
 
2130
            "varnish", "mysql")), [])
 
2131
        self.assertEqual((yield self.service_state_manager.join_descriptors(
 
2132
            "riak:ring", "riak:admin")), [])
 
2133
        self.assertEqual((yield self.service_state_manager.join_descriptors(
 
2134
            "riak", "wordpress")), [])
 
2135
 
 
2136
    @inlineCallbacks
 
2137
    def test_join_descriptors_no_service_state(self):
 
2138
        """Test joining of nonexistent services"""
 
2139
        yield self.add_service_from_charm("wordpress")
 
2140
        yield self.assertFailure(self.service_state_manager.join_descriptors(
 
2141
            "wordpress", "nosuch"), ServiceStateNotFound)
 
2142
        yield self.assertFailure(self.service_state_manager.join_descriptors(
 
2143
            "notyet", "nosuch"), ServiceStateNotFound)
 
2144
 
 
2145
    @inlineCallbacks
 
2146
    def test_watch_services_initial_callback(self):
 
2147
        """Watch service processes initial state before returning.
 
2148
 
 
2149
        Note the callback is only executed if there is some meaningful state
 
2150
        change.
 
2151
        """
 
2152
        results = []
 
2153
 
 
2154
        def callback(*args):
 
2155
            results.append(True)
 
2156
 
 
2157
        yield self.service_state_manager.watch_service_states(callback)
 
2158
        yield self.add_service("wordpress")
 
2159
        yield self.poke_zk()
 
2160
        self.assertTrue(results)
 
2161
 
 
2162
    @inlineCallbacks
 
2163
    def test_watch_services_when_being_created(self):
 
2164
        """
 
2165
        It should be possible to start watching services even
 
2166
        before they are created.  In this case, the callback will
 
2167
        be made when it's actually introduced.
 
2168
        """
 
2169
        wait_callback = [Deferred() for i in range(10)]
 
2170
 
 
2171
        calls = []
 
2172
 
 
2173
        def watch_services(old_services, new_services):
 
2174
            calls.append((old_services, new_services))
 
2175
            wait_callback[len(calls) - 1].callback(True)
 
2176
 
 
2177
        # Start watching.
 
2178
        self.service_state_manager.watch_service_states(watch_services)
 
2179
 
 
2180
        # Callback is still untouched.
 
2181
        self.assertEquals(calls, [])
 
2182
 
 
2183
        # Add a service, and wait for callback.
 
2184
        yield self.add_service("wordpress")
 
2185
        yield wait_callback[0]
 
2186
 
 
2187
        # The first callback must have been fired, and it must have None
 
2188
        # as the first argument because that's the first service seen.
 
2189
        self.assertEquals(len(calls), 1)
 
2190
        old_services, new_services = calls[0]
 
2191
        self.assertEquals(old_services, set())
 
2192
        self.assertEquals(new_services, set(["wordpress"]))
 
2193
 
 
2194
        # Add a service again.
 
2195
        yield self.add_service("mysql")
 
2196
        yield wait_callback[1]
 
2197
 
 
2198
        # Now the watch callback must have been fired with two
 
2199
        # different service sets.  The old one, and the new one.
 
2200
        self.assertEquals(len(calls), 2)
 
2201
        old_services, new_services = calls[1]
 
2202
        self.assertEquals(old_services, set(["wordpress"]))
 
2203
        self.assertEquals(new_services, set(["mysql", "wordpress"]))
 
2204
 
 
2205
    @inlineCallbacks
 
2206
    def test_watch_services_may_defer(self):
 
2207
        """
 
2208
        The watch services callback may return a deferred so that it
 
2209
        performs some of its logic asynchronously. In this case, it
 
2210
        must not be called a second time before its postponed logic
 
2211
        is finished completely.
 
2212
        """
 
2213
        wait_callback = [Deferred() for i in range(10)]
 
2214
        finish_callback = [Deferred() for i in range(10)]
 
2215
 
 
2216
        calls = []
 
2217
 
 
2218
        def watch_services(old_services, new_services):
 
2219
            calls.append((old_services, new_services))
 
2220
            wait_callback[len(calls) - 1].callback(True)
 
2221
            return finish_callback[len(calls) - 1]
 
2222
 
 
2223
        # Start watching.
 
2224
        self.service_state_manager.watch_service_states(watch_services)
 
2225
 
 
2226
        # Create the service.
 
2227
        yield self.add_service("wordpress")
 
2228
 
 
2229
        # Hold off until callback is started.
 
2230
        yield wait_callback[0]
 
2231
 
 
2232
        # Add another service.
 
2233
        yield self.add_service("mysql")
 
2234
 
 
2235
        # Ensure we still have a single call.
 
2236
        self.assertEquals(len(calls), 1)
 
2237
 
 
2238
        # Allow the first call to be completed, and wait on the
 
2239
        # next one.
 
2240
        finish_callback[0].callback(None)
 
2241
        yield wait_callback[1]
 
2242
        finish_callback[1].callback(None)
 
2243
 
 
2244
        # We should have the second change now.
 
2245
        self.assertEquals(len(calls), 2)
 
2246
        old_services, new_services = calls[1]
 
2247
        self.assertEquals(old_services, set(["wordpress"]))
 
2248
        self.assertEquals(new_services, set(["mysql", "wordpress"]))
 
2249
 
 
2250
    @inlineCallbacks
 
2251
    def test_watch_services_with_changing_topology(self):
 
2252
        """
 
2253
        If the topology changes in an unrelated way, the services
 
2254
        watch callback should not be called with two equal
 
2255
        arguments.
 
2256
        """
 
2257
        wait_callback = [Deferred() for i in range(10)]
 
2258
 
 
2259
        calls = []
 
2260
 
 
2261
        def watch_services(old_services, new_services):
 
2262
            calls.append((old_services, new_services))
 
2263
            wait_callback[len(calls) - 1].callback(True)
 
2264
 
 
2265
        # Start watching.
 
2266
        self.service_state_manager.watch_service_states(watch_services)
 
2267
 
 
2268
        # Callback is still untouched.
 
2269
        self.assertEquals(calls, [])
 
2270
 
 
2271
        # Add a service, and wait for callback.
 
2272
        yield self.add_service("wordpress")
 
2273
        yield wait_callback[0]
 
2274
 
 
2275
        # Now change the topology in an unrelated way.
 
2276
        yield self.machine_state_manager.add_machine_state(
 
2277
                series_constraints)
 
2278
 
 
2279
        # Add a service again.
 
2280
        yield self.add_service("mysql")
 
2281
        yield wait_callback[1]
 
2282
 
 
2283
        # But it *shouldn't* have happened.
 
2284
        self.assertEquals(len(calls), 2)
 
2285
 
 
2286
    @inlineCallbacks
 
2287
    def test_watch_service_units_initial_callback(self):
 
2288
        """Watch service unit processes initial state before returning.
 
2289
 
 
2290
        Note the callback is only executed if there is some meaningful state
 
2291
        change.
 
2292
        """
 
2293
        results = []
 
2294
 
 
2295
        def callback(*args):
 
2296
            results.append(True)
 
2297
 
 
2298
        service_state = yield self.add_service("wordpress")
 
2299
        yield service_state.watch_service_unit_states(callback)
 
2300
        yield service_state.add_unit_state()
 
2301
        yield self.poke_zk()
 
2302
        self.assertTrue(results)
 
2303
 
 
2304
    @inlineCallbacks
 
2305
    def test_watch_service_units_when_being_created(self):
 
2306
        """
 
2307
        It should be possible to start watching service units even
 
2308
        before they are created.  In this case, the callback will be
 
2309
        made when it's actually introduced.
 
2310
        """
 
2311
        wait_callback = [Deferred() for i in range(10)]
 
2312
 
 
2313
        calls = []
 
2314
 
 
2315
        def watch_service_units(old_service_units, new_service_units):
 
2316
            calls.append((old_service_units, new_service_units))
 
2317
            wait_callback[len(calls) - 1].callback(True)
 
2318
 
 
2319
        # Start watching.
 
2320
        service_state = yield self.add_service("wordpress")
 
2321
        service_state.watch_service_unit_states(watch_service_units)
 
2322
 
 
2323
        # Callback is still untouched.
 
2324
        self.assertEquals(calls, [])
 
2325
 
 
2326
        # Add a service unit, and wait for callback.
 
2327
        yield service_state.add_unit_state()
 
2328
 
 
2329
        yield wait_callback[0]
 
2330
 
 
2331
        # The first callback must have been fired, and it must have None
 
2332
        # as the first argument because that's the first service seen.
 
2333
        self.assertEquals(len(calls), 1)
 
2334
        old_service_units, new_service_units = calls[0]
 
2335
        self.assertEquals(old_service_units, set())
 
2336
        self.assertEquals(new_service_units, set(["wordpress/0"]))
 
2337
 
 
2338
        # Add another service unit.
 
2339
        yield service_state.add_unit_state()
 
2340
        yield wait_callback[1]
 
2341
 
 
2342
        # Now the watch callback must have been fired with two
 
2343
        # different service sets.  The old one, and the new one.
 
2344
        self.assertEquals(len(calls), 2)
 
2345
        old_service_units, new_service_units = calls[1]
 
2346
        self.assertEquals(old_service_units, set(["wordpress/0"]))
 
2347
        self.assertEquals(new_service_units, set(["wordpress/0",
 
2348
                                                  "wordpress/1"]))
 
2349
 
 
2350
    @inlineCallbacks
 
2351
    def test_watch_service_units_may_defer(self):
 
2352
        """
 
2353
        The watch service units callback may return a deferred so that
 
2354
        it performs some of its logic asynchronously.  In this case,
 
2355
        it must not be called a second time before its postponed logic
 
2356
        is finished completely.
 
2357
        """
 
2358
        wait_callback = [Deferred() for i in range(10)]
 
2359
        finish_callback = [Deferred() for i in range(10)]
 
2360
 
 
2361
        calls = []
 
2362
 
 
2363
        def watch_service_units(old_service_units, new_service_units):
 
2364
            calls.append((old_service_units, new_service_units))
 
2365
            wait_callback[len(calls) - 1].callback(True)
 
2366
            return finish_callback[len(calls) - 1]
 
2367
 
 
2368
        # Start watching.
 
2369
        service_state = yield self.add_service("wordpress")
 
2370
        service_state.watch_service_unit_states(watch_service_units)
 
2371
 
 
2372
        # Create the service unit.
 
2373
        yield service_state.add_unit_state()
 
2374
 
 
2375
        # Hold off until callback is started.
 
2376
        yield wait_callback[0]
 
2377
 
 
2378
        # Add another service unit.
 
2379
        yield service_state.add_unit_state()
 
2380
 
 
2381
        # Ensure we still have a single call.
 
2382
        self.assertEquals(len(calls), 1)
 
2383
 
 
2384
        # Allow the first call to be completed, and wait on the
 
2385
        # next one.
 
2386
        finish_callback[0].callback(None)
 
2387
        yield wait_callback[1]
 
2388
        finish_callback[1].callback(None)
 
2389
 
 
2390
        # We should have the second change now.
 
2391
        self.assertEquals(len(calls), 2)
 
2392
        old_service_units, new_service_units = calls[1]
 
2393
        self.assertEquals(old_service_units, set(["wordpress/0"]))
 
2394
        self.assertEquals(
 
2395
                new_service_units, set(["wordpress/0", "wordpress/1"]))
 
2396
 
 
2397
    @inlineCallbacks
 
2398
    def test_watch_service_units_with_changing_topology(self):
 
2399
        """
 
2400
        If the topology changes in an unrelated way, the services
 
2401
        watch callback should not be called with two equal
 
2402
        arguments.
 
2403
        """
 
2404
        wait_callback = [Deferred() for i in range(10)]
 
2405
 
 
2406
        calls = []
 
2407
 
 
2408
        def watch_service_units(old_service_units, new_service_units):
 
2409
            calls.append((old_service_units, new_service_units))
 
2410
            wait_callback[len(calls) - 1].callback(True)
 
2411
 
 
2412
        # Start watching.
 
2413
        service_state = yield self.add_service("wordpress")
 
2414
        service_state.watch_service_unit_states(watch_service_units)
 
2415
 
 
2416
        # Callback is still untouched.
 
2417
        self.assertEquals(calls, [])
 
2418
 
 
2419
        # Add a service, and wait for callback.
 
2420
        yield service_state.add_unit_state()
 
2421
        yield wait_callback[0]
 
2422
 
 
2423
        # Now change the topology in an unrelated way.
 
2424
        yield self.machine_state_manager.add_machine_state(
 
2425
                series_constraints)
 
2426
 
 
2427
        # Add a service again.
 
2428
        yield service_state.add_unit_state()
 
2429
        yield wait_callback[1]
 
2430
 
 
2431
        # But it *shouldn't* have happened.
 
2432
        self.assertEquals(len(calls), 2)
 
2433
 
 
2434
    @inlineCallbacks
 
2435
    def test_service_config_get_set(self):
 
2436
        """Validate that we can set and get service config options."""
 
2437
        wordpress = yield self.add_service_from_charm("wordpress")
 
2438
 
 
2439
        # attempt to get the initialized service state
 
2440
        config = yield wordpress.get_config()
 
2441
 
 
2442
        # the initial state is empty
 
2443
        self.assertEqual(config, {"blog-title": "My Title"})
 
2444
 
 
2445
        # behaves as a normal dict
 
2446
        self.assertRaises(KeyError, config.__getitem__, "missing")
 
2447
 
 
2448
        # various ways to set state
 
2449
        config.update(dict(alpha="beta", one="two"))
 
2450
        config["another"] = "value"
 
2451
 
 
2452
        # write the values
 
2453
        yield config.write()
 
2454
 
 
2455
        # we should be able to read the config and see the same values
 
2456
        # (in this case it would be the cached object)
 
2457
        config2 = yield wordpress.get_config()
 
2458
        self.assertEqual(config2, {"alpha": "beta",
 
2459
                                   "one": "two",
 
2460
                                   "another": "value",
 
2461
                                   "blog-title": "My Title"})
 
2462
 
 
2463
        # now set a non-string value and recover it
 
2464
        config2["number"] = 1
 
2465
        config2["one"] = None
 
2466
        yield config2.write()
 
2467
 
 
2468
        yield config.read()
 
2469
        self.assertEquals(config["number"], 1)
 
2470
        self.assertEquals(config["one"], None)
 
2471
 
 
2472
    @inlineCallbacks
 
2473
    def test_service_config_get_returns_new(self):
 
2474
        """Validate that we can set and get service config options."""
 
2475
        wordpress = yield self.add_service_from_charm("wordpress")
 
2476
 
 
2477
        # attempt to get the initialized service state
 
2478
        config = yield wordpress.get_config()
 
2479
        config.update({"foo": "bar"})
 
2480
        # Defaults come through
 
2481
        self.assertEqual(config, {"foo": "bar", "blog-title": "My Title"})
 
2482
 
 
2483
        config2 = yield wordpress.get_config()
 
2484
        self.assertEqual(config2, {"blog-title": "My Title"})
 
2485
 
 
2486
        yield config.write()
 
2487
        self.assertEqual(config, {"foo": "bar", "blog-title": "My Title"})
 
2488
 
 
2489
        # Config2 is still empty (a different YAML State), with charm defaults.
 
2490
        self.assertEqual(config2, {"blog-title": "My Title"})
 
2491
 
 
2492
        yield config2.read()
 
2493
        self.assertEqual(config, {"foo": "bar", "blog-title": "My Title"})
 
2494
 
 
2495
        # The default was never written to storage.
 
2496
        data, stat = yield self.client.get(
 
2497
                "/services/%s/config" % wordpress.internal_id)
 
2498
        self.assertEqual(yaml.load(data), {"foo": "bar"})
 
2499
 
 
2500
    @inlineCallbacks
 
2501
    def test_get_charm_state(self):
 
2502
        wordpress = yield self.add_service_from_charm("wordpress")
 
2503
        charm = yield wordpress.get_charm_state()
 
2504
 
 
2505
        self.assertEqual(charm.name, "wordpress")
 
2506
        metadata = yield charm.get_metadata()
 
2507
        self.assertEqual(metadata.summary, "Blog engine")
2528
2508
 
2529
2509
 
2530
2510
class ExposedFlagTest(ServiceStateManagerTestBase):
2531
2511
 
2532
 
        @inlineCallbacks
2533
 
        def test_set_and_clear_exposed_flag(self):
2534
 
                """An exposed flag can be set on a service."""
2535
 
 
2536
 
                # Defaults to false
2537
 
                service_state = yield self.service_state_manager.add_service_state(
2538
 
                        "wordpress", self.charm_state, dummy_constraints)
2539
 
                exposed_flag = yield service_state.get_exposed_flag()
2540
 
                self.assertEqual(exposed_flag, False)
2541
 
 
2542
 
                # Can be set
2543
 
                yield service_state.set_exposed_flag()
2544
 
                exposed_flag = yield service_state.get_exposed_flag()
2545
 
                self.assertEqual(exposed_flag, True)
2546
 
 
2547
 
                # Can be set multiple times
2548
 
                yield service_state.set_exposed_flag()
2549
 
                exposed_flag = yield service_state.get_exposed_flag()
2550
 
                self.assertEqual(exposed_flag, True)
2551
 
 
2552
 
                # Can be cleared
2553
 
                yield service_state.clear_exposed_flag()
2554
 
                exposed_flag = yield service_state.get_exposed_flag()
2555
 
                self.assertEqual(exposed_flag, False)
2556
 
 
2557
 
                # Can be cleared multiple times
2558
 
                yield service_state.clear_exposed_flag()
2559
 
                exposed_flag = yield service_state.get_exposed_flag()
2560
 
                self.assertEqual(exposed_flag, False)
2561
 
 
2562
 
        @inlineCallbacks
2563
 
        def test_watch_exposed_flag(self):
2564
 
                """An exposed watch is setup on a permanent basis."""
2565
 
                service_state = yield self.add_service("wordpress")
2566
 
 
2567
 
                results = []
2568
 
 
2569
 
                def callback(value):
2570
 
                        results.append(value)
2571
 
 
2572
 
                yield service_state.set_exposed_flag()
2573
 
 
2574
 
                # verify that the current state is processed before returning
2575
 
                yield service_state.watch_exposed_flag(callback)
2576
 
                yield service_state.clear_exposed_flag()
2577
 
                yield service_state.set_exposed_flag()
2578
 
                yield service_state.clear_exposed_flag()
2579
 
                yield service_state.clear_exposed_flag()  # should be ignored
2580
 
                yield service_state.set_exposed_flag()
2581
 
 
2582
 
                self.assertEqual(
2583
 
                        (yield service_state.get_exposed_flag()),
2584
 
                        True)
2585
 
                self.assertEqual(results, [True, False, True, False, True])
2586
 
 
2587
 
        @inlineCallbacks
2588
 
        def test_stop_watch_exposed_flag(self):
2589
 
                """The watch is setup on a permanent basis, but can be stopped.
2590
 
 
2591
 
                The callback can raise StopWatcher at any time to stop the
2592
 
                watch.
2593
 
                """
2594
 
                service_state = yield self.add_service("wordpress")
2595
 
                results = []
2596
 
 
2597
 
                def callback(value):
2598
 
                        results.append(value)
2599
 
                        if len(results) == 2:
2600
 
                                raise StopWatcher()
2601
 
                        if len(results) == 4:
2602
 
                                raise StopWatcher()
2603
 
 
2604
 
                yield service_state.watch_exposed_flag(callback)
2605
 
                yield service_state.set_exposed_flag()
2606
 
                # two sets in a row do not retrigger callback since no change
2607
 
                # in flag status
2608
 
                yield service_state.set_exposed_flag()
2609
 
                yield service_state.clear_exposed_flag()
2610
 
 
2611
 
                # no callback now, because StopWatcher was just raised
2612
 
                yield service_state.set_exposed_flag()
2613
 
 
2614
 
                # then setup watch again
2615
 
                yield service_state.watch_exposed_flag(callback)
2616
 
                yield service_state.clear_exposed_flag()
2617
 
 
2618
 
                # no callbacks for these two lines, because StopWatcher was
2619
 
                # already raised
2620
 
                yield service_state.set_exposed_flag()
2621
 
                yield service_state.clear_exposed_flag()
2622
 
 
2623
 
                self.assertEqual(
2624
 
                        (yield service_state.get_exposed_flag()), False)
2625
 
                self.assertEqual(results, [False, True, True, False])
2626
 
 
2627
 
        @inlineCallbacks
2628
 
        def test_watch_exposed_flag_waits_on_slow_callbacks(self):
2629
 
                """Verify that a slow watch callback is still invoked serially."""
2630
 
 
2631
 
                service_state = yield self.add_service("wordpress")
2632
 
 
2633
 
                callbacks = [Deferred() for i in range(3)]
2634
 
                before = []  # values seen before callback in `cb_watch`
2635
 
                after = []   # and after
2636
 
 
2637
 
                @inlineCallbacks
2638
 
                def cb_watch(value):
2639
 
                        before.append(value)
2640
 
                        yield callbacks[len(before) - 1]
2641
 
                        after.append((yield service_state.get_exposed_flag()))
2642
 
 
2643
 
                yield service_state.set_exposed_flag()
2644
 
 
2645
 
                # Need to let first callback be completed, otherwise will wait
2646
 
                # forever in watch_exposed_flag. This is because `cb_watch` is
2647
 
                # initially called in the setup of the watch
2648
 
                callbacks[0].callback(True)
2649
 
 
2650
 
                yield service_state.watch_exposed_flag(cb_watch)
2651
 
                self.assertEqual(before, [True])
2652
 
                self.assertEqual(after, [True])
2653
 
 
2654
 
                # Go through the watch again, verifying that it is waiting on
2655
 
                # `callbacks[1]`
2656
 
                yield service_state.clear_exposed_flag()
2657
 
                yield self.poke_zk()
2658
 
                self.assertEqual(before, [True, False])
2659
 
                self.assertEqual(after, [True])
2660
 
 
2661
 
                # Now let `cb_watch` finish
2662
 
                callbacks[1].callback(True)
2663
 
                yield self.poke_zk()
2664
 
 
2665
 
                # Go through another watch cycle
2666
 
                yield service_state.set_exposed_flag()
2667
 
                yield self.poke_zk()
2668
 
 
2669
 
                # Verify results, still haven't advanced through `callbacks[2]`
2670
 
                self.assertEqual(before, [True, False, True])
2671
 
                self.assertEqual(after, [True, False])
2672
 
 
2673
 
                # Now let it go through, verifying that `before` hasn't
2674
 
                # changed, but `after` has now updated
2675
 
                callbacks[2].callback(True)
2676
 
                yield self.poke_zk()
2677
 
                self.assertEqual(before, [True, False, True])
2678
 
                self.assertEqual(after, [True, False, True])
 
2512
    @inlineCallbacks
 
2513
    def test_set_and_clear_exposed_flag(self):
 
2514
        """An exposed flag can be set on a service."""
 
2515
 
 
2516
        # Defaults to false
 
2517
        service_state = yield self.service_state_manager.add_service_state(
 
2518
                "wordpress", self.charm_state, dummy_constraints)
 
2519
        exposed_flag = yield service_state.get_exposed_flag()
 
2520
        self.assertEqual(exposed_flag, False)
 
2521
 
 
2522
        # Can be set
 
2523
        yield service_state.set_exposed_flag()
 
2524
        exposed_flag = yield service_state.get_exposed_flag()
 
2525
        self.assertEqual(exposed_flag, True)
 
2526
 
 
2527
        # Can be set multiple times
 
2528
        yield service_state.set_exposed_flag()
 
2529
        exposed_flag = yield service_state.get_exposed_flag()
 
2530
        self.assertEqual(exposed_flag, True)
 
2531
 
 
2532
        # Can be cleared
 
2533
        yield service_state.clear_exposed_flag()
 
2534
        exposed_flag = yield service_state.get_exposed_flag()
 
2535
        self.assertEqual(exposed_flag, False)
 
2536
 
 
2537
        # Can be cleared multiple times
 
2538
        yield service_state.clear_exposed_flag()
 
2539
        exposed_flag = yield service_state.get_exposed_flag()
 
2540
        self.assertEqual(exposed_flag, False)
 
2541
 
 
2542
    @inlineCallbacks
 
2543
    def test_watch_exposed_flag(self):
 
2544
        """An exposed watch is setup on a permanent basis."""
 
2545
        service_state = yield self.add_service("wordpress")
 
2546
 
 
2547
        results = []
 
2548
 
 
2549
        def callback(value):
 
2550
            results.append(value)
 
2551
 
 
2552
        yield service_state.set_exposed_flag()
 
2553
 
 
2554
        # verify that the current state is processed before returning
 
2555
        yield service_state.watch_exposed_flag(callback)
 
2556
        yield service_state.clear_exposed_flag()
 
2557
        yield service_state.set_exposed_flag()
 
2558
        yield service_state.clear_exposed_flag()
 
2559
        yield service_state.clear_exposed_flag()  # should be ignored
 
2560
        yield service_state.set_exposed_flag()
 
2561
 
 
2562
        self.assertEqual((yield service_state.get_exposed_flag()),
 
2563
                         True)
 
2564
        self.assertEqual(results, [True, False, True, False, True])
 
2565
 
 
2566
    @inlineCallbacks
 
2567
    def test_stop_watch_exposed_flag(self):
 
2568
        """The watch is setup on a permanent basis, but can be stopped.
 
2569
 
 
2570
        The callback can raise StopWatcher at any time to stop the
 
2571
        watch.
 
2572
        """
 
2573
        service_state = yield self.add_service("wordpress")
 
2574
        results = []
 
2575
 
 
2576
        def callback(value):
 
2577
            results.append(value)
 
2578
            if len(results) == 2:
 
2579
                raise StopWatcher()
 
2580
            if len(results) == 4:
 
2581
                raise StopWatcher()
 
2582
 
 
2583
        yield service_state.watch_exposed_flag(callback)
 
2584
        yield service_state.set_exposed_flag()
 
2585
        # two sets in a row do not retrigger callback since no change
 
2586
        # in flag status
 
2587
        yield service_state.set_exposed_flag()
 
2588
        yield service_state.clear_exposed_flag()
 
2589
 
 
2590
        # no callback now, because StopWatcher was just raised
 
2591
        yield service_state.set_exposed_flag()
 
2592
 
 
2593
        # then setup watch again
 
2594
        yield service_state.watch_exposed_flag(callback)
 
2595
        yield service_state.clear_exposed_flag()
 
2596
 
 
2597
        # no callbacks for these two lines, because StopWatcher was
 
2598
        # already raised
 
2599
        yield service_state.set_exposed_flag()
 
2600
        yield service_state.clear_exposed_flag()
 
2601
 
 
2602
        self.assertEqual(
 
2603
                (yield service_state.get_exposed_flag()), False)
 
2604
        self.assertEqual(results, [False, True, True, False])
 
2605
 
 
2606
    @inlineCallbacks
 
2607
    def test_watch_exposed_flag_waits_on_slow_callbacks(self):
 
2608
        """Verify that a slow watch callback is still invoked serially."""
 
2609
 
 
2610
        service_state = yield self.add_service("wordpress")
 
2611
 
 
2612
        callbacks = [Deferred() for i in range(3)]
 
2613
        before = []  # values seen before callback in `cb_watch`
 
2614
        after = []   # and after
 
2615
 
 
2616
        @inlineCallbacks
 
2617
        def cb_watch(value):
 
2618
            before.append(value)
 
2619
            yield callbacks[len(before) - 1]
 
2620
            after.append((yield service_state.get_exposed_flag()))
 
2621
 
 
2622
        yield service_state.set_exposed_flag()
 
2623
 
 
2624
        # Need to let first callback be completed, otherwise will wait
 
2625
        # forever in watch_exposed_flag. This is because `cb_watch` is
 
2626
        # initially called in the setup of the watch
 
2627
        callbacks[0].callback(True)
 
2628
 
 
2629
        yield service_state.watch_exposed_flag(cb_watch)
 
2630
        self.assertEqual(before, [True])
 
2631
        self.assertEqual(after, [True])
 
2632
 
 
2633
        # Go through the watch again, verifying that it is waiting on
 
2634
        # `callbacks[1]`
 
2635
        yield service_state.clear_exposed_flag()
 
2636
        yield self.poke_zk()
 
2637
        self.assertEqual(before, [True, False])
 
2638
        self.assertEqual(after, [True])
 
2639
 
 
2640
        # Now let `cb_watch` finish
 
2641
        callbacks[1].callback(True)
 
2642
        yield self.poke_zk()
 
2643
 
 
2644
        # Go through another watch cycle
 
2645
        yield service_state.set_exposed_flag()
 
2646
        yield self.poke_zk()
 
2647
 
 
2648
        # Verify results, still haven't advanced through `callbacks[2]`
 
2649
        self.assertEqual(before, [True, False, True])
 
2650
        self.assertEqual(after, [True, False])
 
2651
 
 
2652
        # Now let it go through, verifying that `before` hasn't
 
2653
        # changed, but `after` has now updated
 
2654
        callbacks[2].callback(True)
 
2655
        yield self.poke_zk()
 
2656
        self.assertEqual(before, [True, False, True])
 
2657
        self.assertEqual(after, [True, False, True])
2679
2658
 
2680
2659
 
2681
2660
class PortsTest(ServiceStateManagerTestBase):
2682
2661
 
2683
 
        @inlineCallbacks
2684
 
        def test_watch_config_options(self):
2685
 
                """Verify callback trigger on config options modification"""
2686
 
 
2687
 
                service_state = yield self.service_state_manager.add_service_state(
2688
 
                        "wordpress", self.charm_state, dummy_constraints)
2689
 
                results = []
2690
 
 
2691
 
                def callback(value):
2692
 
                        results.append(value)
2693
 
 
2694
 
                yield service_state.watch_config_state(callback)
2695
 
                config = yield service_state.get_config()
2696
 
                config["alpha"] = "beta"
2697
 
                yield config.write()
2698
 
 
2699
 
                yield self.poke_zk()
2700
 
                self.assertIdentical(results.pop(0), True)
2701
 
                self.assertIdentical(results.pop(0).type_name, "changed")
2702
 
 
2703
 
                # and changing it again should trigger the callback again
2704
 
                config["gamma"] = "delta"
2705
 
                yield config.write()
2706
 
 
2707
 
                yield self.poke_zk()
2708
 
                self.assertEqual(len(results), 1)
2709
 
                self.assertIdentical(results.pop(0).type_name, "changed")
2710
 
 
2711
 
        @inlineCallbacks
2712
 
        def test_get_open_ports(self):
2713
 
                """Verify introspection and that the ports changes are immediate."""
2714
 
                service_state = yield self.add_service("wordpress")
2715
 
                unit_state = yield service_state.add_unit_state()
2716
 
 
2717
 
                # verify no open ports before activity
2718
 
                self.assertEqual((yield unit_state.get_open_ports()), [])
2719
 
 
2720
 
                # then open_port, close_port
2721
 
                yield unit_state.open_port(80, "tcp")
2722
 
                self.assertEqual(
2723
 
                        (yield unit_state.get_open_ports()),
2724
 
                        [{"port": 80, "proto": "tcp"}])
2725
 
 
2726
 
                yield unit_state.open_port(53, "udp")
2727
 
                self.assertEqual(
2728
 
                        (yield unit_state.get_open_ports()),
2729
 
                        [{"port": 80, "proto": "tcp"},
2730
 
                         {"port": 53, "proto": "udp"}])
2731
 
 
2732
 
                yield unit_state.open_port(53, "tcp")
2733
 
                self.assertEqual(
2734
 
                        (yield unit_state.get_open_ports()),
2735
 
                        [{"port": 80, "proto": "tcp"},
2736
 
                         {"port": 53, "proto": "udp"},
2737
 
                         {"port": 53, "proto": "tcp"}])
2738
 
 
2739
 
                yield unit_state.open_port(443, "tcp")
2740
 
                self.assertEqual(
2741
 
                        (yield unit_state.get_open_ports()),
2742
 
                        [{"port": 80, "proto": "tcp"},
2743
 
                         {"port": 53, "proto": "udp"},
2744
 
                         {"port": 53, "proto": "tcp"},
2745
 
                         {"port": 443, "proto": "tcp"}])
2746
 
 
2747
 
                yield unit_state.close_port(80, "tcp")
2748
 
                self.assertEqual(
2749
 
                        (yield unit_state.get_open_ports()),
2750
 
                        [{"port": 53, "proto": "udp"},
2751
 
                         {"port": 53, "proto": "tcp"},
2752
 
                         {"port": 443, "proto": "tcp"}])
2753
 
 
2754
 
        @inlineCallbacks
2755
 
        def test_close_open_port(self):
2756
 
                """Verify closing an unopened port, then actually opening it, works."""
2757
 
                service_state = yield self.add_service("wordpress")
2758
 
                unit_state = yield service_state.add_unit_state()
2759
 
                unit_name = unit_state.unit_name
2760
 
 
2761
 
                yield unit_state.close_port(80, "tcp")
2762
 
                self.assertEqual(
2763
 
                        (yield unit_state.get_open_ports()),
2764
 
                        [])
2765
 
 
2766
 
                yield unit_state.open_port(80, "tcp")
2767
 
                self.assertEqual(
2768
 
                        (yield unit_state.get_open_ports()),
2769
 
                        [{"port": 80, "proto": "tcp"}])
2770
 
 
2771
 
        @inlineCallbacks
2772
 
        def test_open_ports_znode_representation(self):
2773
 
                """Verify the specific representation of open ports in ZK."""
2774
 
                service_state = yield self.add_service("wordpress")
2775
 
                unit_state = yield service_state.add_unit_state()
2776
 
                ports_path = "/units/unit-0000000000/ports"
2777
 
 
2778
 
                # verify no node exists before activity
2779
 
                self.assertFailure(
2780
 
                        self.client.get(ports_path), zookeeper.NoNodeException)
2781
 
 
2782
 
                # verify representation format after open_port, close_port
2783
 
                yield unit_state.open_port(80, "tcp")
2784
 
                content, stat = yield self.client.get(ports_path)
2785
 
                self.assertEquals(
2786
 
                        yaml.load(content),
2787
 
                        {"open": [{"port": 80, "proto": "tcp"}]})
2788
 
 
2789
 
                yield unit_state.open_port(53, "udp")
2790
 
                content, stat = yield self.client.get(ports_path)
2791
 
                self.assertEquals(
2792
 
                        yaml.load(content),
2793
 
                        {"open": [{"port": 80, "proto": "tcp"},
2794
 
                                          {"port": 53, "proto": "udp"}]})
2795
 
 
2796
 
                yield unit_state.open_port(443, "tcp")
2797
 
                content, stat = yield self.client.get(ports_path)
2798
 
                self.assertEquals(
2799
 
                        yaml.load(content),
2800
 
                        {"open": [{"port": 80, "proto": "tcp"},
2801
 
                                          {"port": 53, "proto": "udp"},
2802
 
                                          {"port": 443, "proto": "tcp"}]})
2803
 
 
2804
 
                yield unit_state.close_port(80, "tcp")
2805
 
                content, stat = yield self.client.get(ports_path)
2806
 
                self.assertEquals(
2807
 
                        yaml.load(content),
2808
 
                        {"open": [{"port": 53, "proto": "udp"},
2809
 
                                          {"port": 443, "proto": "tcp"}]})
2810
 
 
2811
 
        @inlineCallbacks
2812
 
        def test_watch_ports(self):
2813
 
                """An open ports watch notifies of presence and changes."""
2814
 
                service_state = yield self.add_service("wordpress")
2815
 
                unit_state = yield service_state.add_unit_state()
2816
 
                yield unit_state.open_port(80, "tcp")
2817
 
                yield unit_state.open_port(53, "udp")
2818
 
                yield unit_state.open_port(443, "tcp")
2819
 
 
2820
 
                results = []
2821
 
 
2822
 
                def callback(value):
2823
 
                        results.append(value)
2824
 
 
2825
 
                # set up a one-time watch
2826
 
                unit_state.watch_ports(callback)
2827
 
 
2828
 
                # do two actions
2829
 
                yield unit_state.close_port(80, "tcp")
2830
 
                yield unit_state.open_port(22, "tcp")
2831
 
 
2832
 
                # but see just the callback with one changed event, plus initial setup
2833
 
                yield self.poke_zk()
2834
 
                self.assertEqual(len(results), 3)
2835
 
                self.assertEqual(results.pop(0), True)
2836
 
                self.assertEqual(results.pop().type_name, "changed")
2837
 
                self.assertEqual(results.pop().type_name, "changed")
2838
 
                self.assertEqual(
2839
 
                        (yield unit_state.get_open_ports()),
2840
 
                        [{'port': 53, 'proto': 'udp'},
2841
 
                         {'port': 443, 'proto': 'tcp'},
2842
 
                         {'port': 22, 'proto': 'tcp'}])
2843
 
 
2844
 
        @inlineCallbacks
2845
 
        def test_stop_watch_ports(self):
2846
 
                """An exposed watch can be instituted on a permanent basis.
2847
 
 
2848
 
                However the callback can raise StopWatcher any time to stop the watch.
2849
 
                """
2850
 
                service_state = yield self.add_service("wordpress")
2851
 
                unit_state = yield service_state.add_unit_state()
2852
 
                yield unit_state.open_port(80, "tcp")
2853
 
 
2854
 
                results = []
2855
 
 
2856
 
                def callback(value):
2857
 
                        results.append(value)
2858
 
                        if len(results) == 1:
2859
 
                                raise StopWatcher()
2860
 
                        if len(results) == 4:
2861
 
                                raise StopWatcher()
2862
 
 
2863
 
                unit_state.watch_ports(callback)
2864
 
                yield unit_state.close_port(80, "tcp")
2865
 
                yield self.poke_zk()
2866
 
                self.assertEqual(len(results), 1)
2867
 
                self.assertEqual(results[0], True)
2868
 
 
2869
 
                unit_state.watch_ports(callback)
2870
 
                yield unit_state.open_port(53, "udp")
2871
 
                yield self.poke_zk()
2872
 
                yield self.sleep(0.1)
2873
 
                self.assertEqual(len(results), 3)
2874
 
                self.assertEqual(results[0], True)
2875
 
                self.assertEqual(results[1], True)
2876
 
                self.assertEqual(results[2].type_name, "changed")
2877
 
                self.assertEqual(
2878
 
                        (yield unit_state.get_open_ports()),
2879
 
                        [{'port': 53, 'proto': 'udp'}])
2880
 
 
2881
 
        @inlineCallbacks
2882
 
        def test_watch_ports_slow_callbacks(self):
2883
 
                """A slow watch callback is still invoked serially."""
2884
 
                unit_state = yield self.get_unit_state()
2885
 
 
2886
 
                callbacks = [Deferred() for i in range(5)]
2887
 
                results = []
2888
 
                contents = []
2889
 
 
2890
 
                @inlineCallbacks
2891
 
                def watch(value):
2892
 
                        results.append(value)
2893
 
                        yield callbacks[len(results) - 1]
2894
 
                        contents.append((yield unit_state.get_open_ports()))
2895
 
 
2896
 
                callbacks[0].callback(True)
2897
 
                yield unit_state.watch_ports(watch)
2898
 
 
2899
 
                # These get collapsed into a single event
2900
 
                yield unit_state.open_port(80, "tcp")
2901
 
                yield unit_state.open_port(53, "udp")
2902
 
                yield unit_state.open_port(443, "tcp")
2903
 
                yield unit_state.close_port(80, "tcp")
2904
 
                yield self.poke_zk()
2905
 
 
2906
 
                # Verify the callback hasn't completed
2907
 
                self.assertEqual(len(results), 2)
2908
 
                self.assertEqual(len(contents), 1)
2909
 
 
2910
 
                # Let it finish
2911
 
                callbacks[1].callback(True)
2912
 
                yield self.poke_zk()
2913
 
 
2914
 
                # Verify the callback hasn't completed
2915
 
                self.assertEqual(len(contents), 2)
2916
 
 
2917
 
                callbacks[2].callback(True)
2918
 
                yield self.poke_zk()
2919
 
 
2920
 
                # Verify values.
2921
 
                self.assertEqual(len(contents), 3)
2922
 
                self.assertEqual(results[-1].type_name, "changed")
2923
 
                self.assertEqual(contents[-1], [
2924
 
                                {'port': 53, 'proto': 'udp'}, {'port': 443, 'proto': 'tcp'}])
2925
 
                yield self.poke_zk()
2926
 
 
2927
 
        def test_parse_service_name(self):
2928
 
                self.assertEqual(parse_service_name("wordpress/0"), "wordpress")
2929
 
                self.assertEqual(parse_service_name("myblog/1"), "myblog")
2930
 
                self.assertRaises(ValueError, parse_service_name, "invalid")
2931
 
                self.assertRaises(ValueError, parse_service_name, None)
 
2662
    @inlineCallbacks
 
2663
    def test_watch_config_options(self):
 
2664
        """Verify callback trigger on config options modification"""
 
2665
 
 
2666
        service_state = yield self.service_state_manager.add_service_state(
 
2667
                "wordpress", self.charm_state, dummy_constraints)
 
2668
        results = []
 
2669
 
 
2670
        def callback(value):
 
2671
            results.append(value)
 
2672
 
 
2673
        yield service_state.watch_config_state(callback)
 
2674
        config = yield service_state.get_config()
 
2675
        config["alpha"] = "beta"
 
2676
        yield config.write()
 
2677
 
 
2678
        yield self.poke_zk()
 
2679
        self.assertIdentical(results.pop(0), True)
 
2680
        self.assertIdentical(results.pop(0).type_name, "changed")
 
2681
 
 
2682
        # and changing it again should trigger the callback again
 
2683
        config["gamma"] = "delta"
 
2684
        yield config.write()
 
2685
 
 
2686
        yield self.poke_zk()
 
2687
        self.assertEqual(len(results), 1)
 
2688
        self.assertIdentical(results.pop(0).type_name, "changed")
 
2689
 
 
2690
    @inlineCallbacks
 
2691
    def test_get_open_ports(self):
 
2692
        """Verify introspection and that the ports changes are immediate."""
 
2693
        service_state = yield self.add_service("wordpress")
 
2694
        unit_state = yield service_state.add_unit_state()
 
2695
 
 
2696
        # verify no open ports before activity
 
2697
        self.assertEqual((yield unit_state.get_open_ports()), [])
 
2698
 
 
2699
        # then open_port, close_port
 
2700
        yield unit_state.open_port(80, "tcp")
 
2701
        self.assertEqual(
 
2702
                (yield unit_state.get_open_ports()),
 
2703
                [{"port": 80, "proto": "tcp"}])
 
2704
 
 
2705
        yield unit_state.open_port(53, "udp")
 
2706
        self.assertEqual(
 
2707
                (yield unit_state.get_open_ports()),
 
2708
                [{"port": 80, "proto": "tcp"},
 
2709
                 {"port": 53, "proto": "udp"}])
 
2710
 
 
2711
        yield unit_state.open_port(53, "tcp")
 
2712
        self.assertEqual(
 
2713
                (yield unit_state.get_open_ports()),
 
2714
                [{"port": 80, "proto": "tcp"},
 
2715
                 {"port": 53, "proto": "udp"},
 
2716
                 {"port": 53, "proto": "tcp"}])
 
2717
 
 
2718
        yield unit_state.open_port(443, "tcp")
 
2719
        self.assertEqual(
 
2720
                (yield unit_state.get_open_ports()),
 
2721
                [{"port": 80, "proto": "tcp"},
 
2722
                 {"port": 53, "proto": "udp"},
 
2723
                 {"port": 53, "proto": "tcp"},
 
2724
                 {"port": 443, "proto": "tcp"}])
 
2725
 
 
2726
        yield unit_state.close_port(80, "tcp")
 
2727
        self.assertEqual(
 
2728
                (yield unit_state.get_open_ports()),
 
2729
                [{"port": 53, "proto": "udp"},
 
2730
                 {"port": 53, "proto": "tcp"},
 
2731
                 {"port": 443, "proto": "tcp"}])
 
2732
 
 
2733
    @inlineCallbacks
 
2734
    def test_close_open_port(self):
 
2735
        """Verify closing an unopened port, then actually opening it, works."""
 
2736
        service_state = yield self.add_service("wordpress")
 
2737
        unit_state = yield service_state.add_unit_state()
 
2738
 
 
2739
        yield unit_state.close_port(80, "tcp")
 
2740
        self.assertEqual(
 
2741
                (yield unit_state.get_open_ports()),
 
2742
                [])
 
2743
 
 
2744
        yield unit_state.open_port(80, "tcp")
 
2745
        self.assertEqual(
 
2746
                (yield unit_state.get_open_ports()),
 
2747
                [{"port": 80, "proto": "tcp"}])
 
2748
 
 
2749
    @inlineCallbacks
 
2750
    def test_open_ports_znode_representation(self):
 
2751
        """Verify the specific representation of open ports in ZK."""
 
2752
        service_state = yield self.add_service("wordpress")
 
2753
        unit_state = yield service_state.add_unit_state()
 
2754
        ports_path = "/units/unit-0000000000/ports"
 
2755
 
 
2756
        # verify no node exists before activity
 
2757
        self.assertFailure(
 
2758
                self.client.get(ports_path), zookeeper.NoNodeException)
 
2759
 
 
2760
        # verify representation format after open_port, close_port
 
2761
        yield unit_state.open_port(80, "tcp")
 
2762
        content, stat = yield self.client.get(ports_path)
 
2763
        self.assertEquals(
 
2764
                yaml.load(content),
 
2765
                {"open": [{"port": 80, "proto": "tcp"}]})
 
2766
 
 
2767
        yield unit_state.open_port(53, "udp")
 
2768
        content, stat = yield self.client.get(ports_path)
 
2769
        self.assertEquals(
 
2770
                yaml.load(content),
 
2771
                {"open": [{"port": 80, "proto": "tcp"},
 
2772
                                  {"port": 53, "proto": "udp"}]})
 
2773
 
 
2774
        yield unit_state.open_port(443, "tcp")
 
2775
        content, stat = yield self.client.get(ports_path)
 
2776
        self.assertEquals(
 
2777
                yaml.load(content),
 
2778
                {"open": [{"port": 80, "proto": "tcp"},
 
2779
                                  {"port": 53, "proto": "udp"},
 
2780
                                  {"port": 443, "proto": "tcp"}]})
 
2781
 
 
2782
        yield unit_state.close_port(80, "tcp")
 
2783
        content, stat = yield self.client.get(ports_path)
 
2784
        self.assertEquals(
 
2785
                yaml.load(content),
 
2786
                {"open": [{"port": 53, "proto": "udp"},
 
2787
                                  {"port": 443, "proto": "tcp"}]})
 
2788
 
 
2789
    @inlineCallbacks
 
2790
    def test_watch_ports(self):
 
2791
        """An open ports watch notifies of presence and changes."""
 
2792
        service_state = yield self.add_service("wordpress")
 
2793
        unit_state = yield service_state.add_unit_state()
 
2794
        yield unit_state.open_port(80, "tcp")
 
2795
        yield unit_state.open_port(53, "udp")
 
2796
        yield unit_state.open_port(443, "tcp")
 
2797
 
 
2798
        results = []
 
2799
 
 
2800
        def callback(value):
 
2801
            results.append(value)
 
2802
 
 
2803
        # set up a one-time watch
 
2804
        unit_state.watch_ports(callback)
 
2805
 
 
2806
        # do two actions
 
2807
        yield unit_state.close_port(80, "tcp")
 
2808
        yield unit_state.open_port(22, "tcp")
 
2809
 
 
2810
        # but see just the callback with one changed event, plus initial setup
 
2811
        yield self.poke_zk()
 
2812
        self.assertEqual(len(results), 3)
 
2813
        self.assertEqual(results.pop(0), True)
 
2814
        self.assertEqual(results.pop().type_name, "changed")
 
2815
        self.assertEqual(results.pop().type_name, "changed")
 
2816
        self.assertEqual(
 
2817
                (yield unit_state.get_open_ports()),
 
2818
                [{"port": 53, "proto": "udp"},
 
2819
                 {"port": 443, "proto": "tcp"},
 
2820
                 {"port": 22, "proto": "tcp"}])
 
2821
 
 
2822
    @inlineCallbacks
 
2823
    def test_stop_watch_ports(self):
 
2824
        """An exposed watch can be instituted on a permanent basis.
 
2825
 
 
2826
        However the callback can raise StopWatcher any time to stop the watch.
 
2827
        """
 
2828
        service_state = yield self.add_service("wordpress")
 
2829
        unit_state = yield service_state.add_unit_state()
 
2830
        yield unit_state.open_port(80, "tcp")
 
2831
 
 
2832
        results = []
 
2833
 
 
2834
        def callback(value):
 
2835
            results.append(value)
 
2836
            if len(results) == 1:
 
2837
                raise StopWatcher()
 
2838
            if len(results) == 4:
 
2839
                raise StopWatcher()
 
2840
 
 
2841
        unit_state.watch_ports(callback)
 
2842
        yield unit_state.close_port(80, "tcp")
 
2843
        yield self.poke_zk()
 
2844
        self.assertEqual(len(results), 1)
 
2845
        self.assertEqual(results[0], True)
 
2846
 
 
2847
        unit_state.watch_ports(callback)
 
2848
        yield unit_state.open_port(53, "udp")
 
2849
        yield self.poke_zk()
 
2850
        yield self.sleep(0.1)
 
2851
        self.assertEqual(len(results), 3)
 
2852
        self.assertEqual(results[0], True)
 
2853
        self.assertEqual(results[1], True)
 
2854
        self.assertEqual(results[2].type_name, "changed")
 
2855
        self.assertEqual(
 
2856
                (yield unit_state.get_open_ports()),
 
2857
                [{"port": 53, "proto": "udp"}])
 
2858
 
 
2859
    @inlineCallbacks
 
2860
    def test_watch_ports_slow_callbacks(self):
 
2861
        """A slow watch callback is still invoked serially."""
 
2862
        unit_state = yield self.get_unit_state()
 
2863
 
 
2864
        callbacks = [Deferred() for i in range(5)]
 
2865
        results = []
 
2866
        contents = []
 
2867
 
 
2868
        @inlineCallbacks
 
2869
        def watch(value):
 
2870
            results.append(value)
 
2871
            yield callbacks[len(results) - 1]
 
2872
            contents.append((yield unit_state.get_open_ports()))
 
2873
 
 
2874
        callbacks[0].callback(True)
 
2875
        yield unit_state.watch_ports(watch)
 
2876
 
 
2877
        # These get collapsed into a single event
 
2878
        yield unit_state.open_port(80, "tcp")
 
2879
        yield unit_state.open_port(53, "udp")
 
2880
        yield unit_state.open_port(443, "tcp")
 
2881
        yield unit_state.close_port(80, "tcp")
 
2882
        yield self.poke_zk()
 
2883
 
 
2884
        # Verify the callback hasn't completed
 
2885
        self.assertEqual(len(results), 2)
 
2886
        self.assertEqual(len(contents), 1)
 
2887
 
 
2888
        # Let it finish
 
2889
        callbacks[1].callback(True)
 
2890
        yield self.poke_zk()
 
2891
 
 
2892
        # Verify the callback hasn't completed
 
2893
        self.assertEqual(len(contents), 2)
 
2894
 
 
2895
        callbacks[2].callback(True)
 
2896
        yield self.poke_zk()
 
2897
 
 
2898
        # Verify values.
 
2899
        self.assertEqual(len(contents), 3)
 
2900
        self.assertEqual(results[-1].type_name, "changed")
 
2901
        self.assertEqual(contents[-1], [{"port": 53, "proto": "udp"},
 
2902
                                        {"port": 443, "proto": "tcp"}])
 
2903
        yield self.poke_zk()
 
2904
 
 
2905
    def test_parse_service_name(self):
 
2906
        self.assertEqual(parse_service_name("wordpress/0"), "wordpress")
 
2907
        self.assertEqual(parse_service_name("myblog/1"), "myblog")
 
2908
        self.assertRaises(ValueError, parse_service_name, "invalid")
 
2909
        self.assertRaises(ValueError, parse_service_name, None)