Merge lp:~james-page/charms/trusty/nova-cloud-controller/service-guard-stable into lp:~openstack-charmers-archive/charms/trusty/nova-cloud-controller/trunk

Proposed by James Page
Status: Merged
Merged at revision: 79
Proposed branch: lp:~james-page/charms/trusty/nova-cloud-controller/service-guard-stable
Merge into: lp:~openstack-charmers-archive/charms/trusty/nova-cloud-controller/trunk
Diff against target: 390 lines (+228/-7)
6 files modified
config.yaml (+19/-0)
hooks/nova_cc_hooks.py (+25/-1)
hooks/nova_cc_utils.py (+60/-1)
unit_tests/test_nova_cc_hooks.py (+5/-1)
unit_tests/test_nova_cc_utils.py (+116/-1)
unit_tests/test_utils.py (+3/-3)
To merge this branch: bzr merge lp:~james-page/charms/trusty/nova-cloud-controller/service-guard-stable
Reviewer Review Type Date Requested Status
Adam Collard (community) Approve
OpenStack Charmers Pending
Review via email: mp+228699@code.launchpad.net

Description of the change

Backport support for service-guard option to ensure that services are not
started until all required interfaces have been related and fully configured.

To post a comment you must log in.
Revision history for this message
Adam Collard (adam-collard) wrote :

A few style comments inline below, nothing blocking :)

Revision history for this message
Adam Collard (adam-collard) wrote :

Tested successfully fixes the bugs in Landscape that were caused by having services run prematurely ( #1344413 and #1347986).

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

Subscribers

People subscribed via source and target branches