Merge lp:~thedac/charm-helpers/openstack-object-oriented-status into lp:charm-helpers
- openstack-object-oriented-status
- Merge into devel
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Liam Young (community) | Approve | ||
David Ames (community) | Needs Resubmitting | ||
Review via email:
|
Commit message
Object oriented workload status for openstack charms
Description of the change
Object oriented workload status for openstack charms
- 407. By David Ames
-
Rename and add charm specific function to avoid clobbering
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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_
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.
- 408. By David Ames
-
Rename incomplete_contexts incomplete_
relation_ data
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Liam Young (gnuoy) wrote : | # |
A couple of nits (see diff comments).
- 409. By David Ames
-
More renames
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
David Ames (thedac) wrote : | # |
Liam, please take another look
- 410. By David Ames
-
Unit is ready
Preview Diff
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 |
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.