Merge lp:~thedac/charm-helpers/openstack-object-oriented-status into lp:charm-helpers

Proposed by David Ames
Status: Merged
Merged at revision: 449
Proposed branch: lp:~thedac/charm-helpers/openstack-object-oriented-status
Merge into: lp:charm-helpers
Diff against target: 515 lines (+357/-10)
5 files modified
charmhelpers/contrib/openstack/context.py (+52/-7)
charmhelpers/contrib/openstack/templating.py (+28/-1)
charmhelpers/contrib/openstack/utils.py (+173/-1)
tests/contrib/openstack/test_openstack_utils.py (+103/-0)
tests/contrib/openstack/test_os_contexts.py (+1/-1)
To merge this branch: bzr merge lp:~thedac/charm-helpers/openstack-object-oriented-status
Reviewer Review Type Date Requested Status
Liam Young (community) Approve
David Ames (community) Needs Resubmitting
Review via email: mp+265344@code.launchpad.net

Commit message

Object oriented workload status for openstack charms

Description of the change

Object oriented workload status for openstack charms

To post a comment you must log in.
Revision history for this message
Liam Young (gnuoy) wrote :

I really like this but I have some concerns around some of the object names used. It looks to me like context_states is really relation_states and incomplete_contexts does not return a list of incomplete contexts but rather a list of incomplete relations so would more correctly be called unsatisfied_relations.

I'm wondering whether this mp should be extended to include the checking of contexts which aren't associated with a relation. For example when doing an HA deployment the user needs to have set a vip for the charm to use, if they don't do this the charm is blocked until they do. If the main hook function communicates this to the user via a status set then the context_status decorator will stamp on that message I think.

review: Needs Fixing
407. By David Ames

Rename and add charm specific function to avoid clobbering

Revision history for this message
David Ames (thedac) wrote :

> I really like this but I have some concerns around some of the object names
> used. It looks to me like context_states is really relation_states

I have renamed this os_workload_status

> and
> incomplete_contexts does not return a list of incomplete contexts but rather a
> list of incomplete relations so would more correctly be called
> unsatisfied_relations.

I actually need you to be more specific on this? Maybe I am missing what a "context" is. I'd like to discuss it further.

> I'm wondering whether this mp should be extended to include the checking of
> contexts which aren't associated with a relation. For example when doing an HA
> deployment the user needs to have set a vip for the charm to use, if they
> don't do this the charm is blocked until they do. If the main hook function
> communicates this to the user via a status set then the context_status
> decorator will stamp on that message I think.

I have added a charm specific function call so that these status settings will not be clobbered.

review: Needs Resubmitting
408. By David Ames

Rename incomplete_contexts incomplete_relation_data

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

A couple of nits (see diff comments).

review: Needs Fixing
409. By David Ames

More renames

Revision history for this message
David Ames (thedac) wrote :

Liam, please take another look

review: Needs Resubmitting
410. By David Ames

Unit is ready

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

