Merge lp:~james-page/charms/trusty/nova-cloud-controller/service-guard into lp:~openstack-charmers-archive/charms/trusty/nova-cloud-controller/next
- Trusty Tahr (14.04)
- service-guard
- Merge into next
Proposed by
James Page
Status: | Merged |
---|---|
Merged at revision: | 89 |
Proposed branch: | lp:~james-page/charms/trusty/nova-cloud-controller/service-guard |
Merge into: | lp:~openstack-charmers-archive/charms/trusty/nova-cloud-controller/next |
Diff against target: |
397 lines (+229/-7) 6 files modified
config.yaml (+19/-1) hooks/nova_cc_hooks.py (+29/-1) hooks/nova_cc_utils.py (+58/-0) unit_tests/test_nova_cc_hooks.py (+5/-1) unit_tests/test_nova_cc_utils.py (+115/-1) unit_tests/test_utils.py (+3/-3) |
To merge this branch: | bzr merge lp:~james-page/charms/trusty/nova-cloud-controller/service-guard |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Liam Young (community) | Approve | ||
Review via email: mp+228671@code.launchpad.net |
This proposal supersedes a proposal from 2014-07-29.
Commit message
Description of the change
Add support for service-guard configuration to disable services prior to relations being completely formed.
To post a comment you must log in.
- 95. By James Page
-
Don't add neutron stuff if related to neutron-api charm
- 96. By James Page
-
Fixup unit tests
- 97. By James Page
-
Tidy lint
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'config.yaml' |
2 | --- config.yaml 2014-07-16 13:42:38 +0000 |
3 | +++ config.yaml 2014-07-29 13:38:36 +0000 |
4 | @@ -184,4 +184,22 @@ |
5 | 192.168.0.0/24) |
6 | . |
7 | This network will be used for public endpoints. |
8 | - |
9 | + service-guard: |
10 | + type: boolean |
11 | + default: false |
12 | + description: | |
13 | + Ensure required relations are made and complete before allowing services |
14 | + to be started |
15 | + . |
16 | + By default, services may be up and accepting API request from install |
17 | + onwards. |
18 | + . |
19 | + Enabling this flag ensures that services will not be started until the |
20 | + minimum 'core relations' have been made between this charm and other |
21 | + charms. |
22 | + . |
23 | + For this charm the following relations must be made: |
24 | + . |
25 | + * shared-db or (pgsql-nova-db, pgsql-neutron-db) |
26 | + * amqp |
27 | + * identity-service |
28 | |
29 | === modified file 'hooks/nova_cc_hooks.py' |
30 | --- hooks/nova_cc_hooks.py 2014-07-25 09:38:56 +0000 |
31 | +++ hooks/nova_cc_hooks.py 2014-07-29 13:38:36 +0000 |
32 | @@ -70,7 +70,9 @@ |
33 | NOVA_CONF, |
34 | QUANTUM_CONF, |
35 | NEUTRON_CONF, |
36 | - QUANTUM_API_PASTE |
37 | + QUANTUM_API_PASTE, |
38 | + service_guard, |
39 | + guard_map, |
40 | ) |
41 | |
42 | from charmhelpers.contrib.hahelpers.cluster import ( |
43 | @@ -112,6 +114,8 @@ |
44 | |
45 | |
46 | @hooks.hook('config-changed') |
47 | +@service_guard(guard_map(), CONFIGS, |
48 | + active=config('service-guard')) |
49 | @restart_on_change(restart_map(), stopstart=True) |
50 | def config_changed(): |
51 | global CONFIGS |
52 | @@ -132,6 +136,8 @@ |
53 | |
54 | @hooks.hook('amqp-relation-changed') |
55 | @hooks.hook('amqp-relation-departed') |
56 | +@service_guard(guard_map(), CONFIGS, |
57 | + active=config('service-guard')) |
58 | @restart_on_change(restart_map()) |
59 | def amqp_changed(): |
60 | if 'amqp' not in CONFIGS.complete_contexts(): |
61 | @@ -190,6 +196,8 @@ |
62 | |
63 | |
64 | @hooks.hook('shared-db-relation-changed') |
65 | +@service_guard(guard_map(), CONFIGS, |
66 | + active=config('service-guard')) |
67 | @restart_on_change(restart_map()) |
68 | def db_changed(): |
69 | if 'shared-db' not in CONFIGS.complete_contexts(): |
70 | @@ -205,6 +213,8 @@ |
71 | |
72 | |
73 | @hooks.hook('pgsql-nova-db-relation-changed') |
74 | +@service_guard(guard_map(), CONFIGS, |
75 | + active=config('service-guard')) |
76 | @restart_on_change(restart_map()) |
77 | def postgresql_nova_db_changed(): |
78 | if 'pgsql-nova-db' not in CONFIGS.complete_contexts(): |
79 | @@ -220,6 +230,8 @@ |
80 | |
81 | |
82 | @hooks.hook('pgsql-neutron-db-relation-changed') |
83 | +@service_guard(guard_map(), CONFIGS, |
84 | + active=config('service-guard')) |
85 | @restart_on_change(restart_map()) |
86 | def postgresql_neutron_db_changed(): |
87 | if network_manager() in ['neutron', 'quantum']: |
88 | @@ -229,6 +241,8 @@ |
89 | |
90 | |
91 | @hooks.hook('image-service-relation-changed') |
92 | +@service_guard(guard_map(), CONFIGS, |
93 | + active=config('service-guard')) |
94 | @restart_on_change(restart_map()) |
95 | def image_service_changed(): |
96 | if 'image-service' not in CONFIGS.complete_contexts(): |
97 | @@ -251,6 +265,8 @@ |
98 | |
99 | |
100 | @hooks.hook('identity-service-relation-changed') |
101 | +@service_guard(guard_map(), CONFIGS, |
102 | + active=config('service-guard')) |
103 | @restart_on_change(restart_map()) |
104 | def identity_changed(): |
105 | if 'identity-service' not in CONFIGS.complete_contexts(): |
106 | @@ -274,6 +290,8 @@ |
107 | |
108 | @hooks.hook('nova-volume-service-relation-joined', |
109 | 'cinder-volume-service-relation-joined') |
110 | +@service_guard(guard_map(), CONFIGS, |
111 | + active=config('service-guard')) |
112 | @restart_on_change(restart_map()) |
113 | def volume_joined(): |
114 | CONFIGS.write(NOVA_CONF) |
115 | @@ -465,6 +483,8 @@ |
116 | |
117 | @hooks.hook('cluster-relation-changed', |
118 | 'cluster-relation-departed') |
119 | +@service_guard(guard_map(), CONFIGS, |
120 | + active=config('service-guard')) |
121 | @restart_on_change(restart_map(), stopstart=True) |
122 | def cluster_changed(): |
123 | CONFIGS.write_all() |
124 | @@ -534,6 +554,8 @@ |
125 | 'pgsql-nova-db-relation-broken', |
126 | 'pgsql-neutron-db-relation-broken', |
127 | 'quantum-network-service-relation-broken') |
128 | +@service_guard(guard_map(), CONFIGS, |
129 | + active=config('service-guard')) |
130 | def relation_broken(): |
131 | CONFIGS.write_all() |
132 | |
133 | @@ -574,6 +596,8 @@ |
134 | |
135 | |
136 | @hooks.hook('nova-vmware-relation-changed') |
137 | +@service_guard(guard_map(), CONFIGS, |
138 | + active=config('service-guard')) |
139 | @restart_on_change(restart_map()) |
140 | def nova_vmware_relation_changed(): |
141 | CONFIGS.write('/etc/nova/nova.conf') |
142 | @@ -605,6 +629,8 @@ |
143 | |
144 | |
145 | @hooks.hook('neutron-api-relation-changed') |
146 | +@service_guard(guard_map(), CONFIGS, |
147 | + active=config('service-guard')) |
148 | @restart_on_change(restart_map()) |
149 | def neutron_api_relation_changed(): |
150 | CONFIGS.write(NOVA_CONF) |
151 | @@ -615,6 +641,8 @@ |
152 | |
153 | |
154 | @hooks.hook('neutron-api-relation-broken') |
155 | +@service_guard(guard_map(), CONFIGS, |
156 | + active=config('service-guard')) |
157 | @restart_on_change(restart_map()) |
158 | def neutron_api_relation_broken(): |
159 | if os.path.isfile('/etc/init/neutron-server.override'): |
160 | |
161 | === modified file 'hooks/nova_cc_utils.py' |
162 | --- hooks/nova_cc_utils.py 2014-07-25 09:38:56 +0000 |
163 | +++ hooks/nova_cc_utils.py 2014-07-29 13:38:36 +0000 |
164 | @@ -40,6 +40,8 @@ |
165 | |
166 | from charmhelpers.core.host import ( |
167 | service_start, |
168 | + service_stop, |
169 | + service_running |
170 | ) |
171 | |
172 | import nova_cc_context |
173 | @@ -753,3 +755,59 @@ |
174 | # quantum-plugin config setting can be safely overriden |
175 | # as we only supported OVS in G/neutron |
176 | return config('neutron-plugin') or config('quantum-plugin') |
177 | + |
178 | + |
179 | +def guard_map(): |
180 | + '''Map of services and required interfaces that must be present before |
181 | + the service should be allowed to start''' |
182 | + gmap = {} |
183 | + nova_services = deepcopy(BASE_SERVICES) |
184 | + if os_release('nova-common') not in ['essex', 'folsom']: |
185 | + nova_services.append('nova-conductor') |
186 | + |
187 | + nova_interfaces = ['identity-service', 'amqp'] |
188 | + if relation_ids('pgsql-nova-db'): |
189 | + nova_interfaces.append('pgsql-nova-db') |
190 | + else: |
191 | + nova_interfaces.append('shared-db') |
192 | + |
193 | + for svc in nova_services: |
194 | + gmap[svc] = nova_interfaces |
195 | + |
196 | + net_manager = network_manager() |
197 | + if net_manager in ['neutron', 'quantum'] and \ |
198 | + not is_relation_made('neutron-api'): |
199 | + neutron_interfaces = ['identity-service', 'amqp'] |
200 | + if relation_ids('pgsql-neutron-db'): |
201 | + neutron_interfaces.append('pgsql-neutron-db') |
202 | + else: |
203 | + neutron_interfaces.append('shared-db') |
204 | + if network_manager() == 'quantum': |
205 | + gmap['quantum-server'] = neutron_interfaces |
206 | + else: |
207 | + gmap['neutron-server'] = neutron_interfaces |
208 | + |
209 | + return gmap |
210 | + |
211 | + |
212 | +def service_guard(guard_map, contexts, active=False): |
213 | + '''Inhibit services in guard_map from running unless |
214 | + required interfaces are found complete in contexts.''' |
215 | + def wrap(f): |
216 | + def wrapped_f(*args): |
217 | + if active is True: |
218 | + incomplete_services = [] |
219 | + for svc in guard_map: |
220 | + for interface in guard_map[svc]: |
221 | + if interface not in contexts.complete_contexts(): |
222 | + incomplete_services.append(svc) |
223 | + f(*args) |
224 | + for svc in incomplete_services: |
225 | + if service_running(svc): |
226 | + log('Service {} has unfulfilled ' |
227 | + 'interface requirements, stopping.'.format(svc)) |
228 | + service_stop(svc) |
229 | + else: |
230 | + f(*args) |
231 | + return wrapped_f |
232 | + return wrap |
233 | |
234 | === modified file 'unit_tests/test_nova_cc_hooks.py' |
235 | --- unit_tests/test_nova_cc_hooks.py 2014-07-16 09:45:44 +0000 |
236 | +++ unit_tests/test_nova_cc_hooks.py 2014-07-29 13:38:36 +0000 |
237 | @@ -11,7 +11,11 @@ |
238 | utils.register_configs = MagicMock() |
239 | utils.restart_map = MagicMock() |
240 | |
241 | -import nova_cc_hooks as hooks |
242 | +with patch('nova_cc_utils.guard_map') as gmap: |
243 | + with patch('charmhelpers.core.hookenv.config') as config: |
244 | + config.return_value = False |
245 | + gmap.return_value = {} |
246 | + import nova_cc_hooks as hooks |
247 | |
248 | utils.register_configs = _reg |
249 | utils.restart_map = _map |
250 | |
251 | === modified file 'unit_tests/test_nova_cc_utils.py' |
252 | --- unit_tests/test_nova_cc_utils.py 2014-07-25 09:38:56 +0000 |
253 | +++ unit_tests/test_nova_cc_utils.py 2014-07-29 13:38:36 +0000 |
254 | @@ -35,7 +35,9 @@ |
255 | 'remote_unit', |
256 | '_save_script_rc', |
257 | 'service_start', |
258 | - 'services' |
259 | + 'services', |
260 | + 'service_running', |
261 | + 'service_stop' |
262 | ] |
263 | |
264 | SCRIPTRC_ENV_VARS = { |
265 | @@ -596,3 +598,115 @@ |
266 | utils.do_openstack_upgrade() |
267 | expected = [call('cloud:precise-icehouse')] |
268 | self.assertEquals(_do_openstack_upgrade.call_args_list, expected) |
269 | + |
270 | + def test_guard_map_nova(self): |
271 | + self.relation_ids.return_value = [] |
272 | + self.os_release.return_value = 'havana' |
273 | + self.assertEqual( |
274 | + {'nova-api-ec2': ['identity-service', 'amqp', 'shared-db'], |
275 | + 'nova-api-os-compute': ['identity-service', 'amqp', 'shared-db'], |
276 | + 'nova-cert': ['identity-service', 'amqp', 'shared-db'], |
277 | + 'nova-conductor': ['identity-service', 'amqp', 'shared-db'], |
278 | + 'nova-objectstore': ['identity-service', 'amqp', 'shared-db'], |
279 | + 'nova-scheduler': ['identity-service', 'amqp', 'shared-db']}, |
280 | + utils.guard_map() |
281 | + ) |
282 | + self.os_release.return_value = 'essex' |
283 | + self.assertEqual( |
284 | + {'nova-api-ec2': ['identity-service', 'amqp', 'shared-db'], |
285 | + 'nova-api-os-compute': ['identity-service', 'amqp', 'shared-db'], |
286 | + 'nova-cert': ['identity-service', 'amqp', 'shared-db'], |
287 | + 'nova-objectstore': ['identity-service', 'amqp', 'shared-db'], |
288 | + 'nova-scheduler': ['identity-service', 'amqp', 'shared-db']}, |
289 | + utils.guard_map() |
290 | + ) |
291 | + |
292 | + def test_guard_map_neutron(self): |
293 | + self.relation_ids.return_value = [] |
294 | + self.network_manager.return_value = 'neutron' |
295 | + self.os_release.return_value = 'icehouse' |
296 | + self.is_relation_made.return_value = False |
297 | + self.assertEqual( |
298 | + {'neutron-server': ['identity-service', 'amqp', 'shared-db'], |
299 | + 'nova-api-ec2': ['identity-service', 'amqp', 'shared-db'], |
300 | + 'nova-api-os-compute': ['identity-service', 'amqp', 'shared-db'], |
301 | + 'nova-cert': ['identity-service', 'amqp', 'shared-db'], |
302 | + 'nova-conductor': ['identity-service', 'amqp', 'shared-db'], |
303 | + 'nova-objectstore': ['identity-service', 'amqp', 'shared-db'], |
304 | + 'nova-scheduler': ['identity-service', 'amqp', 'shared-db'], }, |
305 | + utils.guard_map() |
306 | + ) |
307 | + self.network_manager.return_value = 'quantum' |
308 | + self.os_release.return_value = 'grizzly' |
309 | + self.assertEqual( |
310 | + {'quantum-server': ['identity-service', 'amqp', 'shared-db'], |
311 | + 'nova-api-ec2': ['identity-service', 'amqp', 'shared-db'], |
312 | + 'nova-api-os-compute': ['identity-service', 'amqp', 'shared-db'], |
313 | + 'nova-cert': ['identity-service', 'amqp', 'shared-db'], |
314 | + 'nova-conductor': ['identity-service', 'amqp', 'shared-db'], |
315 | + 'nova-objectstore': ['identity-service', 'amqp', 'shared-db'], |
316 | + 'nova-scheduler': ['identity-service', 'amqp', 'shared-db'], }, |
317 | + utils.guard_map() |
318 | + ) |
319 | + |
320 | + def test_guard_map_pgsql(self): |
321 | + self.relation_ids.return_value = ['pgsql:1'] |
322 | + self.network_manager.return_value = 'neutron' |
323 | + self.is_relation_made.return_value = False |
324 | + self.os_release.return_value = 'icehouse' |
325 | + self.assertEqual( |
326 | + {'neutron-server': ['identity-service', 'amqp', |
327 | + 'pgsql-neutron-db'], |
328 | + 'nova-api-ec2': ['identity-service', 'amqp', 'pgsql-nova-db'], |
329 | + 'nova-api-os-compute': ['identity-service', 'amqp', |
330 | + 'pgsql-nova-db'], |
331 | + 'nova-cert': ['identity-service', 'amqp', 'pgsql-nova-db'], |
332 | + 'nova-conductor': ['identity-service', 'amqp', 'pgsql-nova-db'], |
333 | + 'nova-objectstore': ['identity-service', 'amqp', |
334 | + 'pgsql-nova-db'], |
335 | + 'nova-scheduler': ['identity-service', 'amqp', |
336 | + 'pgsql-nova-db'], }, |
337 | + utils.guard_map() |
338 | + ) |
339 | + |
340 | + def test_service_guard_inactive(self): |
341 | + '''Ensure that if disabled, service guards nothing''' |
342 | + contexts = MagicMock() |
343 | + |
344 | + @utils.service_guard({'test': ['interfacea', 'interfaceb']}, |
345 | + contexts, False) |
346 | + def dummy_func(): |
347 | + pass |
348 | + dummy_func() |
349 | + self.assertFalse(self.service_running.called) |
350 | + self.assertFalse(contexts.complete_contexts.called) |
351 | + |
352 | + def test_service_guard_active_guard(self): |
353 | + '''Ensure services with incomplete interfaces are stopped''' |
354 | + contexts = MagicMock() |
355 | + contexts.complete_contexts.return_value = ['interfacea'] |
356 | + self.service_running.return_value = True |
357 | + |
358 | + @utils.service_guard({'test': ['interfacea', 'interfaceb']}, |
359 | + contexts, True) |
360 | + def dummy_func(): |
361 | + pass |
362 | + dummy_func() |
363 | + self.service_running.assert_called_with('test') |
364 | + self.service_stop.assert_called_with('test') |
365 | + self.assertTrue(contexts.complete_contexts.called) |
366 | + |
367 | + def test_service_guard_active_release(self): |
368 | + '''Ensure services with complete interfaces are not stopped''' |
369 | + contexts = MagicMock() |
370 | + contexts.complete_contexts.return_value = ['interfacea', |
371 | + 'interfaceb'] |
372 | + |
373 | + @utils.service_guard({'test': ['interfacea', 'interfaceb']}, |
374 | + contexts, True) |
375 | + def dummy_func(): |
376 | + pass |
377 | + dummy_func() |
378 | + self.assertFalse(self.service_running.called) |
379 | + self.assertFalse(self.service_stop.called) |
380 | + self.assertTrue(contexts.complete_contexts.called) |
381 | |
382 | === modified file 'unit_tests/test_utils.py' |
383 | --- unit_tests/test_utils.py 2013-11-08 05:41:39 +0000 |
384 | +++ unit_tests/test_utils.py 2014-07-29 13:38:36 +0000 |
385 | @@ -82,9 +82,9 @@ |
386 | return self.config |
387 | |
388 | def set(self, attr, value): |
389 | - if attr not in self.config: |
390 | - raise KeyError |
391 | - self.config[attr] = value |
392 | + if attr not in self.config: |
393 | + raise KeyError |
394 | + self.config[attr] = value |
395 | |
396 | |
397 | class TestRelation(object): |
Approved