Merge lp:~le-charmers/charms/trusty/glance/leadership-election into lp:~openstack-charmers-archive/charms/trusty/glance/next

Proposed by Edward Hope-Morley
Status: Merged
Merged at revision: 118
Proposed branch: lp:~le-charmers/charms/trusty/glance/leadership-election
Merge into: lp:~openstack-charmers-archive/charms/trusty/glance/next
Diff against target: 590 lines (+246/-39)
10 files modified
hooks/charmhelpers/contrib/hahelpers/cluster.py (+37/-2)
hooks/charmhelpers/contrib/openstack/neutron.py (+10/-5)
hooks/charmhelpers/core/hookenv.py (+147/-10)
hooks/charmhelpers/core/host.py (+1/-1)
hooks/charmhelpers/core/services/base.py (+32/-11)
hooks/charmhelpers/fetch/__init__.py (+1/-1)
hooks/glance_relations.py (+3/-3)
hooks/glance_utils.py (+9/-2)
unit_tests/test_glance_relations.py (+3/-1)
unit_tests/test_glance_utils.py (+3/-3)
To merge this branch: bzr merge lp:~le-charmers/charms/trusty/glance/leadership-election
Reviewer Review Type Date Requested Status
OpenStack Charmers Pending
Review via email: mp+255008@code.launchpad.net
To post a comment you must log in.
113. By Liam Young

Merged trunk in + LE charmhelper sync

114. By Liam Young

Fix lint and unit tests

115. By Liam Young