LGTM, thanks!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charmhelpers/contrib/openstack/context.py'
2--- charmhelpers/contrib/openstack/context.py 2015-09-11 12:19:23 +0000
3+++ charmhelpers/contrib/openstack/context.py 2015-09-15 17:38:07 +0000
4@@ -194,10 +194,50 @@
5 class OSContextGenerator(object):
6 """Base class for all context generators."""
7 interfaces = []
8+ related = False
9+ complete = False
10+ missing_data = []
11
12 def __call__(self):
13 raise NotImplementedError
14
15+ def context_complete(self, ctxt):
16+ """Check for missing data for the required context data.
17+ Set self.missing_data if it exists and return False.
18+ Set self.complete if no missing data and return True.
19+ """
20+ # Fresh start
21+ self.complete = False
22+ self.missing_data = []
23+ for k, v in six.iteritems(ctxt):
24+ if v is None or v == '':
25+ if k not in self.missing_data:
26+ self.missing_data.append(k)
27+
28+ if self.missing_data:
29+ self.complete = False
30+ log('Missing required data: %s' % ' '.join(self.missing_data), level=INFO)
31+ else:
32+ self.complete = True
33+ return self.complete
34+
35+ def get_related(self):
36+ """Check if any of the context interfaces have relation ids.
37+ Set self.related and return True if one of the interfaces
38+ has relation ids.
39+ """
40+ # Fresh start
41+ self.related = False
42+ try:
43+ for interface in self.interfaces:
44+ if relation_ids(interface):
45+ self.related = True
46+ return self.related
47+ except AttributeError as e:
48+ log("{} {}"
49+ "".format(self, e), 'INFO')
50+ return self.related
51+
52
53 class SharedDBContext(OSContextGenerator):
54 interfaces = ['shared-db']
55@@ -213,6 +253,7 @@
56 self.database = database
57 self.user = user
58 self.ssl_dir = ssl_dir
59+ self.rel_name = self.interfaces[0]
60
61 def __call__(self):
62 self.database = self.database or config('database')
63@@ -246,6 +287,7 @@
64 password_setting = self.relation_prefix + '_password'
65
66 for rid in relation_ids(self.interfaces[0]):
67+ self.related = True
68 for unit in related_units(rid):
69 rdata = relation_get(rid=rid, unit=unit)
70 host = rdata.get('db_host')
71@@ -257,7 +299,7 @@
72 'database_password': rdata.get(password_setting),
73 'database_type': 'mysql'
74 }
75- if context_complete(ctxt):
76+ if self.context_complete(ctxt):
77 db_ssl(rdata, ctxt, self.ssl_dir)
78 return ctxt
79 return {}
80@@ -278,6 +320,7 @@
81
82 ctxt = {}
83 for rid in relation_ids(self.interfaces[0]):
84+ self.related = True
85 for unit in related_units(rid):
86 rel_host = relation_get('host', rid=rid, unit=unit)
87 rel_user = relation_get('user', rid=rid, unit=unit)
88@@ -287,7 +330,7 @@
89 'database_user': rel_user,
90 'database_password': rel_passwd,
91 'database_type': 'postgresql'}
92- if context_complete(ctxt):
93+ if self.context_complete(ctxt):
94 return ctxt
95
96 return {}
97@@ -348,6 +391,7 @@
98 ctxt['signing_dir'] = cachedir
99
100 for rid in relation_ids(self.rel_name):
101+ self.related = True
102 for unit in related_units(rid):
103 rdata = relation_get(rid=rid, unit=unit)
104 serv_host = rdata.get('service_host')
105@@ -366,7 +410,7 @@
106 'service_protocol': svc_protocol,
107 'auth_protocol': auth_protocol})
108
109- if context_complete(ctxt):
110+ if self.context_complete(ctxt):
111 # NOTE(jamespage) this is required for >= icehouse
112 # so a missing value just indicates keystone needs
113 # upgrading
114@@ -405,6 +449,7 @@
115 ctxt = {}
116 for rid in relation_ids(self.rel_name):
117 ha_vip_only = False
118+ self.related = True
119 for unit in related_units(rid):
120 if relation_get('clustered', rid=rid, unit=unit):
121 ctxt['clustered'] = True
122@@ -437,7 +482,7 @@
123 ha_vip_only = relation_get('ha-vip-only',
124 rid=rid, unit=unit) is not None
125
126- if context_complete(ctxt):
127+ if self.context_complete(ctxt):
128 if 'rabbit_ssl_ca' in ctxt:
129 if not self.ssl_dir:
130 log("Charm not setup for ssl support but ssl ca "
131@@ -469,7 +514,7 @@
132 ctxt['oslo_messaging_flags'] = config_flags_parser(
133 oslo_messaging_flags)
134
135- if not context_complete(ctxt):
136+ if not self.complete:
137 return {}
138
139 return ctxt
140@@ -507,7 +552,7 @@
141 if not os.path.isdir('/etc/ceph'):
142 os.mkdir('/etc/ceph')
143
144- if not context_complete(ctxt):
145+ if not self.context_complete(ctxt):
146 return {}
147
148 ensure_packages(['ceph-common'])
149@@ -1366,6 +1411,6 @@
150 'auth_protocol':
151 rdata.get('auth_protocol') or 'http',
152 }
153- if context_complete(ctxt):
154+ if self.context_complete(ctxt):
155 return ctxt
156 return {}
157
158=== modified file 'charmhelpers/contrib/openstack/templating.py'
159--- charmhelpers/contrib/openstack/templating.py 2015-07-22 01:19:53 +0000
160+++ charmhelpers/contrib/openstack/templating.py 2015-09-15 17:38:07 +0000
161@@ -112,7 +112,7 @@
162
163 def complete_contexts(self):
164 '''
165- Return a list of interfaces that have atisfied contexts.
166+ Return a list of interfaces that have satisfied contexts.
167 '''
168 if self._complete_contexts:
169 return self._complete_contexts
170@@ -293,3 +293,30 @@
171 [interfaces.extend(i.complete_contexts())
172 for i in six.itervalues(self.templates)]
173 return interfaces
174+
175+ def get_incomplete_context_data(self, interfaces):
176+ '''
177+ Return dictionary of relation status of interfaces and any missing
178+ required context data. Example:
179+ {'amqp': {'missing_data': ['rabbitmq_password'], 'related': True},
180+ 'zeromq-configuration': {'related': False}}
181+ '''
182+ incomplete_context_data = {}
183+
184+ for i in six.itervalues(self.templates):
185+ for context in i.contexts:
186+ for interface in interfaces:
187+ related = False
188+ if interface in context.interfaces:
189+ related = context.get_related()
190+ missing_data = context.missing_data
191+ if missing_data:
192+ incomplete_context_data[interface] = {'missing_data': missing_data}
193+ if related:
194+ if incomplete_context_data.get(interface):
195+ incomplete_context_data[interface].update({'related': True})
196+ else:
197+ incomplete_context_data[interface] = {'related': True}
198+ else:
199+ incomplete_context_data[interface] = {'related': False}
200+ return incomplete_context_data
201
202=== modified file 'charmhelpers/contrib/openstack/utils.py'
203--- charmhelpers/contrib/openstack/utils.py 2015-09-07 11:09:43 +0000
204+++ charmhelpers/contrib/openstack/utils.py 2015-09-15 17:38:07 +0000
205@@ -39,7 +39,9 @@
206 charm_dir,
207 INFO,
208 relation_ids,
209- relation_set
210+ relation_set,
211+ status_set,
212+ hook_name
213 )
214
215 from charmhelpers.contrib.storage.linux.lvm import (
216@@ -749,3 +751,173 @@
217 return projects[key]
218
219 return None
220+
221+
222+def os_workload_status(configs, required_interfaces, charm_func=None):
223+ """
224+ Decorator to set workload status based on complete contexts
225+ """
226+ def wrap(f):
227+ @wraps(f)
228+ def wrapped_f(*args, **kwargs):
229+ # Run the original function first
230+ f(*args, **kwargs)
231+ # Set workload status now that contexts have been
232+ # acted on
233+ set_os_workload_status(configs, required_interfaces, charm_func)
234+ return wrapped_f
235+ return wrap
236+
237+
238+def set_os_workload_status(configs, required_interfaces, charm_func=None):
239+ """
240+ Set workload status based on complete contexts.
241+ status-set missing or incomplete contexts
242+ and juju-log details of missing required data.
243+ charm_func is a charm specific function to run checking
244+ for charm specific requirements such as a VIP setting.
245+ """
246+ incomplete_rel_data = incomplete_relation_data(configs, required_interfaces)
247+ state = 'active'
248+ missing_relations = []
249+ incomplete_relations = []
250+ message = None
251+ charm_state = None
252+ charm_message = None
253+
254+ for generic_interface in incomplete_rel_data.keys():
255+ related_interface = None
256+ missing_data = {}
257+ # Related or not?
258+ for interface in incomplete_rel_data[generic_interface]:
259+ if incomplete_rel_data[generic_interface][interface].get('related'):
260+ related_interface = interface
261+ missing_data = incomplete_rel_data[generic_interface][interface].get('missing_data')
262+ # No relation ID for the generic_interface
263+ if not related_interface:
264+ juju_log("{} relation is missing and must be related for "
265+ "functionality. ".format(generic_interface), 'WARN')
266+ state = 'blocked'
267+ if generic_interface not in missing_relations:
268+ missing_relations.append(generic_interface)
269+ else:
270+ # Relation ID exists but no related unit
271+ if not missing_data:
272+ # Edge case relation ID exists but departing
273+ if ('departed' in hook_name() or 'broken' in hook_name()) \
274+ and related_interface in hook_name():
275+ state = 'blocked'
276+ if generic_interface not in missing_relations:
277+ missing_relations.append(generic_interface)
278+ juju_log("{} relation's interface, {}, "
279+ "relationship is departed or broken "
280+ "and is required for functionality."
281+ "".format(generic_interface, related_interface), "WARN")
282+ # Normal case relation ID exists but no related unit
283+ # (joining)
284+ else:
285+ juju_log("{} relations's interface, {}, is related but has "
286+ "no units in the relation."
287+ "".format(generic_interface, related_interface), "INFO")
288+ # Related unit exists and data missing on the relation
289+ else:
290+ juju_log("{} relation's interface, {}, is related awaiting "
291+ "the following data from the relationship: {}. "
292+ "".format(generic_interface, related_interface,
293+ ", ".join(missing_data)), "INFO")
294+ if state != 'blocked':
295+ state = 'waiting'
296+ if generic_interface not in incomplete_relations \
297+ and generic_interface not in missing_relations:
298+ incomplete_relations.append(generic_interface)
299+
300+ if missing_relations:
301+ message = "Missing relations: {}".format(", ".join(missing_relations))
302+ if incomplete_relations:
303+ message += "; incomplete relations: {}" \
304+ "".format(", ".join(incomplete_relations))
305+ state = 'blocked'
306+ elif incomplete_relations:
307+ message = "Incomplete relations: {}" \
308+ "".format(", ".join(incomplete_relations))
309+ state = 'waiting'
310+
311+ # Run charm specific checks
312+ if charm_func:
313+ charm_state, charm_message = charm_func(configs)
314+ if charm_state != 'active' and charm_state != 'unknown':
315+ state = workload_state_compare(state, charm_state)
316+ if message:
317+ message = "{} {}".format(message, charm_message)
318+ else:
319+ message = charm_message
320+
321+ # Set to active if all requirements have been met
322+ if state == 'active':
323+ message = "Unit is ready"
324+ juju_log(message, "INFO")
325+
326+ status_set(state, message)
327+
328+
329+def workload_state_compare(current_workload_state, workload_state):
330+ """ Return highest priority of two states"""
331+ hierarchy = {'unknown': -1,
332+ 'active': 0,
333+ 'maintenance': 1,
334+ 'waiting': 2,
335+ 'blocked': 3,
336+ }
337+
338+ if hierarchy.get(workload_state) is None:
339+ workload_state = 'unknown'
340+ if hierarchy.get(current_workload_state) is None:
341+ current_workload_state = 'unknown'
342+
343+ # Set workload_state based on hierarchy of statuses
344+ if hierarchy.get(current_workload_state) > hierarchy.get(workload_state):
345+ return current_workload_state
346+ else:
347+ return workload_state
348+
349+
350+def incomplete_relation_data(configs, required_interfaces):
351+ """
352+ Check complete contexts against required_interfaces
353+ Return dictionary of incomplete relation data.
354+
355+ configs is an OSConfigRenderer object with configs registered
356+
357+ required_interfaces is a dictionary of required general interfaces
358+ with dictionary values of possible specific interfaces.
359+ Example:
360+ required_interfaces = {'database': ['shared-db', 'pgsql-db']}
361+
362+ The interface is said to be satisfied if anyone of the interfaces in the
363+ list has a complete context.
364+
365+ Return dictionary of incomplete or missing required contexts with relation
366+ status of interfaces and any missing data points. Example:
367+ {'message':
368+ {'amqp': {'missing_data': ['rabbitmq_password'], 'related': True},
369+ 'zeromq-configuration': {'related': False}},
370+ 'identity':
371+ {'identity-service': {'related': False}},
372+ 'database':
373+ {'pgsql-db': {'related': False},
374+ 'shared-db': {'related': True}}}
375+ """
376+ complete_ctxts = configs.complete_contexts()
377+ incomplete_relations = []
378+ for svc_type in required_interfaces.keys():
379+ # Avoid duplicates
380+ found_ctxt = False
381+ for interface in required_interfaces[svc_type]:
382+ if interface in complete_ctxts:
383+ found_ctxt = True
384+ if not found_ctxt:
385+ incomplete_relations.append(svc_type)
386+ incomplete_context_data = {}
387+ for i in incomplete_relations:
388+ incomplete_context_data[i] = configs.get_incomplete_context_data(required_interfaces[i])
389+ return incomplete_context_data
390
391=== modified file 'tests/contrib/openstack/test_openstack_utils.py'
392--- tests/contrib/openstack/test_openstack_utils.py 2015-09-03 08:41:45 +0000
393+++ tests/contrib/openstack/test_openstack_utils.py 2015-09-15 17:38:07 +0000
394@@ -840,5 +840,108 @@
395 openstack.git_src_dir(openstack_origin_git, 'keystone')
396 join.assert_called_with('/mnt/openstack-git', 'keystone')
397
398+ def test_incomplete_relation_data(self):
399+ configs = MagicMock()
400+ configs.complete_contexts.return_value = ['pgsql-db', 'amqp']
401+ required_interfaces = {
402+ 'database': ['shared-db', 'pgsql-db'],
403+ 'message': ['amqp', 'zeromq-configuration'],
404+ 'identity': ['identity-service']}
405+ expected_result = 'identity'
406+
407+ result = openstack.incomplete_relation_data(configs, required_interfaces)
408+ self.assertTrue(expected_result in result.keys())
409+
410+ @patch('charmhelpers.contrib.openstack.utils.status_set')
411+ def test_set_os_workload_status_complete(self, status_set):
412+ configs = MagicMock()
413+ configs.complete_contexts.return_value = ['shared-db',
414+ 'amqp',
415+ 'identity-service']
416+ required_interfaces = {
417+ 'database': ['shared-db', 'pgsql-db'],
418+ 'message': ['amqp', 'zeromq-configuration'],
419+ 'identity': ['identity-service']}
420+
421+ openstack.set_os_workload_status(configs, required_interfaces)
422+ status_set.assert_called_with('active', 'Unit is ready')
423+
424+ @patch('charmhelpers.contrib.openstack.utils.incomplete_relation_data',
425+ return_value={'identity': {'identity-service': {'related': True}}})
426+ @patch('charmhelpers.contrib.openstack.utils.status_set')
427+ def test_set_os_workload_status_related_incomplete(self, status_set,
428+ incomplete_relation_data):
429+ configs = MagicMock()
430+ configs.complete_contexts.return_value = ['shared-db', 'amqp']
431+ required_interfaces = {
432+ 'database': ['shared-db', 'pgsql-db'],
433+ 'message': ['amqp', 'zeromq-configuration'],
434+ 'identity': ['identity-service']}
435+
436+ openstack.set_os_workload_status(configs, required_interfaces)
437+ status_set.assert_called_with('waiting',
438+ "Incomplete relations: identity")
439+
440+ @patch('charmhelpers.contrib.openstack.utils.incomplete_relation_data',
441+ return_value={'identity': {'identity-service': {'related': False}}})
442+ @patch('charmhelpers.contrib.openstack.utils.status_set')
443+ def test_set_os_workload_status_absent(self, status_set,
444+ incomplete_relation_data):
445+ configs = MagicMock()
446+ configs.complete_contexts.return_value = ['shared-db', 'amqp']
447+ required_interfaces = {
448+ 'database': ['shared-db', 'pgsql-db'],
449+ 'message': ['amqp', 'zeromq-configuration'],
450+ 'identity': ['identity-service']}
451+
452+ openstack.set_os_workload_status(configs, required_interfaces)
453+ status_set.assert_called_with('blocked',
454+ 'Missing relations: identity')
455+
456+ @patch('charmhelpers.contrib.openstack.utils.hook_name',
457+ return_value='identity-service-relation-broken')
458+ @patch('charmhelpers.contrib.openstack.utils.incomplete_relation_data',
459+ return_value={'identity': {'identity-service': {'related': True}}})
460+ @patch('charmhelpers.contrib.openstack.utils.status_set')
461+ def test_set_os_workload_status_related_broken(self, status_set,
462+ incomplete_relation_data,
463+ hook_name):
464+ configs = MagicMock()
465+ configs.complete_contexts.return_value = ['shared-db', 'amqp']
466+ required_interfaces = {
467+ 'database': ['shared-db', 'pgsql-db'],
468+ 'message': ['amqp', 'zeromq-configuration'],
469+ 'identity': ['identity-service']}
470+
471+ openstack.set_os_workload_status(configs, required_interfaces)
472+ status_set.assert_called_with('blocked',
473+ "Missing relations: identity")
474+
475+ @patch('charmhelpers.contrib.openstack.utils.incomplete_relation_data',
476+ return_value={'identity':
477+ {'identity-service': {'related': True}},
478+
479+ 'message':
480+ {'amqp': {'missing_data': ['rabbitmq-password'],
481+ 'related': True}},
482+
483+ 'database':
484+ {'shared-db': {'related': False}}
485+ })
486+ @patch('charmhelpers.contrib.openstack.utils.status_set')
487+ def test_set_os_workload_status_mixed(self, status_set, incomplete_relation_data):
488+ configs = MagicMock()
489+ configs.complete_contexts.return_value = ['shared-db', 'amqp']
490+ required_interfaces = {
491+ 'database': ['shared-db', 'pgsql-db'],
492+ 'message': ['amqp', 'zeromq-configuration'],
493+ 'identity': ['identity-service']}
494+
495+ openstack.set_os_workload_status(configs, required_interfaces)
496+ status_set.assert_called_with('blocked',
497+ "Missing relations: database; "
498+ "incomplete relations: message, "
499+ "identity")
500+
501 if __name__ == '__main__':
502 unittest.main()
503
504=== modified file 'tests/contrib/openstack/test_os_contexts.py'
505--- tests/contrib/openstack/test_os_contexts.py 2015-09-11 12:19:23 +0000
506+++ tests/contrib/openstack/test_os_contexts.py 2015-09-15 17:38:07 +0000
507@@ -2594,7 +2594,7 @@
508 self.related_units.return_value = []
509 self.assertEquals(context.NetworkServiceContext()(), {})
510
511- @patch.object(context, 'context_complete')
512+ @patch.object(context.OSContextGenerator, 'context_complete')
513 def test_network_service_ctxt_no_data(self, mock_context_complete):
514 rel = FakeRelation(QUANTUM_NETWORK_SERVICE_RELATION)
515 self.relation_ids.side_effect = rel.relation_ids

Subscribers

People subscribed via source and target branches