Merge lp:~james-page/charms/trusty/nova-cloud-controller/service-guard into lp:~openstack-charmers-archive/charms/trusty/nova-cloud-controller/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
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.

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

Revision history for this message
Liam Young (gnuoy) wrote :

Approved

review: Approve

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):

Subscribers

People subscribed via source and target branches