Resync le charm helpers

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'hooks/charmhelpers/contrib/hahelpers/cluster.py'
2--- hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-03-20 17:15:02 +0000
3+++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-06-04 08:44:51 +0000
4@@ -44,6 +44,7 @@
5 ERROR,
6 WARNING,
7 unit_get,
8+ is_leader as juju_is_leader
9 )
10 from charmhelpers.core.decorators import (
11 retry_on_exception,
12@@ -52,6 +53,8 @@
13 bool_from_string,
14 )
15
16+DC_RESOURCE_NAME = 'DC'
17+
18
19 class HAIncompleteConfig(Exception):
20 pass
21@@ -66,12 +69,21 @@
22 Returns True if the charm executing this is the elected cluster leader.
23
24 It relies on two mechanisms to determine leadership:
25- 1. If the charm is part of a corosync cluster, call corosync to
26+ 1. If juju is sufficiently new and leadership election is supported,
27+ the is_leader command will be used.
28+ 2. If the charm is part of a corosync cluster, call corosync to
29 determine leadership.
30- 2. If the charm is not part of a corosync cluster, the leader is
31+ 3. If the charm is not part of a corosync cluster, the leader is
32 determined as being "the alive unit with the lowest unit numer". In
33 other words, the oldest surviving unit.
34 """
35+ try:
36+ return juju_is_leader()
37+ except NotImplementedError:
38+ log('Juju leadership election feature not enabled'
39+ ', using fallback support',
40+ level=WARNING)
41+
42 if is_clustered():
43 if not is_crm_leader(resource):
44 log('Deferring action to CRM leader.', level=INFO)
45@@ -95,6 +107,27 @@
46 return False
47
48
49+def is_crm_dc():
50+ """
51+ Determine leadership by querying the pacemaker Designated Controller
52+ """
53+ cmd = ['crm', 'status']
54+ try:
55+ status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
56+ if not isinstance(status, six.text_type):
57+ status = six.text_type(status, "utf-8")
58+ except subprocess.CalledProcessError:
59+ return False
60+ current_dc = ''
61+ for line in status.split('\n'):
62+ if line.startswith('Current DC'):
63+ # Current DC: juju-lytrusty-machine-2 (168108163) - partition with quorum
64+ current_dc = line.split(':')[1].split()[0]
65+ if current_dc == get_unit_hostname():
66+ return True
67+ return False
68+
69+
70 @retry_on_exception(5, base_delay=2, exc_type=CRMResourceNotFound)
71 def is_crm_leader(resource, retry=False):
72 """
73@@ -104,6 +137,8 @@
74 We allow this operation to be retried to avoid the possibility of getting a
75 false negative. See LP #1396246 for more info.
76 """
77+ if resource == DC_RESOURCE_NAME:
78+ return is_crm_dc()
79 cmd = ['crm', 'resource', 'show', resource]
80 try:
81 status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
82
83=== modified file 'hooks/charmhelpers/contrib/openstack/neutron.py'
84--- hooks/charmhelpers/contrib/openstack/neutron.py 2015-04-16 19:53:49 +0000
85+++ hooks/charmhelpers/contrib/openstack/neutron.py 2015-06-04 08:44:51 +0000
86@@ -256,11 +256,14 @@
87 def parse_mappings(mappings):
88 parsed = {}
89 if mappings:
90- mappings = mappings.split(' ')
91+ mappings = mappings.split()
92 for m in mappings:
93 p = m.partition(':')
94- if p[1] == ':':
95- parsed[p[0].strip()] = p[2].strip()
96+ key = p[0].strip()
97+ if p[1]:
98+ parsed[key] = p[2].strip()
99+ else:
100+ parsed[key] = ''
101
102 return parsed
103
104@@ -283,13 +286,13 @@
105 Returns dict of the form {bridge:port}.
106 """
107 _mappings = parse_mappings(mappings)
108- if not _mappings:
109+ if not _mappings or list(_mappings.values()) == ['']:
110 if not mappings:
111 return {}
112
113 # For backwards-compatibility we need to support port-only provided in
114 # config.
115- _mappings = {default_bridge: mappings.split(' ')[0]}
116+ _mappings = {default_bridge: mappings.split()[0]}
117
118 bridges = _mappings.keys()
119 ports = _mappings.values()
120@@ -309,6 +312,8 @@
121
122 Mappings must be a space-delimited list of provider:start:end mappings.
123
124+ The start:end range is optional and may be omitted.
125+
126 Returns dict of the form {provider: (start, end)}.
127 """
128 _mappings = parse_mappings(mappings)
129
130=== modified file 'hooks/charmhelpers/core/hookenv.py'
131--- hooks/charmhelpers/core/hookenv.py 2015-04-16 19:53:49 +0000
132+++ hooks/charmhelpers/core/hookenv.py 2015-06-04 08:44:51 +0000
133@@ -21,12 +21,14 @@
134 # Charm Helpers Developers <juju@lists.ubuntu.com>
135
136 from __future__ import print_function
137+from functools import wraps
138 import os
139 import json
140 import yaml
141 import subprocess
142 import sys
143 import errno
144+import tempfile
145 from subprocess import CalledProcessError
146
147 import six
148@@ -58,15 +60,17 @@
149
150 will cache the result of unit_get + 'test' for future calls.
151 """
152+ @wraps(func)
153 def wrapper(*args, **kwargs):
154 global cache
155 key = str((func, args, kwargs))
156 try:
157 return cache[key]
158 except KeyError:
159- res = func(*args, **kwargs)
160- cache[key] = res
161- return res
162+ pass # Drop out of the exception handler scope.
163+ res = func(*args, **kwargs)
164+ cache[key] = res
165+ return res
166 return wrapper
167
168
169@@ -178,7 +182,7 @@
170
171 def remote_unit():
172 """The remote unit for the current relation hook"""
173- return os.environ['JUJU_REMOTE_UNIT']
174+ return os.environ.get('JUJU_REMOTE_UNIT', None)
175
176
177 def service_name():
178@@ -250,6 +254,12 @@
179 except KeyError:
180 return (self._prev_dict or {})[key]
181
182+ def get(self, key, default=None):
183+ try:
184+ return self[key]
185+ except KeyError:
186+ return default
187+
188 def keys(self):
189 prev_keys = []
190 if self._prev_dict is not None:
191@@ -353,18 +363,49 @@
192 """Set relation information for the current unit"""
193 relation_settings = relation_settings if relation_settings else {}
194 relation_cmd_line = ['relation-set']
195+ accepts_file = "--file" in subprocess.check_output(
196+ relation_cmd_line + ["--help"], universal_newlines=True)
197 if relation_id is not None:
198 relation_cmd_line.extend(('-r', relation_id))
199- for k, v in (list(relation_settings.items()) + list(kwargs.items())):
200- if v is None:
201- relation_cmd_line.append('{}='.format(k))
202- else:
203- relation_cmd_line.append('{}={}'.format(k, v))
204- subprocess.check_call(relation_cmd_line)
205+ settings = relation_settings.copy()
206+ settings.update(kwargs)
207+ for key, value in settings.items():
208+ # Force value to be a string: it always should, but some call
209+ # sites pass in things like dicts or numbers.
210+ if value is not None:
211+ settings[key] = "{}".format(value)
212+ if accepts_file:
213+ # --file was introduced in Juju 1.23.2. Use it by default if
214+ # available, since otherwise we'll break if the relation data is
215+ # too big. Ideally we should tell relation-set to read the data from
216+ # stdin, but that feature is broken in 1.23.2: Bug #1454678.
217+ with tempfile.NamedTemporaryFile(delete=False) as settings_file:
218+ settings_file.write(yaml.safe_dump(settings).encode("utf-8"))
219+ subprocess.check_call(
220+ relation_cmd_line + ["--file", settings_file.name])
221+ os.remove(settings_file.name)
222+ else:
223+ for key, value in settings.items():
224+ if value is None:
225+ relation_cmd_line.append('{}='.format(key))
226+ else:
227+ relation_cmd_line.append('{}={}'.format(key, value))
228+ subprocess.check_call(relation_cmd_line)
229 # Flush cache of any relation-gets for local unit
230 flush(local_unit())
231
232
233+def relation_clear(r_id=None):
234+ ''' Clears any relation data already set on relation r_id '''
235+ settings = relation_get(rid=r_id,
236+ unit=local_unit())
237+ for setting in settings:
238+ if setting not in ['public-address', 'private-address']:
239+ settings[setting] = None
240+ relation_set(relation_id=r_id,
241+ **settings)
242+
243+
244 @cached
245 def relation_ids(reltype=None):
246 """A list of relation_ids"""
247@@ -509,6 +550,11 @@
248 return None
249
250
251+def unit_public_ip():
252+ """Get this unit's public IP address"""
253+ return unit_get('public-address')
254+
255+
256 def unit_private_ip():
257 """Get this unit's private IP address"""
258 return unit_get('private-address')
259@@ -605,3 +651,94 @@
260
261 The results set by action_set are preserved."""
262 subprocess.check_call(['action-fail', message])
263+
264+
265+def status_set(workload_state, message):
266+ """Set the workload state with a message
267+
268+ Use status-set to set the workload state with a message which is visible
269+ to the user via juju status. If the status-set command is not found then
270+ assume this is juju < 1.23 and juju-log the message unstead.
271+
272+ workload_state -- valid juju workload state.
273+ message -- status update message
274+ """
275+ valid_states = ['maintenance', 'blocked', 'waiting', 'active']
276+ if workload_state not in valid_states:
277+ raise ValueError(
278+ '{!r} is not a valid workload state'.format(workload_state)
279+ )
280+ cmd = ['status-set', workload_state, message]
281+ try:
282+ ret = subprocess.call(cmd)
283+ if ret == 0:
284+ return
285+ except OSError as e:
286+ if e.errno != errno.ENOENT:
287+ raise
288+ log_message = 'status-set failed: {} {}'.format(workload_state,
289+ message)
290+ log(log_message, level='INFO')
291+
292+
293+def status_get():
294+ """Retrieve the previously set juju workload state
295+
296+ If the status-set command is not found then assume this is juju < 1.23 and
297+ return 'unknown'
298+ """
299+ cmd = ['status-get']
300+ try:
301+ raw_status = subprocess.check_output(cmd, universal_newlines=True)
302+ status = raw_status.rstrip()
303+ return status
304+ except OSError as e:
305+ if e.errno == errno.ENOENT:
306+ return 'unknown'
307+ else:
308+ raise
309+
310+
311+def translate_exc(from_exc, to_exc):
312+ def inner_translate_exc1(f):
313+ def inner_translate_exc2(*args, **kwargs):
314+ try:
315+ return f(*args, **kwargs)
316+ except from_exc:
317+ raise to_exc
318+
319+ return inner_translate_exc2
320+
321+ return inner_translate_exc1
322+
323+
324+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
325+def is_leader():
326+ """Does the current unit hold the juju leadership
327+
328+ Uses juju to determine whether the current unit is the leader of its peers
329+ """
330+ cmd = ['is-leader', '--format=json']
331+ return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
332+
333+
334+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
335+def leader_get(attribute=None):
336+ """Juju leader get value(s)"""
337+ cmd = ['leader-get', '--format=json'] + [attribute or '-']
338+ return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
339+
340+
341+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
342+def leader_set(settings=None, **kwargs):
343+ """Juju leader set value(s)"""
344+ log("Juju leader-set '%s'" % (settings), level=DEBUG)
345+ cmd = ['leader-set']
346+ settings = settings or {}
347+ settings.update(kwargs)
348+ for k, v in settings.iteritems():
349+ if v is None:
350+ cmd.append('{}='.format(k))
351+ else:
352+ cmd.append('{}={}'.format(k, v))
353+ subprocess.check_call(cmd)
354
355=== modified file 'hooks/charmhelpers/core/host.py'
356--- hooks/charmhelpers/core/host.py 2015-03-20 17:15:02 +0000
357+++ hooks/charmhelpers/core/host.py 2015-06-04 08:44:51 +0000
358@@ -90,7 +90,7 @@
359 ['service', service_name, 'status'],
360 stderr=subprocess.STDOUT).decode('UTF-8')
361 except subprocess.CalledProcessError as e:
362- return 'unrecognized service' not in e.output
363+ return b'unrecognized service' not in e.output
364 else:
365 return True
366
367
368=== modified file 'hooks/charmhelpers/core/services/base.py'
369--- hooks/charmhelpers/core/services/base.py 2015-03-20 17:15:02 +0000
370+++ hooks/charmhelpers/core/services/base.py 2015-06-04 08:44:51 +0000
371@@ -15,9 +15,9 @@
372 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
373
374 import os
375-import re
376 import json
377-from collections import Iterable
378+from inspect import getargspec
379+from collections import Iterable, OrderedDict
380
381 from charmhelpers.core import host
382 from charmhelpers.core import hookenv
383@@ -119,7 +119,7 @@
384 """
385 self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
386 self._ready = None
387- self.services = {}
388+ self.services = OrderedDict()
389 for service in services or []:
390 service_name = service['service']
391 self.services[service_name] = service
392@@ -132,8 +132,8 @@
393 if hook_name == 'stop':
394 self.stop_services()
395 else:
396+ self.reconfigure_services()
397 self.provide_data()
398- self.reconfigure_services()
399 cfg = hookenv.config()
400 if cfg.implicit_save:
401 cfg.save()
402@@ -145,15 +145,36 @@
403 A provider must have a `name` attribute, which indicates which relation
404 to set data on, and a `provide_data()` method, which returns a dict of
405 data to set.
406+
407+ The `provide_data()` method can optionally accept two parameters:
408+
409+ * ``remote_service`` The name of the remote service that the data will
410+ be provided to. The `provide_data()` method will be called once
411+ for each connected service (not unit). This allows the method to
412+ tailor its data to the given service.
413+ * ``service_ready`` Whether or not the service definition had all of
414+ its requirements met, and thus the ``data_ready`` callbacks run.
415+
416+ Note that the ``provided_data`` methods are now called **after** the
417+ ``data_ready`` callbacks are run. This gives the ``data_ready`` callbacks
418+ a chance to generate any data necessary for the providing to the remote
419+ services.
420 """
421- hook_name = hookenv.hook_name()
422- for service in self.services.values():
423+ for service_name, service in self.services.items():
424+ service_ready = self.is_ready(service_name)
425 for provider in service.get('provided_data', []):
426- if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):
427- data = provider.provide_data()
428- _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data
429- if _ready:
430- hookenv.relation_set(None, data)
431+ for relid in hookenv.relation_ids(provider.name):
432+ units = hookenv.related_units(relid)
433+ if not units:
434+ continue
435+ remote_service = units[0].split('/')[0]
436+ argspec = getargspec(provider.provide_data)
437+ if len(argspec.args) > 1:
438+ data = provider.provide_data(remote_service, service_ready)
439+ else:
440+ data = provider.provide_data()
441+ if data:
442+ hookenv.relation_set(relid, data)
443
444 def reconfigure_services(self, *service_names):
445 """
446
447=== modified file 'hooks/charmhelpers/fetch/__init__.py'
448--- hooks/charmhelpers/fetch/__init__.py 2015-05-01 14:56:06 +0000
449+++ hooks/charmhelpers/fetch/__init__.py 2015-06-04 08:44:51 +0000
450@@ -158,7 +158,7 @@
451
452 def apt_cache(in_memory=True):
453 """Build and return an apt cache"""
454- import apt_pkg
455+ from apt import apt_pkg
456 apt_pkg.init()
457 if in_memory:
458 apt_pkg.config.set("Dir::Cache::pkgcache", "")
459
460=== modified file 'hooks/glance_relations.py'
461--- hooks/glance_relations.py 2015-04-30 17:06:12 +0000
462+++ hooks/glance_relations.py 2015-06-04 08:44:51 +0000
463@@ -53,7 +53,7 @@
464 filter_installed_packages
465 )
466 from charmhelpers.contrib.hahelpers.cluster import (
467- eligible_leader,
468+ is_elected_leader,
469 get_hacluster_config
470 )
471 from charmhelpers.contrib.openstack.utils import (
472@@ -160,7 +160,7 @@
473 if rel != "essex":
474 CONFIGS.write(GLANCE_API_CONF)
475
476- if eligible_leader(CLUSTER_RES):
477+ if is_elected_leader(CLUSTER_RES):
478 # Bugs 1353135 & 1187508. Dbs can appear to be ready before the units
479 # acl entry has been added. So, if the db supports passing a list of
480 # permitted units then check if we're in the list.
481@@ -194,7 +194,7 @@
482 if rel != "essex":
483 CONFIGS.write(GLANCE_API_CONF)
484
485- if eligible_leader(CLUSTER_RES):
486+ if is_elected_leader(CLUSTER_RES):
487 if rel == "essex":
488 status = call(['glance-manage', 'db_version'])
489 if status != 0:
490
491=== modified file 'hooks/glance_utils.py'
492--- hooks/glance_utils.py 2015-05-12 19:46:43 +0000
493+++ hooks/glance_utils.py 2015-06-04 08:44:51 +0000
494@@ -42,7 +42,7 @@
495 context,)
496
497 from charmhelpers.contrib.hahelpers.cluster import (
498- eligible_leader,
499+ is_elected_leader,
500 )
501
502 from charmhelpers.contrib.openstack.alternatives import install_alternative
503@@ -59,6 +59,10 @@
504
505 from charmhelpers.core.templating import render
506
507+from charmhelpers.core.decorators import (
508+ retry_on_exception,
509+)
510+
511 CLUSTER_RES = "grp_glance_vips"
512
513 PACKAGES = [
514@@ -219,6 +223,9 @@
515 return configs
516
517
518+# NOTE(jamespage): Retry deals with sync issues during one-shot HA deploys.
519+# mysql might be restarting or suchlike.
520+@retry_on_exception(5, base_delay=3, exc_type=subprocess.CalledProcessError)
521 def determine_packages():
522 packages = [] + PACKAGES
523
524@@ -266,7 +273,7 @@
525 configs.write_all()
526
527 [service_stop(s) for s in services()]
528- if eligible_leader(CLUSTER_RES):
529+ if is_elected_leader(CLUSTER_RES):
530 migrate_database()
531 [service_start(s) for s in services()]
532
533
534=== modified file 'unit_tests/test_glance_relations.py'
535--- unit_tests/test_glance_relations.py 2015-05-12 19:46:43 +0000
536+++ unit_tests/test_glance_relations.py 2015-06-04 08:44:51 +0000
537@@ -41,12 +41,13 @@
538 'restart_on_change',
539 'service_reload',
540 'service_stop',
541+ 'service_restart',
542 # charmhelpers.contrib.openstack.utils
543 'configure_installation_source',
544 'os_release',
545 'openstack_upgrade_available',
546 # charmhelpers.contrib.hahelpers.cluster_utils
547- 'eligible_leader',
548+ 'is_elected_leader',
549 # glance_utils
550 'restart_map',
551 'register_configs',
552@@ -431,6 +432,7 @@
553 self.assertEquals([call('/etc/glance/glance-api.conf'),
554 call(self.ceph_config_file())],
555 configs.write.call_args_list)
556+ self.service_restart.assert_called_with('glance-api')
557
558 @patch.object(relations, 'CONFIGS')
559 def test_ceph_broken(self, configs):
560
561=== modified file 'unit_tests/test_glance_utils.py'
562--- unit_tests/test_glance_utils.py 2015-05-08 12:35:25 +0000
563+++ unit_tests/test_glance_utils.py 2015-06-04 08:44:51 +0000
564@@ -16,7 +16,7 @@
565 'relation_ids',
566 'get_os_codename_install_source',
567 'configure_installation_source',
568- 'eligible_leader',
569+ 'is_elected_leader',
570 'templating',
571 'apt_update',
572 'apt_upgrade',
573@@ -153,7 +153,7 @@
574 git_requested.return_value = True
575 self.config.side_effect = None
576 self.config.return_value = 'cloud:precise-havana'
577- self.eligible_leader.return_value = True
578+ self.is_elected_leader.return_value = True
579 self.get_os_codename_install_source.return_value = 'havana'
580 configs = MagicMock()
581 utils.do_openstack_upgrade(configs)
582@@ -171,7 +171,7 @@
583 git_requested.return_value = True
584 self.config.side_effect = None
585 self.config.return_value = 'cloud:precise-havana'
586- self.eligible_leader.return_value = False
587+ self.is_elected_leader.return_value = False
588 self.get_os_codename_install_source.return_value = 'havana'
589 configs = MagicMock()
590 utils.do_openstack_upgrade(configs)

Subscribers

People subscribed via source and target branches