Merge lp:~brad-marshall/charms/trusty/glance-simplestreams-sync/fix-nagios into lp:charms/trusty/glance-simplestreams-sync

Proposed by Brad Marshall
Status: Merged
Merged at revision: 52
Proposed branch: lp:~brad-marshall/charms/trusty/glance-simplestreams-sync/fix-nagios
Merge into: lp:charms/trusty/glance-simplestreams-sync
Diff against target: 6768 lines (+4588/-544)
47 files modified
bin/charm_helpers_sync.py (+253/-0)
charm-helpers-sync.yaml (+2/-0)
config.yaml (+17/-1)
hooks/charmhelpers/__init__.py (+38/-0)
hooks/charmhelpers/contrib/__init__.py (+15/-0)
hooks/charmhelpers/contrib/charmsupport/IMPORT (+0/-14)
hooks/charmhelpers/contrib/charmsupport/__init__.py (+15/-0)
hooks/charmhelpers/contrib/charmsupport/nrpe.py (+149/-7)
hooks/charmhelpers/contrib/charmsupport/volumes.py (+21/-2)
hooks/charmhelpers/contrib/network/__init__.py (+15/-0)
hooks/charmhelpers/contrib/network/ip.py (+407/-26)
hooks/charmhelpers/contrib/openstack/__init__.py (+15/-0)
hooks/charmhelpers/contrib/openstack/alternatives.py (+16/-0)
hooks/charmhelpers/contrib/openstack/amulet/__init__.py (+15/-0)
hooks/charmhelpers/contrib/openstack/amulet/deployment.py (+111/-0)
hooks/charmhelpers/contrib/openstack/amulet/utils.py (+294/-0)
hooks/charmhelpers/contrib/openstack/context.py (+611/-205)
hooks/charmhelpers/contrib/openstack/files/__init__.py (+18/-0)
hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh (+32/-0)
hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh (+30/-0)
hooks/charmhelpers/contrib/openstack/ip.py (+146/-0)
hooks/charmhelpers/contrib/openstack/neutron.py (+55/-3)
hooks/charmhelpers/contrib/openstack/templates/__init__.py (+16/-0)
hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg (+30/-8)
hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend (+9/-8)
hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend.conf (+9/-8)
hooks/charmhelpers/contrib/openstack/templates/zeromq (+14/-0)
hooks/charmhelpers/contrib/openstack/templating.py (+43/-28)
hooks/charmhelpers/contrib/openstack/utils.py (+206/-91)
hooks/charmhelpers/core/__init__.py (+15/-0)
hooks/charmhelpers/core/decorators.py (+57/-0)
hooks/charmhelpers/core/fstab.py (+33/-13)
hooks/charmhelpers/core/hookenv.py (+106/-36)
hooks/charmhelpers/core/host.py (+157/-36)
hooks/charmhelpers/core/services/__init__.py (+18/-0)
hooks/charmhelpers/core/services/base.py (+329/-0)
hooks/charmhelpers/core/services/helpers.py (+267/-0)
hooks/charmhelpers/core/strutils.py (+42/-0)
hooks/charmhelpers/core/sysctl.py (+56/-0)
hooks/charmhelpers/core/templating.py (+68/-0)
hooks/charmhelpers/core/unitdata.py (+477/-0)
hooks/charmhelpers/fetch/__init__.py (+129/-39)
hooks/charmhelpers/fetch/archiveurl.py (+115/-17)
hooks/charmhelpers/fetch/bzrurl.py (+30/-2)
hooks/charmhelpers/fetch/giturl.py (+71/-0)
hooks/hooks.py (+13/-0)
metadata.yaml (+3/-0)
To merge this branch: bzr merge lp:~brad-marshall/charms/trusty/glance-simplestreams-sync/fix-nagios
Reviewer Review Type Date Requested Status
Liam Young (community) Approve
OpenStack Charmers Pending
Review via email: mp+250708@code.launchpad.net

Description of the change

Synced charmhelpers, added nagios_servicegroup config option

To post a comment you must log in.
55. By Brad Marshall

[bradm] Add basic nagios checks

56. By Brad Marshall

[bradm] Add nrpe config update to config-changed and upgrade-charm hooks

57. By Brad Marshall

[bradm] Add charmhelpers.contrib.ip to sync

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

Approve

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'bin'
2=== added file 'bin/charm_helpers_sync.py'
3--- bin/charm_helpers_sync.py 1970-01-01 00:00:00 +0000
4+++ bin/charm_helpers_sync.py 2015-03-03 23:20:09 +0000
5@@ -0,0 +1,253 @@
6+#!/usr/bin/python
7+
8+# Copyright 2014-2015 Canonical Limited.
9+#
10+# This file is part of charm-helpers.
11+#
12+# charm-helpers is free software: you can redistribute it and/or modify
13+# it under the terms of the GNU Lesser General Public License version 3 as
14+# published by the Free Software Foundation.
15+#
16+# charm-helpers is distributed in the hope that it will be useful,
17+# but WITHOUT ANY WARRANTY; without even the implied warranty of
18+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+# GNU Lesser General Public License for more details.
20+#
21+# You should have received a copy of the GNU Lesser General Public License
22+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
23+
24+# Authors:
25+# Adam Gandelman <adamg@ubuntu.com>
26+
27+import logging
28+import optparse
29+import os
30+import subprocess
31+import shutil
32+import sys
33+import tempfile
34+import yaml
35+from fnmatch import fnmatch
36+
37+import six
38+
39+CHARM_HELPERS_BRANCH = 'lp:charm-helpers'
40+
41+
42+def parse_config(conf_file):
43+ if not os.path.isfile(conf_file):
44+ logging.error('Invalid config file: %s.' % conf_file)
45+ return False
46+ return yaml.load(open(conf_file).read())
47+
48+
49+def clone_helpers(work_dir, branch):
50+ dest = os.path.join(work_dir, 'charm-helpers')
51+ logging.info('Checking out %s to %s.' % (branch, dest))
52+ cmd = ['bzr', 'checkout', '--lightweight', branch, dest]
53+ subprocess.check_call(cmd)
54+ return dest
55+
56+
57+def _module_path(module):
58+ return os.path.join(*module.split('.'))
59+
60+
61+def _src_path(src, module):
62+ return os.path.join(src, 'charmhelpers', _module_path(module))
63+
64+
65+def _dest_path(dest, module):
66+ return os.path.join(dest, _module_path(module))
67+
68+
69+def _is_pyfile(path):
70+ return os.path.isfile(path + '.py')
71+
72+
73+def ensure_init(path):
74+ '''
75+ ensure directories leading up to path are importable, omitting
76+ parent directory, eg path='/hooks/helpers/foo'/:
77+ hooks/
78+ hooks/helpers/__init__.py
79+ hooks/helpers/foo/__init__.py
80+ '''
81+ for d, dirs, files in os.walk(os.path.join(*path.split('/')[:2])):
82+ _i = os.path.join(d, '__init__.py')
83+ if not os.path.exists(_i):
84+ logging.info('Adding missing __init__.py: %s' % _i)
85+ open(_i, 'wb').close()
86+
87+
88+def sync_pyfile(src, dest):
89+ src = src + '.py'
90+ src_dir = os.path.dirname(src)
91+ logging.info('Syncing pyfile: %s -> %s.' % (src, dest))
92+ if not os.path.exists(dest):
93+ os.makedirs(dest)
94+ shutil.copy(src, dest)
95+ if os.path.isfile(os.path.join(src_dir, '__init__.py')):
96+ shutil.copy(os.path.join(src_dir, '__init__.py'),
97+ dest)
98+ ensure_init(dest)
99+
100+
101+def get_filter(opts=None):
102+ opts = opts or []
103+ if 'inc=*' in opts:
104+ # do not filter any files, include everything
105+ return None
106+
107+ def _filter(dir, ls):
108+ incs = [opt.split('=').pop() for opt in opts if 'inc=' in opt]
109+ _filter = []
110+ for f in ls:
111+ _f = os.path.join(dir, f)
112+
113+ if not os.path.isdir(_f) and not _f.endswith('.py') and incs:
114+ if True not in [fnmatch(_f, inc) for inc in incs]:
115+ logging.debug('Not syncing %s, does not match include '
116+ 'filters (%s)' % (_f, incs))
117+ _filter.append(f)
118+ else:
119+ logging.debug('Including file, which matches include '
120+ 'filters (%s): %s' % (incs, _f))
121+ elif (os.path.isfile(_f) and not _f.endswith('.py')):
122+ logging.debug('Not syncing file: %s' % f)
123+ _filter.append(f)
124+ elif (os.path.isdir(_f) and not
125+ os.path.isfile(os.path.join(_f, '__init__.py'))):
126+ logging.debug('Not syncing directory: %s' % f)
127+ _filter.append(f)
128+ return _filter
129+ return _filter
130+
131+
132+def sync_directory(src, dest, opts=None):
133+ if os.path.exists(dest):
134+ logging.debug('Removing existing directory: %s' % dest)
135+ shutil.rmtree(dest)
136+ logging.info('Syncing directory: %s -> %s.' % (src, dest))
137+
138+ shutil.copytree(src, dest, ignore=get_filter(opts))
139+ ensure_init(dest)
140+
141+
142+def sync(src, dest, module, opts=None):
143+
144+ # Sync charmhelpers/__init__.py for bootstrap code.
145+ sync_pyfile(_src_path(src, '__init__'), dest)
146+
147+ # Sync other __init__.py files in the path leading to module.
148+ m = []
149+ steps = module.split('.')[:-1]
150+ while steps:
151+ m.append(steps.pop(0))
152+ init = '.'.join(m + ['__init__'])
153+ sync_pyfile(_src_path(src, init),
154+ os.path.dirname(_dest_path(dest, init)))
155+
156+ # Sync the module, or maybe a .py file.
157+ if os.path.isdir(_src_path(src, module)):
158+ sync_directory(_src_path(src, module), _dest_path(dest, module), opts)
159+ elif _is_pyfile(_src_path(src, module)):
160+ sync_pyfile(_src_path(src, module),
161+ os.path.dirname(_dest_path(dest, module)))
162+ else:
163+ logging.warn('Could not sync: %s. Neither a pyfile or directory, '
164+ 'does it even exist?' % module)
165+
166+
167+def parse_sync_options(options):
168+ if not options:
169+ return []
170+ return options.split(',')
171+
172+
173+def extract_options(inc, global_options=None):
174+ global_options = global_options or []
175+ if global_options and isinstance(global_options, six.string_types):
176+ global_options = [global_options]
177+ if '|' not in inc:
178+ return (inc, global_options)
179+ inc, opts = inc.split('|')
180+ return (inc, parse_sync_options(opts) + global_options)
181+
182+
183+def sync_helpers(include, src, dest, options=None):
184+ if not os.path.isdir(dest):
185+ os.makedirs(dest)
186+
187+ global_options = parse_sync_options(options)
188+
189+ for inc in include:
190+ if isinstance(inc, str):
191+ inc, opts = extract_options(inc, global_options)
192+ sync(src, dest, inc, opts)
193+ elif isinstance(inc, dict):
194+ # could also do nested dicts here.
195+ for k, v in six.iteritems(inc):
196+ if isinstance(v, list):
197+ for m in v:
198+ inc, opts = extract_options(m, global_options)
199+ sync(src, dest, '%s.%s' % (k, inc), opts)
200+
201+if __name__ == '__main__':
202+ parser = optparse.OptionParser()
203+ parser.add_option('-c', '--config', action='store', dest='config',
204+ default=None, help='helper config file')
205+ parser.add_option('-D', '--debug', action='store_true', dest='debug',
206+ default=False, help='debug')
207+ parser.add_option('-b', '--branch', action='store', dest='branch',
208+ help='charm-helpers bzr branch (overrides config)')
209+ parser.add_option('-d', '--destination', action='store', dest='dest_dir',
210+ help='sync destination dir (overrides config)')
211+ (opts, args) = parser.parse_args()
212+
213+ if opts.debug:
214+ logging.basicConfig(level=logging.DEBUG)
215+ else:
216+ logging.basicConfig(level=logging.INFO)
217+
218+ if opts.config:
219+ logging.info('Loading charm helper config from %s.' % opts.config)
220+ config = parse_config(opts.config)
221+ if not config:
222+ logging.error('Could not parse config from %s.' % opts.config)
223+ sys.exit(1)
224+ else:
225+ config = {}
226+
227+ if 'branch' not in config:
228+ config['branch'] = CHARM_HELPERS_BRANCH
229+ if opts.branch:
230+ config['branch'] = opts.branch
231+ if opts.dest_dir:
232+ config['destination'] = opts.dest_dir
233+
234+ if 'destination' not in config:
235+ logging.error('No destination dir. specified as option or config.')
236+ sys.exit(1)
237+
238+ if 'include' not in config:
239+ if not args:
240+ logging.error('No modules to sync specified as option or config.')
241+ sys.exit(1)
242+ config['include'] = []
243+ [config['include'].append(a) for a in args]
244+
245+ sync_options = None
246+ if 'options' in config:
247+ sync_options = config['options']
248+ tmpd = tempfile.mkdtemp()
249+ try:
250+ checkout = clone_helpers(tmpd, config['branch'])
251+ sync_helpers(config['include'], checkout, config['destination'],
252+ options=sync_options)
253+ except Exception as e:
254+ logging.error("Could not sync: %s" % e)
255+ raise e
256+ finally:
257+ logging.debug('Cleaning up %s' % tmpd)
258+ shutil.rmtree(tmpd)
259
260=== modified file 'charm-helpers-sync.yaml'
261--- charm-helpers-sync.yaml 2014-06-18 17:30:47 +0000
262+++ charm-helpers-sync.yaml 2015-03-03 23:20:09 +0000
263@@ -4,4 +4,6 @@
264 - core
265 - fetch
266 - contrib.openstack|inc=*
267+ - contrib.charmsupport
268+ - contrib.network.ip
269
270
271=== modified file 'config.yaml'
272--- config.yaml 2014-06-18 18:43:42 +0000
273+++ config.yaml 2015-03-03 23:20:09 +0000
274@@ -28,4 +28,20 @@
275 cloud_name:
276 type: string
277 default: "glance-simplestreams-sync-openstack"
278- description: "Cloud name to be used in simplestreams index file"
279\ No newline at end of file
280+ description: "Cloud name to be used in simplestreams index file"
281+ nagios_context:
282+ default: "juju"
283+ type: string
284+ description: |
285+ Used by the nrpe-external-master subordinate charm.
286+ A string that will be prepended to instance name to set the host name
287+ in nagios. So for instance the hostname would be something like:
288+ juju-myservice-0
289+ If you're running multiple environments with the same services in them
290+ this allows you to differentiate between them.
291+ nagios_servicegroups:
292+ default: ""
293+ type: string
294+ description: |
295+ A comma-separated list of nagios servicegroups.
296+ If left empty, the nagios_context will be used as the servicegroup
297
298=== modified file 'hooks/charmhelpers/__init__.py'
299--- hooks/charmhelpers/__init__.py 2014-05-20 18:55:23 +0000
300+++ hooks/charmhelpers/__init__.py 2015-03-03 23:20:09 +0000
301@@ -0,0 +1,38 @@
302+# Copyright 2014-2015 Canonical Limited.
303+#
304+# This file is part of charm-helpers.
305+#
306+# charm-helpers is free software: you can redistribute it and/or modify
307+# it under the terms of the GNU Lesser General Public License version 3 as
308+# published by the Free Software Foundation.
309+#
310+# charm-helpers is distributed in the hope that it will be useful,
311+# but WITHOUT ANY WARRANTY; without even the implied warranty of
312+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
313+# GNU Lesser General Public License for more details.
314+#
315+# You should have received a copy of the GNU Lesser General Public License
316+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
317+
318+# Bootstrap charm-helpers, installing its dependencies if necessary using
319+# only standard libraries.
320+import subprocess
321+import sys
322+
323+try:
324+ import six # flake8: noqa
325+except ImportError:
326+ if sys.version_info.major == 2:
327+ subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
328+ else:
329+ subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
330+ import six # flake8: noqa
331+
332+try:
333+ import yaml # flake8: noqa
334+except ImportError:
335+ if sys.version_info.major == 2:
336+ subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
337+ else:
338+ subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
339+ import yaml # flake8: noqa
340
341=== modified file 'hooks/charmhelpers/contrib/__init__.py'
342--- hooks/charmhelpers/contrib/__init__.py 2014-05-20 18:55:23 +0000
343+++ hooks/charmhelpers/contrib/__init__.py 2015-03-03 23:20:09 +0000
344@@ -0,0 +1,15 @@
345+# Copyright 2014-2015 Canonical Limited.
346+#
347+# This file is part of charm-helpers.
348+#
349+# charm-helpers is free software: you can redistribute it and/or modify
350+# it under the terms of the GNU Lesser General Public License version 3 as
351+# published by the Free Software Foundation.
352+#
353+# charm-helpers is distributed in the hope that it will be useful,
354+# but WITHOUT ANY WARRANTY; without even the implied warranty of
355+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
356+# GNU Lesser General Public License for more details.
357+#
358+# You should have received a copy of the GNU Lesser General Public License
359+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
360
361=== removed file 'hooks/charmhelpers/contrib/charmsupport/IMPORT'
362--- hooks/charmhelpers/contrib/charmsupport/IMPORT 2014-05-20 18:55:23 +0000
363+++ hooks/charmhelpers/contrib/charmsupport/IMPORT 1970-01-01 00:00:00 +0000
364@@ -1,14 +0,0 @@
365-Source: lp:charmsupport/trunk
366-
367-charmsupport/charmsupport/execd.py -> charm-helpers/charmhelpers/contrib/charmsupport/execd.py
368-charmsupport/charmsupport/hookenv.py -> charm-helpers/charmhelpers/contrib/charmsupport/hookenv.py
369-charmsupport/charmsupport/host.py -> charm-helpers/charmhelpers/contrib/charmsupport/host.py
370-charmsupport/charmsupport/nrpe.py -> charm-helpers/charmhelpers/contrib/charmsupport/nrpe.py
371-charmsupport/charmsupport/volumes.py -> charm-helpers/charmhelpers/contrib/charmsupport/volumes.py
372-
373-charmsupport/tests/test_execd.py -> charm-helpers/tests/contrib/charmsupport/test_execd.py
374-charmsupport/tests/test_hookenv.py -> charm-helpers/tests/contrib/charmsupport/test_hookenv.py
375-charmsupport/tests/test_host.py -> charm-helpers/tests/contrib/charmsupport/test_host.py
376-charmsupport/tests/test_nrpe.py -> charm-helpers/tests/contrib/charmsupport/test_nrpe.py
377-
378-charmsupport/bin/charmsupport -> charm-helpers/bin/contrib/charmsupport/charmsupport
379
380=== modified file 'hooks/charmhelpers/contrib/charmsupport/__init__.py'
381--- hooks/charmhelpers/contrib/charmsupport/__init__.py 2014-05-20 18:55:23 +0000
382+++ hooks/charmhelpers/contrib/charmsupport/__init__.py 2015-03-03 23:20:09 +0000
383@@ -0,0 +1,15 @@
384+# Copyright 2014-2015 Canonical Limited.
385+#
386+# This file is part of charm-helpers.
387+#
388+# charm-helpers is free software: you can redistribute it and/or modify
389+# it under the terms of the GNU Lesser General Public License version 3 as
390+# published by the Free Software Foundation.
391+#
392+# charm-helpers is distributed in the hope that it will be useful,
393+# but WITHOUT ANY WARRANTY; without even the implied warranty of
394+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
395+# GNU Lesser General Public License for more details.
396+#
397+# You should have received a copy of the GNU Lesser General Public License
398+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
399
400=== modified file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py'
401--- hooks/charmhelpers/contrib/charmsupport/nrpe.py 2014-05-20 18:55:23 +0000
402+++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-03-03 23:20:09 +0000
403@@ -1,3 +1,19 @@
404+# Copyright 2014-2015 Canonical Limited.
405+#
406+# This file is part of charm-helpers.
407+#
408+# charm-helpers is free software: you can redistribute it and/or modify
409+# it under the terms of the GNU Lesser General Public License version 3 as
410+# published by the Free Software Foundation.
411+#
412+# charm-helpers is distributed in the hope that it will be useful,
413+# but WITHOUT ANY WARRANTY; without even the implied warranty of
414+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
415+# GNU Lesser General Public License for more details.
416+#
417+# You should have received a copy of the GNU Lesser General Public License
418+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
419+
420 """Compatibility with the nrpe-external-master charm"""
421 # Copyright 2012 Canonical Ltd.
422 #
423@@ -8,6 +24,8 @@
424 import pwd
425 import grp
426 import os
427+import glob
428+import shutil
429 import re
430 import shlex
431 import yaml
432@@ -18,6 +36,7 @@
433 log,
434 relation_ids,
435 relation_set,
436+ relations_of_type,
437 )
438
439 from charmhelpers.core.host import service
440@@ -54,6 +73,12 @@
441 # juju-myservice-0
442 # If you're running multiple environments with the same services in them
443 # this allows you to differentiate between them.
444+# nagios_servicegroups:
445+# default: ""
446+# type: string
447+# description: |
448+# A comma-separated list of nagios servicegroups.
449+# If left empty, the nagios_context will be used as the servicegroup
450 #
451 # 3. Add custom checks (Nagios plugins) to files/nrpe-external-master
452 #
453@@ -138,7 +163,7 @@
454 log('Check command not found: {}'.format(parts[0]))
455 return ''
456
457- def write(self, nagios_context, hostname):
458+ def write(self, nagios_context, hostname, nagios_servicegroups):
459 nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format(
460 self.command)
461 with open(nrpe_check_file, 'w') as nrpe_check_config:
462@@ -150,16 +175,18 @@
463 log('Not writing service config as {} is not accessible'.format(
464 NRPE.nagios_exportdir))
465 else:
466- self.write_service_config(nagios_context, hostname)
467+ self.write_service_config(nagios_context, hostname,
468+ nagios_servicegroups)
469
470- def write_service_config(self, nagios_context, hostname):
471+ def write_service_config(self, nagios_context, hostname,
472+ nagios_servicegroups):
473 for f in os.listdir(NRPE.nagios_exportdir):
474 if re.search('.*{}.cfg'.format(self.command), f):
475 os.remove(os.path.join(NRPE.nagios_exportdir, f))
476
477 templ_vars = {
478 'nagios_hostname': hostname,
479- 'nagios_servicegroup': nagios_context,
480+ 'nagios_servicegroup': nagios_servicegroups,
481 'description': self.description,
482 'shortname': self.shortname,
483 'command': self.command,
484@@ -179,12 +206,19 @@
485 nagios_exportdir = '/var/lib/nagios/export'
486 nrpe_confdir = '/etc/nagios/nrpe.d'
487
488- def __init__(self):
489+ def __init__(self, hostname=None):
490 super(NRPE, self).__init__()
491 self.config = config()
492 self.nagios_context = self.config['nagios_context']
493+ if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']:
494+ self.nagios_servicegroups = self.config['nagios_servicegroups']
495+ else:
496+ self.nagios_servicegroups = self.nagios_context
497 self.unit_name = local_unit().replace('/', '-')
498- self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
499+ if hostname:
500+ self.hostname = hostname
501+ else:
502+ self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
503 self.checks = []
504
505 def add_check(self, *args, **kwargs):
506@@ -205,7 +239,8 @@
507 nrpe_monitors = {}
508 monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}}
509 for nrpecheck in self.checks:
510- nrpecheck.write(self.nagios_context, self.hostname)
511+ nrpecheck.write(self.nagios_context, self.hostname,
512+ self.nagios_servicegroups)
513 nrpe_monitors[nrpecheck.shortname] = {
514 "command": nrpecheck.command,
515 }
516@@ -214,3 +249,110 @@
517
518 for rid in relation_ids("local-monitors"):
519 relation_set(relation_id=rid, monitors=yaml.dump(monitors))
520+
521+
522+def get_nagios_hostcontext(relation_name='nrpe-external-master'):
523+ """
524+ Query relation with nrpe subordinate, return the nagios_host_context
525+
526+ :param str relation_name: Name of relation nrpe sub joined to
527+ """
528+ for rel in relations_of_type(relation_name):
529+ if 'nagios_hostname' in rel:
530+ return rel['nagios_host_context']
531+
532+
533+def get_nagios_hostname(relation_name='nrpe-external-master'):
534+ """
535+ Query relation with nrpe subordinate, return the nagios_hostname
536+
537+ :param str relation_name: Name of relation nrpe sub joined to
538+ """
539+ for rel in relations_of_type(relation_name):
540+ if 'nagios_hostname' in rel:
541+ return rel['nagios_hostname']
542+
543+
544+def get_nagios_unit_name(relation_name='nrpe-external-master'):
545+ """
546+ Return the nagios unit name prepended with host_context if needed
547+
548+ :param str relation_name: Name of relation nrpe sub joined to
549+ """
550+ host_context = get_nagios_hostcontext(relation_name)
551+ if host_context:
552+ unit = "%s:%s" % (host_context, local_unit())
553+ else:
554+ unit = local_unit()
555+ return unit
556+
557+
558+def add_init_service_checks(nrpe, services, unit_name):
559+ """
560+ Add checks for each service in list
561+
562+ :param NRPE nrpe: NRPE object to add check to
563+ :param list services: List of services to check
564+ :param str unit_name: Unit name to use in check description
565+ """
566+ for svc in services:
567+ upstart_init = '/etc/init/%s.conf' % svc
568+ sysv_init = '/etc/init.d/%s' % svc
569+ if os.path.exists(upstart_init):
570+ nrpe.add_check(
571+ shortname=svc,
572+ description='process check {%s}' % unit_name,
573+ check_cmd='check_upstart_job %s' % svc
574+ )
575+ elif os.path.exists(sysv_init):
576+ cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
577+ cron_file = ('*/5 * * * * root '
578+ '/usr/local/lib/nagios/plugins/check_exit_status.pl '
579+ '-s /etc/init.d/%s status > '
580+ '/var/lib/nagios/service-check-%s.txt\n' % (svc,
581+ svc)
582+ )
583+ f = open(cronpath, 'w')
584+ f.write(cron_file)
585+ f.close()
586+ nrpe.add_check(
587+ shortname=svc,
588+ description='process check {%s}' % unit_name,
589+ check_cmd='check_status_file.py -f '
590+ '/var/lib/nagios/service-check-%s.txt' % svc,
591+ )
592+
593+
594+def copy_nrpe_checks():
595+ """
596+ Copy the nrpe checks into place
597+
598+ """
599+ NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
600+ nrpe_files_dir = os.path.join(os.getenv('CHARM_DIR'), 'hooks',
601+ 'charmhelpers', 'contrib', 'openstack',
602+ 'files')
603+
604+ if not os.path.exists(NAGIOS_PLUGINS):
605+ os.makedirs(NAGIOS_PLUGINS)
606+ for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")):
607+ if os.path.isfile(fname):
608+ shutil.copy2(fname,
609+ os.path.join(NAGIOS_PLUGINS, os.path.basename(fname)))
610+
611+
612+def add_haproxy_checks(nrpe, unit_name):
613+ """
614+ Add checks for each service in list
615+
616+ :param NRPE nrpe: NRPE object to add check to
617+ :param str unit_name: Unit name to use in check description
618+ """
619+ nrpe.add_check(
620+ shortname='haproxy_servers',
621+ description='Check HAProxy {%s}' % unit_name,
622+ check_cmd='check_haproxy.sh')
623+ nrpe.add_check(
624+ shortname='haproxy_queue',
625+ description='Check HAProxy queue depth {%s}' % unit_name,
626+ check_cmd='check_haproxy_queue_depth.sh')
627
628=== modified file 'hooks/charmhelpers/contrib/charmsupport/volumes.py'
629--- hooks/charmhelpers/contrib/charmsupport/volumes.py 2014-05-20 18:55:23 +0000
630+++ hooks/charmhelpers/contrib/charmsupport/volumes.py 2015-03-03 23:20:09 +0000
631@@ -1,8 +1,25 @@
632+# Copyright 2014-2015 Canonical Limited.
633+#
634+# This file is part of charm-helpers.
635+#
636+# charm-helpers is free software: you can redistribute it and/or modify
637+# it under the terms of the GNU Lesser General Public License version 3 as
638+# published by the Free Software Foundation.
639+#
640+# charm-helpers is distributed in the hope that it will be useful,
641+# but WITHOUT ANY WARRANTY; without even the implied warranty of
642+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643+# GNU Lesser General Public License for more details.
644+#
645+# You should have received a copy of the GNU Lesser General Public License
646+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
647+
648 '''
649 Functions for managing volumes in juju units. One volume is supported per unit.
650 Subordinates may have their own storage, provided it is on its own partition.
651
652-Configuration stanzas:
653+Configuration stanzas::
654+
655 volume-ephemeral:
656 type: boolean
657 default: true
658@@ -20,7 +37,8 @@
659 is 'true' and no volume-map value is set. Use 'juju set' to set a
660 value and 'juju resolved' to complete configuration.
661
662-Usage:
663+Usage::
664+
665 from charmsupport.volumes import configure_volume, VolumeConfigurationError
666 from charmsupport.hookenv import log, ERROR
667 def post_mount_hook():
668@@ -34,6 +52,7 @@
669 after_change=post_mount_hook)
670 except VolumeConfigurationError:
671 log('Storage could not be configured', ERROR)
672+
673 '''
674
675 # XXX: Known limitations
676
677=== modified file 'hooks/charmhelpers/contrib/network/__init__.py'
678--- hooks/charmhelpers/contrib/network/__init__.py 2014-05-20 18:55:23 +0000
679+++ hooks/charmhelpers/contrib/network/__init__.py 2015-03-03 23:20:09 +0000
680@@ -0,0 +1,15 @@
681+# Copyright 2014-2015 Canonical Limited.
682+#
683+# This file is part of charm-helpers.
684+#
685+# charm-helpers is free software: you can redistribute it and/or modify
686+# it under the terms of the GNU Lesser General Public License version 3 as
687+# published by the Free Software Foundation.
688+#
689+# charm-helpers is distributed in the hope that it will be useful,
690+# but WITHOUT ANY WARRANTY; without even the implied warranty of
691+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
692+# GNU Lesser General Public License for more details.
693+#
694+# You should have received a copy of the GNU Lesser General Public License
695+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
696
697=== modified file 'hooks/charmhelpers/contrib/network/ip.py'
698--- hooks/charmhelpers/contrib/network/ip.py 2014-05-20 18:55:23 +0000
699+++ hooks/charmhelpers/contrib/network/ip.py 2015-03-03 23:20:09 +0000
700@@ -1,8 +1,32 @@
701-import sys
702-
703+# Copyright 2014-2015 Canonical Limited.
704+#
705+# This file is part of charm-helpers.
706+#
707+# charm-helpers is free software: you can redistribute it and/or modify
708+# it under the terms of the GNU Lesser General Public License version 3 as
709+# published by the Free Software Foundation.
710+#
711+# charm-helpers is distributed in the hope that it will be useful,
712+# but WITHOUT ANY WARRANTY; without even the implied warranty of
713+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
714+# GNU Lesser General Public License for more details.
715+#
716+# You should have received a copy of the GNU Lesser General Public License
717+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
718+
719+import glob
720+import re
721+import subprocess
722+import six
723+import socket
724+
725+from functools import partial
726+
727+from charmhelpers.core.hookenv import unit_get
728 from charmhelpers.fetch import apt_install
729 from charmhelpers.core.hookenv import (
730- ERROR, log,
731+ log,
732+ WARNING,
733 )
734
735 try:
736@@ -26,44 +50,401 @@
737 network)
738
739
740+def no_ip_found_error_out(network):
741+ errmsg = ("No IP address found in network: %s" % network)
742+ raise ValueError(errmsg)
743+
744+
745 def get_address_in_network(network, fallback=None, fatal=False):
746- """
747- Get an IPv4 address within the network from the host.
748-
749- Args:
750- network (str): CIDR presentation format. For example,
751- '192.168.1.0/24'.
752- fallback (str): If no address is found, return fallback.
753- fatal (boolean): If no address is found, fallback is not
754- set and fatal is True then exit(1).
755- """
756-
757- def not_found_error_out():
758- log("No IP address found in network: %s" % network,
759- level=ERROR)
760- sys.exit(1)
761-
762+ """Get an IPv4 or IPv6 address within the network from the host.
763+
764+ :param network (str): CIDR presentation format. For example,
765+ '192.168.1.0/24'.
766+ :param fallback (str): If no address is found, return fallback.
767+ :param fatal (boolean): If no address is found, fallback is not
768+ set and fatal is True then exit(1).
769+ """
770 if network is None:
771 if fallback is not None:
772 return fallback
773+
774+ if fatal:
775+ no_ip_found_error_out(network)
776 else:
777- if fatal:
778- not_found_error_out()
779+ return None
780
781 _validate_cidr(network)
782+ network = netaddr.IPNetwork(network)
783 for iface in netifaces.interfaces():
784 addresses = netifaces.ifaddresses(iface)
785- if netifaces.AF_INET in addresses:
786+ if network.version == 4 and netifaces.AF_INET in addresses:
787 addr = addresses[netifaces.AF_INET][0]['addr']
788 netmask = addresses[netifaces.AF_INET][0]['netmask']
789 cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask))
790- if cidr in netaddr.IPNetwork(network):
791+ if cidr in network:
792 return str(cidr.ip)
793
794+ if network.version == 6 and netifaces.AF_INET6 in addresses:
795+ for addr in addresses[netifaces.AF_INET6]:
796+ if not addr['addr'].startswith('fe80'):
797+ cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'],
798+ addr['netmask']))
799+ if cidr in network:
800+ return str(cidr.ip)
801+
802 if fallback is not None:
803 return fallback
804
805 if fatal:
806- not_found_error_out()
807-
808- return None
809+ no_ip_found_error_out(network)
810+
811+ return None
812+
813+
814+def is_ipv6(address):
815+ """Determine whether provided address is IPv6 or not."""
816+ try:
817+ address = netaddr.IPAddress(address)
818+ except netaddr.AddrFormatError:
819+ # probably a hostname - so not an address at all!
820+ return False
821+
822+ return address.version == 6
823+
824+
825+def is_address_in_network(network, address):
826+ """
827+ Determine whether the provided address is within a network range.
828+
829+ :param network (str): CIDR presentation format. For example,
830+ '192.168.1.0/24'.
831+ :param address: An individual IPv4 or IPv6 address without a net
832+ mask or subnet prefix. For example, '192.168.1.1'.
833+ :returns boolean: Flag indicating whether address is in network.
834+ """
835+ try:
836+ network = netaddr.IPNetwork(network)
837+ except (netaddr.core.AddrFormatError, ValueError):
838+ raise ValueError("Network (%s) is not in CIDR presentation format" %
839+ network)
840+
841+ try:
842+ address = netaddr.IPAddress(address)
843+ except (netaddr.core.AddrFormatError, ValueError):
844+ raise ValueError("Address (%s) is not in correct presentation format" %
845+ address)
846+
847+ if address in network:
848+ return True
849+ else:
850+ return False
851+
852+
853+def _get_for_address(address, key):
854+ """Retrieve an attribute of or the physical interface that
855+ the IP address provided could be bound to.
856+
857+ :param address (str): An individual IPv4 or IPv6 address without a net
858+ mask or subnet prefix. For example, '192.168.1.1'.
859+ :param key: 'iface' for the physical interface name or an attribute
860+ of the configured interface, for example 'netmask'.
861+ :returns str: Requested attribute or None if address is not bindable.
862+ """
863+ address = netaddr.IPAddress(address)
864+ for iface in netifaces.interfaces():
865+ addresses = netifaces.ifaddresses(iface)
866+ if address.version == 4 and netifaces.AF_INET in addresses:
867+ addr = addresses[netifaces.AF_INET][0]['addr']
868+ netmask = addresses[netifaces.AF_INET][0]['netmask']
869+ network = netaddr.IPNetwork("%s/%s" % (addr, netmask))
870+ cidr = network.cidr
871+ if address in cidr:
872+ if key == 'iface':
873+ return iface
874+ else:
875+ return addresses[netifaces.AF_INET][0][key]
876+
877+ if address.version == 6 and netifaces.AF_INET6 in addresses:
878+ for addr in addresses[netifaces.AF_INET6]:
879+ if not addr['addr'].startswith('fe80'):
880+ network = netaddr.IPNetwork("%s/%s" % (addr['addr'],
881+ addr['netmask']))
882+ cidr = network.cidr
883+ if address in cidr:
884+ if key == 'iface':
885+ return iface
886+ elif key == 'netmask' and cidr:
887+ return str(cidr).split('/')[1]
888+ else:
889+ return addr[key]
890+
891+ return None
892+
893+
894+get_iface_for_address = partial(_get_for_address, key='iface')
895+
896+
897+get_netmask_for_address = partial(_get_for_address, key='netmask')
898+
899+
900+def format_ipv6_addr(address):
901+ """If address is IPv6, wrap it in '[]' otherwise return None.
902+
903+ This is required by most configuration files when specifying IPv6
904+ addresses.
905+ """
906+ if is_ipv6(address):
907+ return "[%s]" % address
908+
909+ return None
910+
911+
912+def get_iface_addr(iface='eth0', inet_type='AF_INET', inc_aliases=False,
913+ fatal=True, exc_list=None):
914+ """Return the assigned IP address for a given interface, if any."""
915+ # Extract nic if passed /dev/ethX
916+ if '/' in iface:
917+ iface = iface.split('/')[-1]
918+
919+ if not exc_list:
920+ exc_list = []
921+
922+ try:
923+ inet_num = getattr(netifaces, inet_type)
924+ except AttributeError:
925+ raise Exception("Unknown inet type '%s'" % str(inet_type))
926+
927+ interfaces = netifaces.interfaces()
928+ if inc_aliases:
929+ ifaces = []
930+ for _iface in interfaces:
931+ if iface == _iface or _iface.split(':')[0] == iface:
932+ ifaces.append(_iface)
933+
934+ if fatal and not ifaces:
935+ raise Exception("Invalid interface '%s'" % iface)
936+
937+ ifaces.sort()
938+ else:
939+ if iface not in interfaces:
940+ if fatal:
941+ raise Exception("Interface '%s' not found " % (iface))
942+ else:
943+ return []
944+
945+ else:
946+ ifaces = [iface]
947+
948+ addresses = []
949+ for netiface in ifaces:
950+ net_info = netifaces.ifaddresses(netiface)
951+ if inet_num in net_info:
952+ for entry in net_info[inet_num]:
953+ if 'addr' in entry and entry['addr'] not in exc_list:
954+ addresses.append(entry['addr'])
955+
956+ if fatal and not addresses:
957+ raise Exception("Interface '%s' doesn't have any %s addresses." %
958+ (iface, inet_type))
959+
960+ return sorted(addresses)
961+
962+
963+get_ipv4_addr = partial(get_iface_addr, inet_type='AF_INET')
964+
965+
966+def get_iface_from_addr(addr):
967+ """Work out on which interface the provided address is configured."""
968+ for iface in netifaces.interfaces():
969+ addresses = netifaces.ifaddresses(iface)
970+ for inet_type in addresses:
971+ for _addr in addresses[inet_type]:
972+ _addr = _addr['addr']
973+ # link local
974+ ll_key = re.compile("(.+)%.*")
975+ raw = re.match(ll_key, _addr)
976+ if raw:
977+ _addr = raw.group(1)
978+
979+ if _addr == addr:
980+ log("Address '%s' is configured on iface '%s'" %
981+ (addr, iface))
982+ return iface
983+
984+ msg = "Unable to infer net iface on which '%s' is configured" % (addr)
985+ raise Exception(msg)
986+
987+
988+def sniff_iface(f):
989+ """Ensure decorated function is called with a value for iface.
990+
991+ If no iface provided, inject net iface inferred from unit private address.
992+ """
993+ def iface_sniffer(*args, **kwargs):
994+ if not kwargs.get('iface', None):
995+ kwargs['iface'] = get_iface_from_addr(unit_get('private-address'))
996+
997+ return f(*args, **kwargs)
998+
999+ return iface_sniffer
1000+
1001+
1002+@sniff_iface
1003+def get_ipv6_addr(iface=None, inc_aliases=False, fatal=True, exc_list=None,
1004+ dynamic_only=True):
1005+ """Get assigned IPv6 address for a given interface.
1006+
1007+ Returns list of addresses found. If no address found, returns empty list.
1008+
1009+ If iface is None, we infer the current primary interface by doing a reverse
1010+ lookup on the unit private-address.
1011+
1012+ We currently only support scope global IPv6 addresses i.e. non-temporary
1013+ addresses. If no global IPv6 address is found, return the first one found
1014+ in the ipv6 address list.
1015+ """
1016+ addresses = get_iface_addr(iface=iface, inet_type='AF_INET6',
1017+ inc_aliases=inc_aliases, fatal=fatal,
1018+ exc_list=exc_list)
1019+
1020+ if addresses:
1021+ global_addrs = []
1022+ for addr in addresses:
1023+ key_scope_link_local = re.compile("^fe80::..(.+)%(.+)")
1024+ m = re.match(key_scope_link_local, addr)
1025+ if m:
1026+ eui_64_mac = m.group(1)
1027+ iface = m.group(2)
1028+ else:
1029+ global_addrs.append(addr)
1030+
1031+ if global_addrs:
1032+ # Make sure any found global addresses are not temporary
1033+ cmd = ['ip', 'addr', 'show', iface]
1034+ out = subprocess.check_output(cmd).decode('UTF-8')
1035+ if dynamic_only:
1036+ key = re.compile("inet6 (.+)/[0-9]+ scope global dynamic.*")
1037+ else:
1038+ key = re.compile("inet6 (.+)/[0-9]+ scope global.*")
1039+
1040+ addrs = []
1041+ for line in out.split('\n'):
1042+ line = line.strip()
1043+ m = re.match(key, line)
1044+ if m and 'temporary' not in line:
1045+ # Return the first valid address we find
1046+ for addr in global_addrs:
1047+ if m.group(1) == addr:
1048+ if not dynamic_only or \
1049+ m.group(1).endswith(eui_64_mac):
1050+ addrs.append(addr)
1051+
1052+ if addrs:
1053+ return addrs
1054+
1055+ if fatal:
1056+ raise Exception("Interface '%s' does not have a scope global "
1057+ "non-temporary ipv6 address." % iface)
1058+
1059+ return []
1060+
1061+
1062+def get_bridges(vnic_dir='/sys/devices/virtual/net'):
1063+ """Return a list of bridges on the system."""
1064+ b_regex = "%s/*/bridge" % vnic_dir
1065+ return [x.replace(vnic_dir, '').split('/')[1] for x in glob.glob(b_regex)]
1066+
1067+
1068+def get_bridge_nics(bridge, vnic_dir='/sys/devices/virtual/net'):
1069+ """Return a list of nics comprising a given bridge on the system."""
1070+ brif_regex = "%s/%s/brif/*" % (vnic_dir, bridge)
1071+ return [x.split('/')[-1] for x in glob.glob(brif_regex)]
1072+
1073+
1074+def is_bridge_member(nic):
1075+ """Check if a given nic is a member of a bridge."""
1076+ for bridge in get_bridges():
1077+ if nic in get_bridge_nics(bridge):
1078+ return True
1079+
1080+ return False
1081+
1082+
1083+def is_ip(address):
1084+ """
1085+ Returns True if address is a valid IP address.
1086+ """
1087+ try:
1088+ # Test to see if already an IPv4 address
1089+ socket.inet_aton(address)
1090+ return True
1091+ except socket.error:
1092+ return False
1093+
1094+
1095+def ns_query(address):
1096+ try:
1097+ import dns.resolver
1098+ except ImportError:
1099+ apt_install('python-dnspython')
1100+ import dns.resolver
1101+
1102+ if isinstance(address, dns.name.Name):
1103+ rtype = 'PTR'
1104+ elif isinstance(address, six.string_types):
1105+ rtype = 'A'
1106+ else:
1107+ return None
1108+
1109+ answers = dns.resolver.query(address, rtype)
1110+ if answers:
1111+ return str(answers[0])
1112+ return None
1113+
1114+
1115+def get_host_ip(hostname, fallback=None):
1116+ """
1117+ Resolves the IP for a given hostname, or returns
1118+ the input if it is already an IP.
1119+ """
1120+ if is_ip(hostname):
1121+ return hostname
1122+
1123+ ip_addr = ns_query(hostname)
1124+ if not ip_addr:
1125+ try:
1126+ ip_addr = socket.gethostbyname(hostname)
1127+ except:
1128+ log("Failed to resolve hostname '%s'" % (hostname),
1129+ level=WARNING)
1130+ return fallback
1131+ return ip_addr
1132+
1133+
1134+def get_hostname(address, fqdn=True):
1135+ """
1136+ Resolves hostname for given IP, or returns the input
1137+ if it is already a hostname.
1138+ """
1139+ if is_ip(address):
1140+ try:
1141+ import dns.reversename
1142+ except ImportError:
1143+ apt_install("python-dnspython")
1144+ import dns.reversename
1145+
1146+ rev = dns.reversename.from_address(address)
1147+ result = ns_query(rev)
1148+ if not result:
1149+ return None
1150+ else:
1151+ result = address
1152+
1153+ if fqdn:
1154+ # strip trailing .
1155+ if result.endswith('.'):
1156+ return result[:-1]
1157+ else:
1158+ return result
1159+ else:
1160+ return result.split('.')[0]
1161
1162=== modified file 'hooks/charmhelpers/contrib/openstack/__init__.py'
1163--- hooks/charmhelpers/contrib/openstack/__init__.py 2014-05-20 18:55:23 +0000
1164+++ hooks/charmhelpers/contrib/openstack/__init__.py 2015-03-03 23:20:09 +0000
1165@@ -0,0 +1,15 @@
1166+# Copyright 2014-2015 Canonical Limited.
1167+#
1168+# This file is part of charm-helpers.
1169+#
1170+# charm-helpers is free software: you can redistribute it and/or modify
1171+# it under the terms of the GNU Lesser General Public License version 3 as
1172+# published by the Free Software Foundation.
1173+#
1174+# charm-helpers is distributed in the hope that it will be useful,
1175+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1176+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1177+# GNU Lesser General Public License for more details.
1178+#
1179+# You should have received a copy of the GNU Lesser General Public License
1180+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1181
1182=== modified file 'hooks/charmhelpers/contrib/openstack/alternatives.py'
1183--- hooks/charmhelpers/contrib/openstack/alternatives.py 2014-05-20 18:55:23 +0000
1184+++ hooks/charmhelpers/contrib/openstack/alternatives.py 2015-03-03 23:20:09 +0000
1185@@ -1,3 +1,19 @@
1186+# Copyright 2014-2015 Canonical Limited.
1187+#
1188+# This file is part of charm-helpers.
1189+#
1190+# charm-helpers is free software: you can redistribute it and/or modify
1191+# it under the terms of the GNU Lesser General Public License version 3 as
1192+# published by the Free Software Foundation.
1193+#
1194+# charm-helpers is distributed in the hope that it will be useful,
1195+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1196+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1197+# GNU Lesser General Public License for more details.
1198+#
1199+# You should have received a copy of the GNU Lesser General Public License
1200+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1201+
1202 ''' Helper for managing alternatives for file conflict resolution '''
1203
1204 import subprocess
1205
1206=== added directory 'hooks/charmhelpers/contrib/openstack/amulet'
1207=== added file 'hooks/charmhelpers/contrib/openstack/amulet/__init__.py'
1208--- hooks/charmhelpers/contrib/openstack/amulet/__init__.py 1970-01-01 00:00:00 +0000
1209+++ hooks/charmhelpers/contrib/openstack/amulet/__init__.py 2015-03-03 23:20:09 +0000
1210@@ -0,0 +1,15 @@
1211+# Copyright 2014-2015 Canonical Limited.
1212+#
1213+# This file is part of charm-helpers.
1214+#
1215+# charm-helpers is free software: you can redistribute it and/or modify
1216+# it under the terms of the GNU Lesser General Public License version 3 as
1217+# published by the Free Software Foundation.
1218+#
1219+# charm-helpers is distributed in the hope that it will be useful,
1220+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1221+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1222+# GNU Lesser General Public License for more details.
1223+#
1224+# You should have received a copy of the GNU Lesser General Public License
1225+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1226
1227=== added file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py'
1228--- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 1970-01-01 00:00:00 +0000
1229+++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-03-03 23:20:09 +0000
1230@@ -0,0 +1,111 @@
1231+# Copyright 2014-2015 Canonical Limited.
1232+#
1233+# This file is part of charm-helpers.
1234+#
1235+# charm-helpers is free software: you can redistribute it and/or modify
1236+# it under the terms of the GNU Lesser General Public License version 3 as
1237+# published by the Free Software Foundation.
1238+#
1239+# charm-helpers is distributed in the hope that it will be useful,
1240+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1241+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1242+# GNU Lesser General Public License for more details.
1243+#
1244+# You should have received a copy of the GNU Lesser General Public License
1245+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1246+
1247+import six
1248+from charmhelpers.contrib.amulet.deployment import (
1249+ AmuletDeployment
1250+)
1251+
1252+
1253+class OpenStackAmuletDeployment(AmuletDeployment):
1254+ """OpenStack amulet deployment.
1255+
1256+ This class inherits from AmuletDeployment and has additional support
1257+ that is specifically for use by OpenStack charms.
1258+ """
1259+
1260+ def __init__(self, series=None, openstack=None, source=None, stable=True):
1261+ """Initialize the deployment environment."""
1262+ super(OpenStackAmuletDeployment, self).__init__(series)
1263+ self.openstack = openstack
1264+ self.source = source
1265+ self.stable = stable
1266+ # Note(coreycb): this needs to be changed when new next branches come
1267+ # out.
1268+ self.current_next = "trusty"
1269+
1270+ def _determine_branch_locations(self, other_services):
1271+ """Determine the branch locations for the other services.
1272+
1273+ Determine if the local branch being tested is derived from its
1274+ stable or next (dev) branch, and based on this, use the corresonding
1275+ stable or next branches for the other_services."""
1276+ base_charms = ['mysql', 'mongodb', 'rabbitmq-server']
1277+
1278+ if self.stable:
1279+ for svc in other_services:
1280+ temp = 'lp:charms/{}'
1281+ svc['location'] = temp.format(svc['name'])
1282+ else:
1283+ for svc in other_services:
1284+ if svc['name'] in base_charms:
1285+ temp = 'lp:charms/{}'
1286+ svc['location'] = temp.format(svc['name'])
1287+ else:
1288+ temp = 'lp:~openstack-charmers/charms/{}/{}/next'
1289+ svc['location'] = temp.format(self.current_next,
1290+ svc['name'])
1291+ return other_services
1292+
1293+ def _add_services(self, this_service, other_services):
1294+ """Add services to the deployment and set openstack-origin/source."""
1295+ other_services = self._determine_branch_locations(other_services)
1296+
1297+ super(OpenStackAmuletDeployment, self)._add_services(this_service,
1298+ other_services)
1299+
1300+ services = other_services
1301+ services.append(this_service)
1302+ use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
1303+ 'ceph-osd', 'ceph-radosgw']
1304+ # Openstack subordinate charms do not expose an origin option as that
1305+ # is controlled by the principle
1306+ ignore = ['neutron-openvswitch']
1307+
1308+ if self.openstack:
1309+ for svc in services:
1310+ if svc['name'] not in use_source + ignore:
1311+ config = {'openstack-origin': self.openstack}
1312+ self.d.configure(svc['name'], config)
1313+
1314+ if self.source:
1315+ for svc in services:
1316+ if svc['name'] in use_source and svc['name'] not in ignore:
1317+ config = {'source': self.source}
1318+ self.d.configure(svc['name'], config)
1319+
1320+ def _configure_services(self, configs):
1321+ """Configure all of the services."""
1322+ for service, config in six.iteritems(configs):
1323+ self.d.configure(service, config)
1324+
1325+ def _get_openstack_release(self):
1326+ """Get openstack release.
1327+
1328+ Return an integer representing the enum value of the openstack
1329+ release.
1330+ """
1331+ (self.precise_essex, self.precise_folsom, self.precise_grizzly,
1332+ self.precise_havana, self.precise_icehouse,
1333+ self.trusty_icehouse) = range(6)
1334+ releases = {
1335+ ('precise', None): self.precise_essex,
1336+ ('precise', 'cloud:precise-folsom'): self.precise_folsom,
1337+ ('precise', 'cloud:precise-grizzly'): self.precise_grizzly,
1338+ ('precise', 'cloud:precise-havana'): self.precise_havana,
1339+ ('precise', 'cloud:precise-icehouse'): self.precise_icehouse,
1340+ ('trusty', None): self.trusty_icehouse}
1341+ return releases[(self.series, self.openstack)]
1342
1343=== added file 'hooks/charmhelpers/contrib/openstack/amulet/utils.py'
1344--- hooks/charmhelpers/contrib/openstack/amulet/utils.py 1970-01-01 00:00:00 +0000
1345+++ hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-03-03 23:20:09 +0000
1346@@ -0,0 +1,294 @@
1347+# Copyright 2014-2015 Canonical Limited.
1348+#
1349+# This file is part of charm-helpers.
1350+#
1351+# charm-helpers is free software: you can redistribute it and/or modify
1352+# it under the terms of the GNU Lesser General Public License version 3 as
1353+# published by the Free Software Foundation.
1354+#
1355+# charm-helpers is distributed in the hope that it will be useful,
1356+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1357+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1358+# GNU Lesser General Public License for more details.
1359+#
1360+# You should have received a copy of the GNU Lesser General Public License
1361+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1362+
1363+import logging
1364+import os
1365+import time
1366+import urllib
1367+
1368+import glanceclient.v1.client as glance_client
1369+import keystoneclient.v2_0 as keystone_client
1370+import novaclient.v1_1.client as nova_client
1371+
1372+import six
1373+
1374+from charmhelpers.contrib.amulet.utils import (
1375+ AmuletUtils
1376+)
1377+
1378+DEBUG = logging.DEBUG
1379+ERROR = logging.ERROR
1380+
1381+
1382+class OpenStackAmuletUtils(AmuletUtils):
1383+ """OpenStack amulet utilities.
1384+
1385+ This class inherits from AmuletUtils and has additional support
1386+ that is specifically for use by OpenStack charms.
1387+ """
1388+
1389+ def __init__(self, log_level=ERROR):
1390+ """Initialize the deployment environment."""
1391+ super(OpenStackAmuletUtils, self).__init__(log_level)
1392+
1393+ def validate_endpoint_data(self, endpoints, admin_port, internal_port,
1394+ public_port, expected):
1395+ """Validate endpoint data.
1396+
1397+ Validate actual endpoint data vs expected endpoint data. The ports
1398+ are used to find the matching endpoint.
1399+ """
1400+ found = False
1401+ for ep in endpoints:
1402+ self.log.debug('endpoint: {}'.format(repr(ep)))
1403+ if (admin_port in ep.adminurl and
1404+ internal_port in ep.internalurl and
1405+ public_port in ep.publicurl):
1406+ found = True
1407+ actual = {'id': ep.id,
1408+ 'region': ep.region,
1409+ 'adminurl': ep.adminurl,
1410+ 'internalurl': ep.internalurl,
1411+ 'publicurl': ep.publicurl,
1412+ 'service_id': ep.service_id}
1413+ ret = self._validate_dict_data(expected, actual)
1414+ if ret:
1415+ return 'unexpected endpoint data - {}'.format(ret)
1416+
1417+ if not found:
1418+ return 'endpoint not found'
1419+
1420+ def validate_svc_catalog_endpoint_data(self, expected, actual):
1421+ """Validate service catalog endpoint data.
1422+
1423+ Validate a list of actual service catalog endpoints vs a list of
1424+ expected service catalog endpoints.
1425+ """
1426+ self.log.debug('actual: {}'.format(repr(actual)))
1427+ for k, v in six.iteritems(expected):
1428+ if k in actual:
1429+ ret = self._validate_dict_data(expected[k][0], actual[k][0])
1430+ if ret:
1431+ return self.endpoint_error(k, ret)
1432+ else:
1433+ return "endpoint {} does not exist".format(k)
1434+ return ret
1435+
1436+ def validate_tenant_data(self, expected, actual):
1437+ """Validate tenant data.
1438+
1439+ Validate a list of actual tenant data vs list of expected tenant
1440+ data.
1441+ """
1442+ self.log.debug('actual: {}'.format(repr(actual)))
1443+ for e in expected:
1444+ found = False
1445+ for act in actual:
1446+ a = {'enabled': act.enabled, 'description': act.description,
1447+ 'name': act.name, 'id': act.id}
1448+ if e['name'] == a['name']:
1449+ found = True
1450+ ret = self._validate_dict_data(e, a)
1451+ if ret:
1452+ return "unexpected tenant data - {}".format(ret)
1453+ if not found:
1454+ return "tenant {} does not exist".format(e['name'])
1455+ return ret
1456+
1457+ def validate_role_data(self, expected, actual):
1458+ """Validate role data.
1459+
1460+ Validate a list of actual role data vs a list of expected role
1461+ data.
1462+ """
1463+ self.log.debug('actual: {}'.format(repr(actual)))
1464+ for e in expected:
1465+ found = False
1466+ for act in actual:
1467+ a = {'name': act.name, 'id': act.id}
1468+ if e['name'] == a['name']:
1469+ found = True
1470+ ret = self._validate_dict_data(e, a)
1471+ if ret:
1472+ return "unexpected role data - {}".format(ret)
1473+ if not found:
1474+ return "role {} does not exist".format(e['name'])
1475+ return ret
1476+
1477+ def validate_user_data(self, expected, actual):
1478+ """Validate user data.
1479+
1480+ Validate a list of actual user data vs a list of expected user
1481+ data.
1482+ """
1483+ self.log.debug('actual: {}'.format(repr(actual)))
1484+ for e in expected:
1485+ found = False
1486+ for act in actual:
1487+ a = {'enabled': act.enabled, 'name': act.name,
1488+ 'email': act.email, 'tenantId': act.tenantId,
1489+ 'id': act.id}
1490+ if e['name'] == a['name']:
1491+ found = True
1492+ ret = self._validate_dict_data(e, a)
1493+ if ret:
1494+ return "unexpected user data - {}".format(ret)
1495+ if not found:
1496+ return "user {} does not exist".format(e['name'])
1497+ return ret
1498+
1499+ def validate_flavor_data(self, expected, actual):
1500+ """Validate flavor data.
1501+
1502+ Validate a list of actual flavors vs a list of expected flavors.
1503+ """
1504+ self.log.debug('actual: {}'.format(repr(actual)))
1505+ act = [a.name for a in actual]
1506+ return self._validate_list_data(expected, act)
1507+
1508+ def tenant_exists(self, keystone, tenant):
1509+ """Return True if tenant exists."""
1510+ return tenant in [t.name for t in keystone.tenants.list()]
1511+
1512+ def authenticate_keystone_admin(self, keystone_sentry, user, password,
1513+ tenant):
1514+ """Authenticates admin user with the keystone admin endpoint."""
1515+ unit = keystone_sentry
1516+ service_ip = unit.relation('shared-db',
1517+ 'mysql:shared-db')['private-address']
1518+ ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))
1519+ return keystone_client.Client(username=user, password=password,
1520+ tenant_name=tenant, auth_url=ep)
1521+
1522+ def authenticate_keystone_user(self, keystone, user, password, tenant):
1523+ """Authenticates a regular user with the keystone public endpoint."""
1524+ ep = keystone.service_catalog.url_for(service_type='identity',
1525+ endpoint_type='publicURL')
1526+ return keystone_client.Client(username=user, password=password,
1527+ tenant_name=tenant, auth_url=ep)
1528+
1529+ def authenticate_glance_admin(self, keystone):
1530+ """Authenticates admin user with glance."""
1531+ ep = keystone.service_catalog.url_for(service_type='image',
1532+ endpoint_type='adminURL')
1533+ return glance_client.Client(ep, token=keystone.auth_token)
1534+
1535+ def authenticate_nova_user(self, keystone, user, password, tenant):
1536+ """Authenticates a regular user with nova-api."""
1537+ ep = keystone.service_catalog.url_for(service_type='identity',
1538+ endpoint_type='publicURL')
1539+ return nova_client.Client(username=user, api_key=password,
1540+ project_id=tenant, auth_url=ep)
1541+
1542+ def create_cirros_image(self, glance, image_name):
1543+ """Download the latest cirros image and upload it to glance."""
1544+ http_proxy = os.getenv('AMULET_HTTP_PROXY')
1545+ self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
1546+ if http_proxy:
1547+ proxies = {'http': http_proxy}
1548+ opener = urllib.FancyURLopener(proxies)
1549+ else:
1550+ opener = urllib.FancyURLopener()
1551+
1552+ f = opener.open("http://download.cirros-cloud.net/version/released")
1553+ version = f.read().strip()
1554+ cirros_img = "cirros-{}-x86_64-disk.img".format(version)
1555+ local_path = os.path.join('tests', cirros_img)
1556+
1557+ if not os.path.exists(local_path):
1558+ cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",
1559+ version, cirros_img)
1560+ opener.retrieve(cirros_url, local_path)
1561+ f.close()
1562+
1563+ with open(local_path) as f:
1564+ image = glance.images.create(name=image_name, is_public=True,
1565+ disk_format='qcow2',
1566+ container_format='bare', data=f)
1567+ count = 1
1568+ status = image.status
1569+ while status != 'active' and count < 10:
1570+ time.sleep(3)
1571+ image = glance.images.get(image.id)
1572+ status = image.status
1573+ self.log.debug('image status: {}'.format(status))
1574+ count += 1
1575+
1576+ if status != 'active':
1577+ self.log.error('image creation timed out')
1578+ return None
1579+
1580+ return image
1581+
1582+ def delete_image(self, glance, image):
1583+ """Delete the specified image."""
1584+ num_before = len(list(glance.images.list()))
1585+ glance.images.delete(image)
1586+
1587+ count = 1
1588+ num_after = len(list(glance.images.list()))
1589+ while num_after != (num_before - 1) and count < 10:
1590+ time.sleep(3)
1591+ num_after = len(list(glance.images.list()))
1592+ self.log.debug('number of images: {}'.format(num_after))
1593+ count += 1
1594+
1595+ if num_after != (num_before - 1):
1596+ self.log.error('image deletion timed out')
1597+ return False
1598+
1599+ return True
1600+
1601+ def create_instance(self, nova, image_name, instance_name, flavor):
1602+ """Create the specified instance."""
1603+ image = nova.images.find(name=image_name)
1604+ flavor = nova.flavors.find(name=flavor)
1605+ instance = nova.servers.create(name=instance_name, image=image,
1606+ flavor=flavor)
1607+
1608+ count = 1
1609+ status = instance.status
1610+ while status != 'ACTIVE' and count < 60:
1611+ time.sleep(3)
1612+ instance = nova.servers.get(instance.id)
1613+ status = instance.status
1614+ self.log.debug('instance status: {}'.format(status))
1615+ count += 1
1616+
1617+ if status != 'ACTIVE':
1618+ self.log.error('instance creation timed out')
1619+ return None
1620+
1621+ return instance
1622+
1623+ def delete_instance(self, nova, instance):
1624+ """Delete the specified instance."""
1625+ num_before = len(list(nova.servers.list()))
1626+ nova.servers.delete(instance)
1627+
1628+ count = 1
1629+ num_after = len(list(nova.servers.list()))
1630+ while num_after != (num_before - 1) and count < 10:
1631+ time.sleep(3)
1632+ num_after = len(list(nova.servers.list()))
1633+ self.log.debug('number of instances: {}'.format(num_after))
1634+ count += 1
1635+
1636+ if num_after != (num_before - 1):
1637+ self.log.error('instance deletion timed out')
1638+ return False
1639+
1640+ return True
1641
1642=== modified file 'hooks/charmhelpers/contrib/openstack/context.py'
1643--- hooks/charmhelpers/contrib/openstack/context.py 2014-05-20 18:55:23 +0000
1644+++ hooks/charmhelpers/contrib/openstack/context.py 2015-03-03 23:20:09 +0000
1645@@ -1,48 +1,81 @@
1646+# Copyright 2014-2015 Canonical Limited.
1647+#
1648+# This file is part of charm-helpers.
1649+#
1650+# charm-helpers is free software: you can redistribute it and/or modify
1651+# it under the terms of the GNU Lesser General Public License version 3 as
1652+# published by the Free Software Foundation.
1653+#
1654+# charm-helpers is distributed in the hope that it will be useful,
1655+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1656+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1657+# GNU Lesser General Public License for more details.
1658+#
1659+# You should have received a copy of the GNU Lesser General Public License
1660+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1661+
1662 import json
1663 import os
1664 import time
1665-
1666 from base64 import b64decode
1667-
1668-from subprocess import (
1669- check_call
1670-)
1671-
1672+from subprocess import check_call
1673+
1674+import six
1675+import yaml
1676
1677 from charmhelpers.fetch import (
1678 apt_install,
1679 filter_installed_packages,
1680 )
1681-
1682 from charmhelpers.core.hookenv import (
1683 config,
1684+ is_relation_made,
1685 local_unit,
1686 log,
1687 relation_get,
1688 relation_ids,
1689 related_units,
1690+ relation_set,
1691 unit_get,
1692 unit_private_ip,
1693+ charm_name,
1694+ DEBUG,
1695+ INFO,
1696+ WARNING,
1697 ERROR,
1698 )
1699
1700+from charmhelpers.core.sysctl import create as sysctl_create
1701+
1702+from charmhelpers.core.host import (
1703+ mkdir,
1704+ write_file,
1705+)
1706 from charmhelpers.contrib.hahelpers.cluster import (
1707 determine_apache_port,
1708 determine_api_port,
1709 https,
1710- is_clustered
1711+ is_clustered,
1712 )
1713-
1714 from charmhelpers.contrib.hahelpers.apache import (
1715 get_cert,
1716 get_ca_cert,
1717+ install_ca_cert,
1718 )
1719-
1720 from charmhelpers.contrib.openstack.neutron import (
1721 neutron_plugin_attribute,
1722 )
1723+from charmhelpers.contrib.network.ip import (
1724+ get_address_in_network,
1725+ get_ipv6_addr,
1726+ get_netmask_for_address,
1727+ format_ipv6_addr,
1728+ is_address_in_network,
1729+)
1730+from charmhelpers.contrib.openstack.utils import get_host_ip
1731
1732 CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
1733+ADDRESS_TYPES = ['admin', 'internal', 'public']
1734
1735
1736 class OSContextError(Exception):
1737@@ -50,7 +83,7 @@
1738
1739
1740 def ensure_packages(packages):
1741- '''Install but do not upgrade required plugin packages'''
1742+ """Install but do not upgrade required plugin packages."""
1743 required = filter_installed_packages(packages)
1744 if required:
1745 apt_install(required, fatal=True)
1746@@ -58,20 +91,59 @@
1747
1748 def context_complete(ctxt):
1749 _missing = []
1750- for k, v in ctxt.iteritems():
1751+ for k, v in six.iteritems(ctxt):
1752 if v is None or v == '':
1753 _missing.append(k)
1754+
1755 if _missing:
1756- log('Missing required data: %s' % ' '.join(_missing), level='INFO')
1757+ log('Missing required data: %s' % ' '.join(_missing), level=INFO)
1758 return False
1759+
1760 return True
1761
1762
1763 def config_flags_parser(config_flags):
1764+ """Parses config flags string into dict.
1765+
1766+ This parsing method supports a few different formats for the config
1767+ flag values to be parsed:
1768+
1769+ 1. A string in the simple format of key=value pairs, with the possibility
1770+ of specifying multiple key value pairs within the same string. For
1771+ example, a string in the format of 'key1=value1, key2=value2' will
1772+ return a dict of:
1773+ {'key1': 'value1',
1774+ 'key2': 'value2'}.
1775+
1776+ 2. A string in the above format, but supporting a comma-delimited list
1777+ of values for the same key. For example, a string in the format of
1778+ 'key1=value1, key2=value3,value4,value5' will return a dict of:
1779+ {'key1', 'value1',
1780+ 'key2', 'value2,value3,value4'}
1781+
1782+ 3. A string containing a colon character (:) prior to an equal
1783+ character (=) will be treated as yaml and parsed as such. This can be
1784+ used to specify more complex key value pairs. For example,
1785+ a string in the format of 'key1: subkey1=value1, subkey2=value2' will
1786+ return a dict of:
1787+ {'key1', 'subkey1=value1, subkey2=value2'}
1788+
1789+ The provided config_flags string may be a list of comma-separated values
1790+ which themselves may be comma-separated list of values.
1791+ """
1792+ # If we find a colon before an equals sign then treat it as yaml.
1793+ # Note: limit it to finding the colon first since this indicates assignment
1794+ # for inline yaml.
1795+ colon = config_flags.find(':')
1796+ equals = config_flags.find('=')
1797+ if colon > 0:
1798+ if colon < equals or equals < 0:
1799+ return yaml.safe_load(config_flags)
1800+
1801 if config_flags.find('==') >= 0:
1802- log("config_flags is not in expected format (key=value)",
1803- level=ERROR)
1804+ log("config_flags is not in expected format (key=value)", level=ERROR)
1805 raise OSContextError
1806+
1807 # strip the following from each value.
1808 post_strippers = ' ,'
1809 # we strip any leading/trailing '=' or ' ' from the string then
1810@@ -79,7 +151,7 @@
1811 split = config_flags.strip(' =').split('=')
1812 limit = len(split)
1813 flags = {}
1814- for i in xrange(0, limit - 1):
1815+ for i in range(0, limit - 1):
1816 current = split[i]
1817 next = split[i + 1]
1818 vindex = next.rfind(',')
1819@@ -94,17 +166,18 @@
1820 # if this not the first entry, expect an embedded key.
1821 index = current.rfind(',')
1822 if index < 0:
1823- log("invalid config value(s) at index %s" % (i),
1824- level=ERROR)
1825+ log("Invalid config value(s) at index %s" % (i), level=ERROR)
1826 raise OSContextError
1827 key = current[index + 1:]
1828
1829 # Add to collection.
1830 flags[key.strip(post_strippers)] = value.rstrip(post_strippers)
1831+
1832 return flags
1833
1834
1835 class OSContextGenerator(object):
1836+ """Base class for all context generators."""
1837 interfaces = []
1838
1839 def __call__(self):
1840@@ -116,11 +189,11 @@
1841
1842 def __init__(self,
1843 database=None, user=None, relation_prefix=None, ssl_dir=None):
1844- '''
1845- Allows inspecting relation for settings prefixed with relation_prefix.
1846- This is useful for parsing access for multiple databases returned via
1847- the shared-db interface (eg, nova_password, quantum_password)
1848- '''
1849+ """Allows inspecting relation for settings prefixed with
1850+ relation_prefix. This is useful for parsing access for multiple
1851+ databases returned via the shared-db interface (eg, nova_password,
1852+ quantum_password)
1853+ """
1854 self.relation_prefix = relation_prefix
1855 self.database = database
1856 self.user = user
1857@@ -130,12 +203,29 @@
1858 self.database = self.database or config('database')
1859 self.user = self.user or config('database-user')
1860 if None in [self.database, self.user]:
1861- log('Could not generate shared_db context. '
1862- 'Missing required charm config options. '
1863- '(database name and user)')
1864+ log("Could not generate shared_db context. Missing required charm "
1865+ "config options. (database name and user)", level=ERROR)
1866 raise OSContextError
1867+
1868 ctxt = {}
1869
1870+ # NOTE(jamespage) if mysql charm provides a network upon which
1871+ # access to the database should be made, reconfigure relation
1872+ # with the service units local address and defer execution
1873+ access_network = relation_get('access-network')
1874+ if access_network is not None:
1875+ if self.relation_prefix is not None:
1876+ hostname_key = "{}_hostname".format(self.relation_prefix)
1877+ else:
1878+ hostname_key = "hostname"
1879+ access_hostname = get_address_in_network(access_network,
1880+ unit_get('private-address'))
1881+ set_hostname = relation_get(attribute=hostname_key,
1882+ unit=local_unit())
1883+ if set_hostname != access_hostname:
1884+ relation_set(relation_settings={hostname_key: access_hostname})
1885+ return None # Defer any further hook execution for now....
1886+
1887 password_setting = 'password'
1888 if self.relation_prefix:
1889 password_setting = self.relation_prefix + '_password'
1890@@ -143,8 +233,10 @@
1891 for rid in relation_ids('shared-db'):
1892 for unit in related_units(rid):
1893 rdata = relation_get(rid=rid, unit=unit)
1894+ host = rdata.get('db_host')
1895+ host = format_ipv6_addr(host) or host
1896 ctxt = {
1897- 'database_host': rdata.get('db_host'),
1898+ 'database_host': host,
1899 'database': self.database,
1900 'database_user': self.user,
1901 'database_password': rdata.get(password_setting),
1902@@ -165,23 +257,24 @@
1903 def __call__(self):
1904 self.database = self.database or config('database')
1905 if self.database is None:
1906- log('Could not generate postgresql_db context. '
1907- 'Missing required charm config options. '
1908- '(database name)')
1909+ log('Could not generate postgresql_db context. Missing required '
1910+ 'charm config options. (database name)', level=ERROR)
1911 raise OSContextError
1912+
1913 ctxt = {}
1914-
1915 for rid in relation_ids(self.interfaces[0]):
1916 for unit in related_units(rid):
1917- ctxt = {
1918- 'database_host': relation_get('host', rid=rid, unit=unit),
1919- 'database': self.database,
1920- 'database_user': relation_get('user', rid=rid, unit=unit),
1921- 'database_password': relation_get('password', rid=rid, unit=unit),
1922- 'database_type': 'postgresql',
1923- }
1924+ rel_host = relation_get('host', rid=rid, unit=unit)
1925+ rel_user = relation_get('user', rid=rid, unit=unit)
1926+ rel_passwd = relation_get('password', rid=rid, unit=unit)
1927+ ctxt = {'database_host': rel_host,
1928+ 'database': self.database,
1929+ 'database_user': rel_user,
1930+ 'database_password': rel_passwd,
1931+ 'database_type': 'postgresql'}
1932 if context_complete(ctxt):
1933 return ctxt
1934+
1935 return {}
1936
1937
1938@@ -190,85 +283,123 @@
1939 ca_path = os.path.join(ssl_dir, 'db-client.ca')
1940 with open(ca_path, 'w') as fh:
1941 fh.write(b64decode(rdata['ssl_ca']))
1942+
1943 ctxt['database_ssl_ca'] = ca_path
1944 elif 'ssl_ca' in rdata:
1945- log("Charm not setup for ssl support but ssl ca found")
1946+ log("Charm not setup for ssl support but ssl ca found", level=INFO)
1947 return ctxt
1948+
1949 if 'ssl_cert' in rdata:
1950 cert_path = os.path.join(
1951 ssl_dir, 'db-client.cert')
1952 if not os.path.exists(cert_path):
1953- log("Waiting 1m for ssl client cert validity")
1954+ log("Waiting 1m for ssl client cert validity", level=INFO)
1955 time.sleep(60)
1956+
1957 with open(cert_path, 'w') as fh:
1958 fh.write(b64decode(rdata['ssl_cert']))
1959+
1960 ctxt['database_ssl_cert'] = cert_path
1961 key_path = os.path.join(ssl_dir, 'db-client.key')
1962 with open(key_path, 'w') as fh:
1963 fh.write(b64decode(rdata['ssl_key']))
1964+
1965 ctxt['database_ssl_key'] = key_path
1966+
1967 return ctxt
1968
1969
1970 class IdentityServiceContext(OSContextGenerator):
1971 interfaces = ['identity-service']
1972
1973+ def __init__(self, service=None, service_user=None):
1974+ self.service = service
1975+ self.service_user = service_user
1976+
1977 def __call__(self):
1978- log('Generating template context for identity-service')
1979+ log('Generating template context for identity-service', level=DEBUG)
1980 ctxt = {}
1981
1982+ if self.service and self.service_user:
1983+ # This is required for pki token signing if we don't want /tmp to
1984+ # be used.
1985+ cachedir = '/var/cache/%s' % (self.service)
1986+ if not os.path.isdir(cachedir):
1987+ log("Creating service cache dir %s" % (cachedir), level=DEBUG)
1988+ mkdir(path=cachedir, owner=self.service_user,
1989+ group=self.service_user, perms=0o700)
1990+
1991+ ctxt['signing_dir'] = cachedir
1992+
1993 for rid in relation_ids('identity-service'):
1994 for unit in related_units(rid):
1995 rdata = relation_get(rid=rid, unit=unit)
1996- ctxt = {
1997- 'service_port': rdata.get('service_port'),
1998- 'service_host': rdata.get('service_host'),
1999- 'auth_host': rdata.get('auth_host'),
2000- 'auth_port': rdata.get('auth_port'),
2001- 'admin_tenant_name': rdata.get('service_tenant'),
2002- 'admin_user': rdata.get('service_username'),
2003- 'admin_password': rdata.get('service_password'),
2004- 'service_protocol':
2005- rdata.get('service_protocol') or 'http',
2006- 'auth_protocol':
2007- rdata.get('auth_protocol') or 'http',
2008- }
2009+ serv_host = rdata.get('service_host')
2010+ serv_host = format_ipv6_addr(serv_host) or serv_host
2011+ auth_host = rdata.get('auth_host')
2012+ auth_host = format_ipv6_addr(auth_host) or auth_host
2013+ svc_protocol = rdata.get('service_protocol') or 'http'
2014+ auth_protocol = rdata.get('auth_protocol') or 'http'
2015+ ctxt.update({'service_port': rdata.get('service_port'),
2016+ 'service_host': serv_host,
2017+ 'auth_host': auth_host,
2018+ 'auth_port': rdata.get('auth_port'),
2019+ 'admin_tenant_name': rdata.get('service_tenant'),
2020+ 'admin_user': rdata.get('service_username'),
2021+ 'admin_password': rdata.get('service_password'),
2022+ 'service_protocol': svc_protocol,
2023+ 'auth_protocol': auth_protocol})
2024+
2025 if context_complete(ctxt):
2026 # NOTE(jamespage) this is required for >= icehouse
2027 # so a missing value just indicates keystone needs
2028 # upgrading
2029 ctxt['admin_tenant_id'] = rdata.get('service_tenant_id')
2030 return ctxt
2031+
2032 return {}
2033
2034
2035 class AMQPContext(OSContextGenerator):
2036- interfaces = ['amqp']
2037
2038- def __init__(self, ssl_dir=None):
2039+ def __init__(self, ssl_dir=None, rel_name='amqp', relation_prefix=None):
2040 self.ssl_dir = ssl_dir
2041+ self.rel_name = rel_name
2042+ self.relation_prefix = relation_prefix
2043+ self.interfaces = [rel_name]
2044
2045 def __call__(self):
2046- log('Generating template context for amqp')
2047+ log('Generating template context for amqp', level=DEBUG)
2048 conf = config()
2049+ if self.relation_prefix:
2050+ user_setting = '%s-rabbit-user' % (self.relation_prefix)
2051+ vhost_setting = '%s-rabbit-vhost' % (self.relation_prefix)
2052+ else:
2053+ user_setting = 'rabbit-user'
2054+ vhost_setting = 'rabbit-vhost'
2055+
2056 try:
2057- username = conf['rabbit-user']
2058- vhost = conf['rabbit-vhost']
2059+ username = conf[user_setting]
2060+ vhost = conf[vhost_setting]
2061 except KeyError as e:
2062- log('Could not generate shared_db context. '
2063- 'Missing required charm config options: %s.' % e)
2064+ log('Could not generate shared_db context. Missing required charm '
2065+ 'config options: %s.' % e, level=ERROR)
2066 raise OSContextError
2067+
2068 ctxt = {}
2069- for rid in relation_ids('amqp'):
2070+ for rid in relation_ids(self.rel_name):
2071 ha_vip_only = False
2072 for unit in related_units(rid):
2073 if relation_get('clustered', rid=rid, unit=unit):
2074 ctxt['clustered'] = True
2075- ctxt['rabbitmq_host'] = relation_get('vip', rid=rid,
2076- unit=unit)
2077+ vip = relation_get('vip', rid=rid, unit=unit)
2078+ vip = format_ipv6_addr(vip) or vip
2079+ ctxt['rabbitmq_host'] = vip
2080 else:
2081- ctxt['rabbitmq_host'] = relation_get('private-address',
2082- rid=rid, unit=unit)
2083+ host = relation_get('private-address', rid=rid, unit=unit)
2084+ host = format_ipv6_addr(host) or host
2085+ ctxt['rabbitmq_host'] = host
2086+
2087 ctxt.update({
2088 'rabbitmq_user': username,
2089 'rabbitmq_password': relation_get('password', rid=rid,
2090@@ -279,6 +410,7 @@
2091 ssl_port = relation_get('ssl_port', rid=rid, unit=unit)
2092 if ssl_port:
2093 ctxt['rabbit_ssl_port'] = ssl_port
2094+
2095 ssl_ca = relation_get('ssl_ca', rid=rid, unit=unit)
2096 if ssl_ca:
2097 ctxt['rabbit_ssl_ca'] = ssl_ca
2098@@ -292,57 +424,65 @@
2099 if context_complete(ctxt):
2100 if 'rabbit_ssl_ca' in ctxt:
2101 if not self.ssl_dir:
2102- log(("Charm not setup for ssl support "
2103- "but ssl ca found"))
2104+ log("Charm not setup for ssl support but ssl ca "
2105+ "found", level=INFO)
2106 break
2107+
2108 ca_path = os.path.join(
2109 self.ssl_dir, 'rabbit-client-ca.pem')
2110 with open(ca_path, 'w') as fh:
2111 fh.write(b64decode(ctxt['rabbit_ssl_ca']))
2112 ctxt['rabbit_ssl_ca'] = ca_path
2113+
2114 # Sufficient information found = break out!
2115 break
2116+
2117 # Used for active/active rabbitmq >= grizzly
2118- if ('clustered' not in ctxt or ha_vip_only) \
2119- and len(related_units(rid)) > 1:
2120+ if (('clustered' not in ctxt or ha_vip_only) and
2121+ len(related_units(rid)) > 1):
2122 rabbitmq_hosts = []
2123 for unit in related_units(rid):
2124- rabbitmq_hosts.append(relation_get('private-address',
2125- rid=rid, unit=unit))
2126- ctxt['rabbitmq_hosts'] = ','.join(rabbitmq_hosts)
2127+ host = relation_get('private-address', rid=rid, unit=unit)
2128+ host = format_ipv6_addr(host) or host
2129+ rabbitmq_hosts.append(host)
2130+
2131+ ctxt['rabbitmq_hosts'] = ','.join(sorted(rabbitmq_hosts))
2132+
2133 if not context_complete(ctxt):
2134 return {}
2135- else:
2136- return ctxt
2137+
2138+ return ctxt
2139
2140
2141 class CephContext(OSContextGenerator):
2142+ """Generates context for /etc/ceph/ceph.conf templates."""
2143 interfaces = ['ceph']
2144
2145 def __call__(self):
2146- '''This generates context for /etc/ceph/ceph.conf templates'''
2147 if not relation_ids('ceph'):
2148 return {}
2149
2150- log('Generating template context for ceph')
2151-
2152+ log('Generating template context for ceph', level=DEBUG)
2153 mon_hosts = []
2154 auth = None
2155 key = None
2156 use_syslog = str(config('use-syslog')).lower()
2157 for rid in relation_ids('ceph'):
2158 for unit in related_units(rid):
2159- mon_hosts.append(relation_get('private-address', rid=rid,
2160- unit=unit))
2161 auth = relation_get('auth', rid=rid, unit=unit)
2162 key = relation_get('key', rid=rid, unit=unit)
2163+ ceph_pub_addr = relation_get('ceph-public-address', rid=rid,
2164+ unit=unit)
2165+ unit_priv_addr = relation_get('private-address', rid=rid,
2166+ unit=unit)
2167+ ceph_addr = ceph_pub_addr or unit_priv_addr
2168+ ceph_addr = format_ipv6_addr(ceph_addr) or ceph_addr
2169+ mon_hosts.append(ceph_addr)
2170
2171- ctxt = {
2172- 'mon_hosts': ' '.join(mon_hosts),
2173- 'auth': auth,
2174- 'key': key,
2175- 'use_syslog': use_syslog
2176- }
2177+ ctxt = {'mon_hosts': ' '.join(sorted(mon_hosts)),
2178+ 'auth': auth,
2179+ 'key': key,
2180+ 'use_syslog': use_syslog}
2181
2182 if not os.path.isdir('/etc/ceph'):
2183 os.mkdir('/etc/ceph')
2184@@ -351,42 +491,98 @@
2185 return {}
2186
2187 ensure_packages(['ceph-common'])
2188-
2189 return ctxt
2190
2191
2192 class HAProxyContext(OSContextGenerator):
2193+ """Provides half a context for the haproxy template, which describes
2194+ all peers to be included in the cluster. Each charm needs to include
2195+ its own context generator that describes the port mapping.
2196+ """
2197 interfaces = ['cluster']
2198
2199+ def __init__(self, singlenode_mode=False):
2200+ self.singlenode_mode = singlenode_mode
2201+
2202 def __call__(self):
2203- '''
2204- Builds half a context for the haproxy template, which describes
2205- all peers to be included in the cluster. Each charm needs to include
2206- its own context generator that describes the port mapping.
2207- '''
2208- if not relation_ids('cluster'):
2209+ if not relation_ids('cluster') and not self.singlenode_mode:
2210 return {}
2211
2212+ if config('prefer-ipv6'):
2213+ addr = get_ipv6_addr(exc_list=[config('vip')])[0]
2214+ else:
2215+ addr = get_host_ip(unit_get('private-address'))
2216+
2217+ l_unit = local_unit().replace('/', '-')
2218 cluster_hosts = {}
2219- l_unit = local_unit().replace('/', '-')
2220- cluster_hosts[l_unit] = unit_get('private-address')
2221-
2222+
2223+ # NOTE(jamespage): build out map of configured network endpoints
2224+ # and associated backends
2225+ for addr_type in ADDRESS_TYPES:
2226+ cfg_opt = 'os-{}-network'.format(addr_type)
2227+ laddr = get_address_in_network(config(cfg_opt))
2228+ if laddr:
2229+ netmask = get_netmask_for_address(laddr)
2230+ cluster_hosts[laddr] = {'network': "{}/{}".format(laddr,
2231+ netmask),
2232+ 'backends': {l_unit: laddr}}
2233+ for rid in relation_ids('cluster'):
2234+ for unit in related_units(rid):
2235+ _laddr = relation_get('{}-address'.format(addr_type),
2236+ rid=rid, unit=unit)
2237+ if _laddr:
2238+ _unit = unit.replace('/', '-')
2239+ cluster_hosts[laddr]['backends'][_unit] = _laddr
2240+
2241+ # NOTE(jamespage) add backend based on private address - this
2242+ # with either be the only backend or the fallback if no acls
2243+ # match in the frontend
2244+ cluster_hosts[addr] = {}
2245+ netmask = get_netmask_for_address(addr)
2246+ cluster_hosts[addr] = {'network': "{}/{}".format(addr, netmask),
2247+ 'backends': {l_unit: addr}}
2248 for rid in relation_ids('cluster'):
2249 for unit in related_units(rid):
2250- _unit = unit.replace('/', '-')
2251- addr = relation_get('private-address', rid=rid, unit=unit)
2252- cluster_hosts[_unit] = addr
2253+ _laddr = relation_get('private-address',
2254+ rid=rid, unit=unit)
2255+ if _laddr:
2256+ _unit = unit.replace('/', '-')
2257+ cluster_hosts[addr]['backends'][_unit] = _laddr
2258
2259 ctxt = {
2260- 'units': cluster_hosts,
2261+ 'frontends': cluster_hosts,
2262+ 'default_backend': addr
2263 }
2264- if len(cluster_hosts.keys()) > 1:
2265- # Enable haproxy when we have enough peers.
2266- log('Ensuring haproxy enabled in /etc/default/haproxy.')
2267- with open('/etc/default/haproxy', 'w') as out:
2268- out.write('ENABLED=1\n')
2269- return ctxt
2270- log('HAProxy context is incomplete, this unit has no peers.')
2271+
2272+ if config('haproxy-server-timeout'):
2273+ ctxt['haproxy_server_timeout'] = config('haproxy-server-timeout')
2274+
2275+ if config('haproxy-client-timeout'):
2276+ ctxt['haproxy_client_timeout'] = config('haproxy-client-timeout')
2277+
2278+ if config('prefer-ipv6'):
2279+ ctxt['ipv6'] = True
2280+ ctxt['local_host'] = 'ip6-localhost'
2281+ ctxt['haproxy_host'] = '::'
2282+ ctxt['stat_port'] = ':::8888'
2283+ else:
2284+ ctxt['local_host'] = '127.0.0.1'
2285+ ctxt['haproxy_host'] = '0.0.0.0'
2286+ ctxt['stat_port'] = ':8888'
2287+
2288+ for frontend in cluster_hosts:
2289+ if (len(cluster_hosts[frontend]['backends']) > 1 or
2290+ self.singlenode_mode):
2291+ # Enable haproxy when we have enough peers.
2292+ log('Ensuring haproxy enabled in /etc/default/haproxy.',
2293+ level=DEBUG)
2294+ with open('/etc/default/haproxy', 'w') as out:
2295+ out.write('ENABLED=1\n')
2296+
2297+ return ctxt
2298+
2299+ log('HAProxy context is incomplete, this unit has no peers.',
2300+ level=INFO)
2301 return {}
2302
2303
2304@@ -394,36 +590,36 @@
2305 interfaces = ['image-service']
2306
2307 def __call__(self):
2308- '''
2309- Obtains the glance API server from the image-service relation. Useful
2310- in nova and cinder (currently).
2311- '''
2312- log('Generating template context for image-service.')
2313+ """Obtains the glance API server from the image-service relation.
2314+ Useful in nova and cinder (currently).
2315+ """
2316+ log('Generating template context for image-service.', level=DEBUG)
2317 rids = relation_ids('image-service')
2318 if not rids:
2319 return {}
2320+
2321 for rid in rids:
2322 for unit in related_units(rid):
2323 api_server = relation_get('glance-api-server',
2324 rid=rid, unit=unit)
2325 if api_server:
2326 return {'glance_api_servers': api_server}
2327- log('ImageService context is incomplete. '
2328- 'Missing required relation data.')
2329+
2330+ log("ImageService context is incomplete. Missing required relation "
2331+ "data.", level=INFO)
2332 return {}
2333
2334
2335 class ApacheSSLContext(OSContextGenerator):
2336-
2337- """
2338- Generates a context for an apache vhost configuration that configures
2339+ """Generates a context for an apache vhost configuration that configures
2340 HTTPS reverse proxying for one or many endpoints. Generated context
2341- looks something like:
2342- {
2343- 'namespace': 'cinder',
2344- 'private_address': 'iscsi.mycinderhost.com',
2345- 'endpoints': [(8776, 8766), (8777, 8767)]
2346- }
2347+ looks something like::
2348+
2349+ {
2350+ 'namespace': 'cinder',
2351+ 'private_address': 'iscsi.mycinderhost.com',
2352+ 'endpoints': [(8776, 8766), (8777, 8767)]
2353+ }
2354
2355 The endpoints list consists of a tuples mapping external ports
2356 to internal ports.
2357@@ -439,44 +635,112 @@
2358 cmd = ['a2enmod', 'ssl', 'proxy', 'proxy_http']
2359 check_call(cmd)
2360
2361- def configure_cert(self):
2362- if not os.path.isdir('/etc/apache2/ssl'):
2363- os.mkdir('/etc/apache2/ssl')
2364+ def configure_cert(self, cn=None):
2365 ssl_dir = os.path.join('/etc/apache2/ssl/', self.service_namespace)
2366- if not os.path.isdir(ssl_dir):
2367- os.mkdir(ssl_dir)
2368- cert, key = get_cert()
2369- with open(os.path.join(ssl_dir, 'cert'), 'w') as cert_out:
2370- cert_out.write(b64decode(cert))
2371- with open(os.path.join(ssl_dir, 'key'), 'w') as key_out:
2372- key_out.write(b64decode(key))
2373+ mkdir(path=ssl_dir)
2374+ cert, key = get_cert(cn)
2375+ if cn:
2376+ cert_filename = 'cert_{}'.format(cn)
2377+ key_filename = 'key_{}'.format(cn)
2378+ else:
2379+ cert_filename = 'cert'
2380+ key_filename = 'key'
2381+
2382+ write_file(path=os.path.join(ssl_dir, cert_filename),
2383+ content=b64decode(cert))
2384+ write_file(path=os.path.join(ssl_dir, key_filename),
2385+ content=b64decode(key))
2386+
2387+ def configure_ca(self):
2388 ca_cert = get_ca_cert()
2389 if ca_cert:
2390- with open(CA_CERT_PATH, 'w') as ca_out:
2391- ca_out.write(b64decode(ca_cert))
2392- check_call(['update-ca-certificates'])
2393+ install_ca_cert(b64decode(ca_cert))
2394+
2395+ def canonical_names(self):
2396+ """Figure out which canonical names clients will access this service.
2397+ """
2398+ cns = []
2399+ for r_id in relation_ids('identity-service'):
2400+ for unit in related_units(r_id):
2401+ rdata = relation_get(rid=r_id, unit=unit)
2402+ for k in rdata:
2403+ if k.startswith('ssl_key_'):
2404+ cns.append(k.lstrip('ssl_key_'))
2405+
2406+ return sorted(list(set(cns)))
2407+
2408+ def get_network_addresses(self):
2409+ """For each network configured, return corresponding address and vip
2410+ (if available).
2411+
2412+ Returns a list of tuples of the form:
2413+
2414+ [(address_in_net_a, vip_in_net_a),
2415+ (address_in_net_b, vip_in_net_b),
2416+ ...]
2417+
2418+ or, if no vip(s) available:
2419+
2420+ [(address_in_net_a, address_in_net_a),
2421+ (address_in_net_b, address_in_net_b),
2422+ ...]
2423+ """
2424+ addresses = []
2425+ if config('vip'):
2426+ vips = config('vip').split()
2427+ else:
2428+ vips = []
2429+
2430+ for net_type in ['os-internal-network', 'os-admin-network',
2431+ 'os-public-network']:
2432+ addr = get_address_in_network(config(net_type),
2433+ unit_get('private-address'))
2434+ if len(vips) > 1 and is_clustered():
2435+ if not config(net_type):
2436+ log("Multiple networks configured but net_type "
2437+ "is None (%s)." % net_type, level=WARNING)
2438+ continue
2439+
2440+ for vip in vips:
2441+ if is_address_in_network(config(net_type), vip):
2442+ addresses.append((addr, vip))
2443+ break
2444+
2445+ elif is_clustered() and config('vip'):
2446+ addresses.append((addr, config('vip')))
2447+ else:
2448+ addresses.append((addr, addr))
2449+
2450+ return sorted(addresses)
2451
2452 def __call__(self):
2453- if isinstance(self.external_ports, basestring):
2454+ if isinstance(self.external_ports, six.string_types):
2455 self.external_ports = [self.external_ports]
2456- if (not self.external_ports or not https()):
2457+
2458+ if not self.external_ports or not https():
2459 return {}
2460
2461- self.configure_cert()
2462+ self.configure_ca()
2463 self.enable_modules()
2464
2465- ctxt = {
2466- 'namespace': self.service_namespace,
2467- 'private_address': unit_get('private-address'),
2468- 'endpoints': []
2469- }
2470- if is_clustered():
2471- ctxt['private_address'] = config('vip')
2472- for api_port in self.external_ports:
2473- ext_port = determine_apache_port(api_port)
2474- int_port = determine_api_port(api_port)
2475- portmap = (int(ext_port), int(int_port))
2476- ctxt['endpoints'].append(portmap)
2477+ ctxt = {'namespace': self.service_namespace,
2478+ 'endpoints': [],
2479+ 'ext_ports': []}
2480+
2481+ for cn in self.canonical_names():
2482+ self.configure_cert(cn)
2483+
2484+ addresses = self.get_network_addresses()
2485+ for address, endpoint in sorted(set(addresses)):
2486+ for api_port in self.external_ports:
2487+ ext_port = determine_apache_port(api_port,
2488+ singlenode_mode=True)
2489+ int_port = determine_api_port(api_port, singlenode_mode=True)
2490+ portmap = (address, endpoint, int(ext_port), int(int_port))
2491+ ctxt['endpoints'].append(portmap)
2492+ ctxt['ext_ports'].append(int(ext_port))
2493+
2494+ ctxt['ext_ports'] = sorted(list(set(ctxt['ext_ports'])))
2495 return ctxt
2496
2497
2498@@ -493,21 +757,23 @@
2499
2500 @property
2501 def packages(self):
2502- return neutron_plugin_attribute(
2503- self.plugin, 'packages', self.network_manager)
2504+ return neutron_plugin_attribute(self.plugin, 'packages',
2505+ self.network_manager)
2506
2507 @property
2508 def neutron_security_groups(self):
2509 return None
2510
2511 def _ensure_packages(self):
2512- [ensure_packages(pkgs) for pkgs in self.packages]
2513+ for pkgs in self.packages:
2514+ ensure_packages(pkgs)
2515
2516 def _save_flag_file(self):
2517 if self.network_manager == 'quantum':
2518 _file = '/etc/nova/quantum_plugin.conf'
2519 else:
2520 _file = '/etc/nova/neutron_plugin.conf'
2521+
2522 with open(_file, 'wb') as out:
2523 out.write(self.plugin + '\n')
2524
2525@@ -516,13 +782,11 @@
2526 self.network_manager)
2527 config = neutron_plugin_attribute(self.plugin, 'config',
2528 self.network_manager)
2529- ovs_ctxt = {
2530- 'core_plugin': driver,
2531- 'neutron_plugin': 'ovs',
2532- 'neutron_security_groups': self.neutron_security_groups,
2533- 'local_ip': unit_private_ip(),
2534- 'config': config
2535- }
2536+ ovs_ctxt = {'core_plugin': driver,
2537+ 'neutron_plugin': 'ovs',
2538+ 'neutron_security_groups': self.neutron_security_groups,
2539+ 'local_ip': unit_private_ip(),
2540+ 'config': config}
2541
2542 return ovs_ctxt
2543
2544@@ -531,30 +795,63 @@
2545 self.network_manager)
2546 config = neutron_plugin_attribute(self.plugin, 'config',
2547 self.network_manager)
2548- nvp_ctxt = {
2549- 'core_plugin': driver,
2550- 'neutron_plugin': 'nvp',
2551- 'neutron_security_groups': self.neutron_security_groups,
2552- 'local_ip': unit_private_ip(),
2553- 'config': config
2554- }
2555+ nvp_ctxt = {'core_plugin': driver,
2556+ 'neutron_plugin': 'nvp',
2557+ 'neutron_security_groups': self.neutron_security_groups,
2558+ 'local_ip': unit_private_ip(),
2559+ 'config': config}
2560
2561 return nvp_ctxt
2562
2563+ def n1kv_ctxt(self):
2564+ driver = neutron_plugin_attribute(self.plugin, 'driver',
2565+ self.network_manager)
2566+ n1kv_config = neutron_plugin_attribute(self.plugin, 'config',
2567+ self.network_manager)
2568+ n1kv_user_config_flags = config('n1kv-config-flags')
2569+ restrict_policy_profiles = config('n1kv-restrict-policy-profiles')
2570+ n1kv_ctxt = {'core_plugin': driver,
2571+ 'neutron_plugin': 'n1kv',
2572+ 'neutron_security_groups': self.neutron_security_groups,
2573+ 'local_ip': unit_private_ip(),
2574+ 'config': n1kv_config,
2575+ 'vsm_ip': config('n1kv-vsm-ip'),
2576+ 'vsm_username': config('n1kv-vsm-username'),
2577+ 'vsm_password': config('n1kv-vsm-password'),
2578+ 'restrict_policy_profiles': restrict_policy_profiles}
2579+
2580+ if n1kv_user_config_flags:
2581+ flags = config_flags_parser(n1kv_user_config_flags)
2582+ n1kv_ctxt['user_config_flags'] = flags
2583+
2584+ return n1kv_ctxt
2585+
2586+ def calico_ctxt(self):
2587+ driver = neutron_plugin_attribute(self.plugin, 'driver',
2588+ self.network_manager)
2589+ config = neutron_plugin_attribute(self.plugin, 'config',
2590+ self.network_manager)
2591+ calico_ctxt = {'core_plugin': driver,
2592+ 'neutron_plugin': 'Calico',
2593+ 'neutron_security_groups': self.neutron_security_groups,
2594+ 'local_ip': unit_private_ip(),
2595+ 'config': config}
2596+
2597+ return calico_ctxt
2598+
2599 def neutron_ctxt(self):
2600 if https():
2601 proto = 'https'
2602 else:
2603 proto = 'http'
2604+
2605 if is_clustered():
2606 host = config('vip')
2607 else:
2608 host = unit_get('private-address')
2609- url = '%s://%s:%s' % (proto, host, '9696')
2610- ctxt = {
2611- 'network_manager': self.network_manager,
2612- 'neutron_url': url,
2613- }
2614+
2615+ ctxt = {'network_manager': self.network_manager,
2616+ 'neutron_url': '%s://%s:%s' % (proto, host, '9696')}
2617 return ctxt
2618
2619 def __call__(self):
2620@@ -572,6 +869,10 @@
2621 ctxt.update(self.ovs_ctxt())
2622 elif self.plugin in ['nvp', 'nsx']:
2623 ctxt.update(self.nvp_ctxt())
2624+ elif self.plugin == 'n1kv':
2625+ ctxt.update(self.n1kv_ctxt())
2626+ elif self.plugin == 'Calico':
2627+ ctxt.update(self.calico_ctxt())
2628
2629 alchemy_flags = config('neutron-alchemy-flags')
2630 if alchemy_flags:
2631@@ -583,23 +884,40 @@
2632
2633
2634 class OSConfigFlagContext(OSContextGenerator):
2635-
2636- """
2637- Responsible for adding user-defined config-flags in charm config to a
2638- template context.
2639-
2640- NOTE: the value of config-flags may be a comma-separated list of
2641- key=value pairs and some Openstack config files support
2642- comma-separated lists as values.
2643- """
2644-
2645- def __call__(self):
2646- config_flags = config('config-flags')
2647- if not config_flags:
2648- return {}
2649-
2650- flags = config_flags_parser(config_flags)
2651- return {'user_config_flags': flags}
2652+ """Provides support for user-defined config flags.
2653+
2654+ Users can define a comma-seperated list of key=value pairs
2655+ in the charm configuration and apply them at any point in
2656+ any file by using a template flag.
2657+
2658+ Sometimes users might want config flags inserted within a
2659+ specific section so this class allows users to specify the
2660+ template flag name, allowing for multiple template flags
2661+ (sections) within the same context.
2662+
2663+ NOTE: the value of config-flags may be a comma-separated list of
2664+ key=value pairs and some Openstack config files support
2665+ comma-separated lists as values.
2666+ """
2667+
2668+ def __init__(self, charm_flag='config-flags',
2669+ template_flag='user_config_flags'):
2670+ """
2671+ :param charm_flag: config flags in charm configuration.
2672+ :param template_flag: insert point for user-defined flags in template
2673+ file.
2674+ """
2675+ super(OSConfigFlagContext, self).__init__()
2676+ self._charm_flag = charm_flag
2677+ self._template_flag = template_flag
2678+
2679+ def __call__(self):
2680+ config_flags = config(self._charm_flag)
2681+ if not config_flags:
2682+ return {}
2683+
2684+ return {self._template_flag:
2685+ config_flags_parser(config_flags)}
2686
2687
2688 class SubordinateConfigContext(OSContextGenerator):
2689@@ -611,7 +929,7 @@
2690 The subordinate interface allows subordinates to export their
2691 configuration requirements to the principle for multiple config
2692 files and multiple serivces. Ie, a subordinate that has interfaces
2693- to both glance and nova may export to following yaml blob as json:
2694+ to both glance and nova may export to following yaml blob as json::
2695
2696 glance:
2697 /etc/glance/glance-api.conf:
2698@@ -630,7 +948,8 @@
2699
2700 It is then up to the principle charms to subscribe this context to
2701 the service+config file it is interestd in. Configuration data will
2702- be available in the template context, in glance's case, as:
2703+ be available in the template context, in glance's case, as::
2704+
2705 ctxt = {
2706 ... other context ...
2707 'subordinate_config': {
2708@@ -642,7 +961,6 @@
2709 },
2710 }
2711 }
2712-
2713 """
2714
2715 def __init__(self, service, config_file, interface):
2716@@ -657,7 +975,7 @@
2717 self.interface = interface
2718
2719 def __call__(self):
2720- ctxt = {}
2721+ ctxt = {'sections': {}}
2722 for rid in relation_ids(self.interface):
2723 for unit in related_units(rid):
2724 sub_config = relation_get('subordinate_configuration',
2725@@ -672,21 +990,39 @@
2726
2727 if self.service not in sub_config:
2728 log('Found subordinate_config on %s but it contained'
2729- 'nothing for %s service' % (rid, self.service))
2730+ 'nothing for %s service' % (rid, self.service),
2731+ level=INFO)
2732 continue
2733
2734 sub_config = sub_config[self.service]
2735 if self.config_file not in sub_config:
2736 log('Found subordinate_config on %s but it contained'
2737- 'nothing for %s' % (rid, self.config_file))
2738+ 'nothing for %s' % (rid, self.config_file),
2739+ level=INFO)
2740 continue
2741
2742 sub_config = sub_config[self.config_file]
2743- for k, v in sub_config.iteritems():
2744- ctxt[k] = v
2745-
2746- if not ctxt:
2747- ctxt['sections'] = {}
2748+ for k, v in six.iteritems(sub_config):
2749+ if k == 'sections':
2750+ for section, config_dict in six.iteritems(v):
2751+ log("adding section '%s'" % (section),
2752+ level=DEBUG)
2753+ ctxt[k][section] = config_dict
2754+ else:
2755+ ctxt[k] = v
2756+
2757+ log("%d section(s) found" % (len(ctxt['sections'])), level=DEBUG)
2758+ return ctxt
2759+
2760+
2761+class LogLevelContext(OSContextGenerator):
2762+
2763+ def __call__(self):
2764+ ctxt = {}
2765+ ctxt['debug'] = \
2766+ False if config('debug') is None else config('debug')
2767+ ctxt['verbose'] = \
2768+ False if config('verbose') is None else config('verbose')
2769
2770 return ctxt
2771
2772@@ -694,7 +1030,77 @@
2773 class SyslogContext(OSContextGenerator):
2774
2775 def __call__(self):
2776- ctxt = {
2777- 'use_syslog': config('use-syslog')
2778- }
2779- return ctxt
2780+ ctxt = {'use_syslog': config('use-syslog')}
2781+ return ctxt
2782+
2783+
2784+class BindHostContext(OSContextGenerator):
2785+
2786+ def __call__(self):
2787+ if config('prefer-ipv6'):
2788+ return {'bind_host': '::'}
2789+ else:
2790+ return {'bind_host': '0.0.0.0'}
2791+
2792+
2793+class WorkerConfigContext(OSContextGenerator):
2794+
2795+ @property
2796+ def num_cpus(self):
2797+ try:
2798+ from psutil import NUM_CPUS
2799+ except ImportError:
2800+ apt_install('python-psutil', fatal=True)
2801+ from psutil import NUM_CPUS
2802+
2803+ return NUM_CPUS
2804+
2805+ def __call__(self):
2806+ multiplier = config('worker-multiplier') or 0
2807+ ctxt = {"workers": self.num_cpus * multiplier}
2808+ return ctxt
2809+
2810+
2811+class ZeroMQContext(OSContextGenerator):
2812+ interfaces = ['zeromq-configuration']
2813+
2814+ def __call__(self):
2815+ ctxt = {}
2816+ if is_relation_made('zeromq-configuration', 'host'):
2817+ for rid in relation_ids('zeromq-configuration'):
2818+ for unit in related_units(rid):
2819+ ctxt['zmq_nonce'] = relation_get('nonce', unit, rid)
2820+ ctxt['zmq_host'] = relation_get('host', unit, rid)
2821+ ctxt['zmq_redis_address'] = relation_get(
2822+ 'zmq_redis_address', unit, rid)
2823+
2824+ return ctxt
2825+
2826+
2827+class NotificationDriverContext(OSContextGenerator):
2828+
2829+ def __init__(self, zmq_relation='zeromq-configuration',
2830+ amqp_relation='amqp'):
2831+ """
2832+ :param zmq_relation: Name of Zeromq relation to check
2833+ """
2834+ self.zmq_relation = zmq_relation
2835+ self.amqp_relation = amqp_relation
2836+
2837+ def __call__(self):
2838+ ctxt = {'notifications': 'False'}
2839+ if is_relation_made(self.amqp_relation):
2840+ ctxt['notifications'] = "True"
2841+
2842+ return ctxt
2843+
2844+
2845+class SysctlContext(OSContextGenerator):
2846+ """This context check if the 'sysctl' option exists on configuration
2847+ then creates a file with the loaded contents"""
2848+ def __call__(self):
2849+ sysctl_dict = config('sysctl')
2850+ if sysctl_dict:
2851+ sysctl_create(sysctl_dict,
2852+ '/etc/sysctl.d/50-{0}.conf'.format(charm_name()))
2853+ return {'sysctl': sysctl_dict}
2854
2855=== added directory 'hooks/charmhelpers/contrib/openstack/files'
2856=== added file 'hooks/charmhelpers/contrib/openstack/files/__init__.py'
2857--- hooks/charmhelpers/contrib/openstack/files/__init__.py 1970-01-01 00:00:00 +0000
2858+++ hooks/charmhelpers/contrib/openstack/files/__init__.py 2015-03-03 23:20:09 +0000
2859@@ -0,0 +1,18 @@
2860+# Copyright 2014-2015 Canonical Limited.
2861+#
2862+# This file is part of charm-helpers.
2863+#
2864+# charm-helpers is free software: you can redistribute it and/or modify
2865+# it under the terms of the GNU Lesser General Public License version 3 as
2866+# published by the Free Software Foundation.
2867+#
2868+# charm-helpers is distributed in the hope that it will be useful,
2869+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2870+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2871+# GNU Lesser General Public License for more details.
2872+#
2873+# You should have received a copy of the GNU Lesser General Public License
2874+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2875+
2876+# dummy __init__.py to fool syncer into thinking this is a syncable python
2877+# module
2878
2879=== added file 'hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh'
2880--- hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh 1970-01-01 00:00:00 +0000
2881+++ hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh 2015-03-03 23:20:09 +0000
2882@@ -0,0 +1,32 @@
2883+#!/bin/bash
2884+#--------------------------------------------
2885+# This file is managed by Juju
2886+#--------------------------------------------
2887+#
2888+# Copyright 2009,2012 Canonical Ltd.
2889+# Author: Tom Haddon
2890+
2891+CRITICAL=0
2892+NOTACTIVE=''
2893+LOGFILE=/var/log/nagios/check_haproxy.log
2894+AUTH=$(grep -r "stats auth" /etc/haproxy | head -1 | awk '{print $4}')
2895+
2896+for appserver in $(grep ' server' /etc/haproxy/haproxy.cfg | awk '{print $2'});
2897+do
2898+ output=$(/usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 --regex="class=\"(active|backup)(2|3).*${appserver}" -e ' 200 OK')
2899+ if [ $? != 0 ]; then
2900+ date >> $LOGFILE
2901+ echo $output >> $LOGFILE
2902+ /usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -v | grep $appserver >> $LOGFILE 2>&1
2903+ CRITICAL=1
2904+ NOTACTIVE="${NOTACTIVE} $appserver"
2905+ fi
2906+done
2907+
2908+if [ $CRITICAL = 1 ]; then
2909+ echo "CRITICAL:${NOTACTIVE}"
2910+ exit 2
2911+fi
2912+
2913+echo "OK: All haproxy instances looking good"
2914+exit 0
2915
2916=== added file 'hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh'
2917--- hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh 1970-01-01 00:00:00 +0000
2918+++ hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh 2015-03-03 23:20:09 +0000
2919@@ -0,0 +1,30 @@
2920+#!/bin/bash
2921+#--------------------------------------------
2922+# This file is managed by Juju
2923+#--------------------------------------------
2924+#
2925+# Copyright 2009,2012 Canonical Ltd.
2926+# Author: Tom Haddon
2927+
2928+# These should be config options at some stage
2929+CURRQthrsh=0
2930+MAXQthrsh=100
2931+
2932+AUTH=$(grep -r "stats auth" /etc/haproxy | head -1 | awk '{print $4}')
2933+
2934+HAPROXYSTATS=$(/usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -u '/;csv' -v)
2935+
2936+for BACKEND in $(echo $HAPROXYSTATS| xargs -n1 | grep BACKEND | awk -F , '{print $1}')
2937+do
2938+ CURRQ=$(echo "$HAPROXYSTATS" | grep $BACKEND | grep BACKEND | cut -d , -f 3)
2939+ MAXQ=$(echo "$HAPROXYSTATS" | grep $BACKEND | grep BACKEND | cut -d , -f 4)
2940+
2941+ if [[ $CURRQ -gt $CURRQthrsh || $MAXQ -gt $MAXQthrsh ]] ; then
2942+ echo "CRITICAL: queue depth for $BACKEND - CURRENT:$CURRQ MAX:$MAXQ"
2943+ exit 2
2944+ fi
2945+done
2946+
2947+echo "OK: All haproxy queue depths looking good"
2948+exit 0
2949+
2950
2951=== added file 'hooks/charmhelpers/contrib/openstack/ip.py'
2952--- hooks/charmhelpers/contrib/openstack/ip.py 1970-01-01 00:00:00 +0000
2953+++ hooks/charmhelpers/contrib/openstack/ip.py 2015-03-03 23:20:09 +0000
2954@@ -0,0 +1,146 @@
2955+# Copyright 2014-2015 Canonical Limited.
2956+#
2957+# This file is part of charm-helpers.
2958+#
2959+# charm-helpers is free software: you can redistribute it and/or modify
2960+# it under the terms of the GNU Lesser General Public License version 3 as
2961+# published by the Free Software Foundation.
2962+#
2963+# charm-helpers is distributed in the hope that it will be useful,
2964+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2965+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2966+# GNU Lesser General Public License for more details.
2967+#
2968+# You should have received a copy of the GNU Lesser General Public License
2969+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2970+
2971+from charmhelpers.core.hookenv import (
2972+ config,
2973+ unit_get,
2974+)
2975+from charmhelpers.contrib.network.ip import (
2976+ get_address_in_network,
2977+ is_address_in_network,
2978+ is_ipv6,
2979+ get_ipv6_addr,
2980+)
2981+from charmhelpers.contrib.hahelpers.cluster import is_clustered
2982+
2983+from functools import partial
2984+
2985+PUBLIC = 'public'
2986+INTERNAL = 'int'
2987+ADMIN = 'admin'
2988+
2989+ADDRESS_MAP = {
2990+ PUBLIC: {
2991+ 'config': 'os-public-network',
2992+ 'fallback': 'public-address'
2993+ },
2994+ INTERNAL: {
2995+ 'config': 'os-internal-network',
2996+ 'fallback': 'private-address'
2997+ },
2998+ ADMIN: {
2999+ 'config': 'os-admin-network',
3000+ 'fallback': 'private-address'
3001+ }
3002+}
3003+
3004+
3005+def canonical_url(configs, endpoint_type=PUBLIC):
3006+ """Returns the correct HTTP URL to this host given the state of HTTPS
3007+ configuration, hacluster and charm configuration.
3008+
3009+ :param configs: OSTemplateRenderer config templating object to inspect
3010+ for a complete https context.
3011+ :param endpoint_type: str endpoint type to resolve.
3012+ :param returns: str base URL for services on the current service unit.
3013+ """
3014+ scheme = 'http'
3015+ if 'https' in configs.complete_contexts():
3016+ scheme = 'https'
3017+ address = resolve_address(endpoint_type)
3018+ if is_ipv6(address):
3019+ address = "[{}]".format(address)
3020+ return '%s://%s' % (scheme, address)
3021+
3022+
3023+def resolve_address(endpoint_type=PUBLIC):
3024+ """Return unit address depending on net config.
3025+
3026+ If unit is clustered with vip(s) and has net splits defined, return vip on
3027+ correct network. If clustered with no nets defined, return primary vip.
3028+
3029+ If not clustered, return unit address ensuring address is on configured net
3030+ split if one is configured.
3031+
3032+ :param endpoint_type: Network endpoing type
3033+ """
3034+ resolved_address = None
3035+ vips = config('vip')
3036+ if vips:
3037+ vips = vips.split()
3038+
3039+ net_type = ADDRESS_MAP[endpoint_type]['config']
3040+ net_addr = config(net_type)
3041+ net_fallback = ADDRESS_MAP[endpoint_type]['fallback']
3042+ clustered = is_clustered()
3043+ if clustered:
3044+ if not net_addr:
3045+ # If no net-splits defined, we expect a single vip
3046+ resolved_address = vips[0]
3047+ else:
3048+ for vip in vips:
3049+ if is_address_in_network(net_addr, vip):
3050+ resolved_address = vip
3051+ break
3052+ else:
3053+ if config('prefer-ipv6'):
3054+ fallback_addr = get_ipv6_addr(exc_list=vips)[0]
3055+ else:
3056+ fallback_addr = unit_get(net_fallback)
3057+
3058+ resolved_address = get_address_in_network(net_addr, fallback_addr)
3059+
3060+ if resolved_address is None:
3061+ raise ValueError("Unable to resolve a suitable IP address based on "
3062+ "charm state and configuration. (net_type=%s, "
3063+ "clustered=%s)" % (net_type, clustered))
3064+
3065+ return resolved_address
3066+
3067+
3068+def endpoint_url(configs, url_template, port, endpoint_type=PUBLIC,
3069+ override=None):
3070+ """Returns the correct endpoint URL to advertise to Keystone.
3071+
3072+ This method provides the correct endpoint URL which should be advertised to
3073+ the keystone charm for endpoint creation. This method allows for the url to
3074+ be overridden to force a keystone endpoint to have specific URL for any of
3075+ the defined scopes (admin, internal, public).
3076+
3077+ :param configs: OSTemplateRenderer config templating object to inspect
3078+ for a complete https context.
3079+ :param url_template: str format string for creating the url template. Only
3080+ two values will be passed - the scheme+hostname
3081+ returned by the canonical_url and the port.
3082+ :param endpoint_type: str endpoint type to resolve.
3083+ :param override: str the name of the config option which overrides the
3084+ endpoint URL defined by the charm itself. None will
3085+ disable any overrides (default).
3086+ """
3087+ if override:
3088+ # Return any user-defined overrides for the keystone endpoint URL.
3089+ user_value = config(override)
3090+ if user_value:
3091+ return user_value.strip()
3092+
3093+ return url_template % (canonical_url(configs, endpoint_type), port)
3094+
3095+
3096+public_endpoint = partial(endpoint_url, endpoint_type=PUBLIC)
3097+
3098+internal_endpoint = partial(endpoint_url, endpoint_type=INTERNAL)
3099+
3100+admin_endpoint = partial(endpoint_url, endpoint_type=ADMIN)
3101
3102=== modified file 'hooks/charmhelpers/contrib/openstack/neutron.py'
3103--- hooks/charmhelpers/contrib/openstack/neutron.py 2014-05-20 18:55:23 +0000
3104+++ hooks/charmhelpers/contrib/openstack/neutron.py 2015-03-03 23:20:09 +0000
3105@@ -1,3 +1,19 @@
3106+# Copyright 2014-2015 Canonical Limited.
3107+#
3108+# This file is part of charm-helpers.
3109+#
3110+# charm-helpers is free software: you can redistribute it and/or modify
3111+# it under the terms of the GNU Lesser General Public License version 3 as
3112+# published by the Free Software Foundation.
3113+#
3114+# charm-helpers is distributed in the hope that it will be useful,
3115+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3116+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3117+# GNU Lesser General Public License for more details.
3118+#
3119+# You should have received a copy of the GNU Lesser General Public License
3120+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3121+
3122 # Various utilies for dealing with Neutron and the renaming from Quantum.
3123
3124 from subprocess import check_output
3125@@ -14,7 +30,7 @@
3126 def headers_package():
3127 """Ensures correct linux-headers for running kernel are installed,
3128 for building DKMS package"""
3129- kver = check_output(['uname', '-r']).strip()
3130+ kver = check_output(['uname', '-r']).decode('UTF-8').strip()
3131 return 'linux-headers-%s' % kver
3132
3133 QUANTUM_CONF_DIR = '/etc/quantum'
3134@@ -22,7 +38,7 @@
3135
3136 def kernel_version():
3137 """ Retrieve the current major kernel version as a tuple e.g. (3, 13) """
3138- kver = check_output(['uname', '-r']).strip()
3139+ kver = check_output(['uname', '-r']).decode('UTF-8').strip()
3140 kver = kver.split('.')
3141 return (int(kver[0]), int(kver[1]))
3142
3143@@ -128,6 +144,41 @@
3144 'server_packages': ['neutron-server',
3145 'neutron-plugin-vmware'],
3146 'server_services': ['neutron-server']
3147+ },
3148+ 'n1kv': {
3149+ 'config': '/etc/neutron/plugins/cisco/cisco_plugins.ini',
3150+ 'driver': 'neutron.plugins.cisco.network_plugin.PluginV2',
3151+ 'contexts': [
3152+ context.SharedDBContext(user=config('neutron-database-user'),
3153+ database=config('neutron-database'),
3154+ relation_prefix='neutron',
3155+ ssl_dir=NEUTRON_CONF_DIR)],
3156+ 'services': [],
3157+ 'packages': [[headers_package()] + determine_dkms_package(),
3158+ ['neutron-plugin-cisco']],
3159+ 'server_packages': ['neutron-server',
3160+ 'neutron-plugin-cisco'],
3161+ 'server_services': ['neutron-server']
3162+ },
3163+ 'Calico': {
3164+ 'config': '/etc/neutron/plugins/ml2/ml2_conf.ini',
3165+ 'driver': 'neutron.plugins.ml2.plugin.Ml2Plugin',
3166+ 'contexts': [
3167+ context.SharedDBContext(user=config('neutron-database-user'),
3168+ database=config('neutron-database'),
3169+ relation_prefix='neutron',
3170+ ssl_dir=NEUTRON_CONF_DIR)],
3171+ 'services': ['calico-felix',
3172+ 'bird',
3173+ 'neutron-dhcp-agent',
3174+ 'nova-api-metadata'],
3175+ 'packages': [[headers_package()] + determine_dkms_package(),
3176+ ['calico-compute',
3177+ 'bird',
3178+ 'neutron-dhcp-agent',
3179+ 'nova-api-metadata']],
3180+ 'server_packages': ['neutron-server', 'calico-control'],
3181+ 'server_services': ['neutron-server']
3182 }
3183 }
3184 if release >= 'icehouse':
3185@@ -148,7 +199,8 @@
3186 elif manager == 'neutron':
3187 plugins = neutron_plugins()
3188 else:
3189- log('Error: Network manager does not support plugins.')
3190+ log("Network manager '%s' does not support plugins." % (manager),
3191+ level=ERROR)
3192 raise Exception
3193
3194 try:
3195
3196=== modified file 'hooks/charmhelpers/contrib/openstack/templates/__init__.py'
3197--- hooks/charmhelpers/contrib/openstack/templates/__init__.py 2014-05-20 18:55:23 +0000
3198+++ hooks/charmhelpers/contrib/openstack/templates/__init__.py 2015-03-03 23:20:09 +0000
3199@@ -1,2 +1,18 @@
3200+# Copyright 2014-2015 Canonical Limited.
3201+#
3202+# This file is part of charm-helpers.
3203+#
3204+# charm-helpers is free software: you can redistribute it and/or modify
3205+# it under the terms of the GNU Lesser General Public License version 3 as
3206+# published by the Free Software Foundation.
3207+#
3208+# charm-helpers is distributed in the hope that it will be useful,
3209+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3210+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3211+# GNU Lesser General Public License for more details.
3212+#
3213+# You should have received a copy of the GNU Lesser General Public License
3214+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3215+
3216 # dummy __init__.py to fool syncer into thinking this is a syncable python
3217 # module
3218
3219=== modified file 'hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg'
3220--- hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg 2014-05-20 18:55:23 +0000
3221+++ hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg 2015-03-03 23:20:09 +0000
3222@@ -1,6 +1,6 @@
3223 global
3224- log 127.0.0.1 local0
3225- log 127.0.0.1 local1 notice
3226+ log {{ local_host }} local0
3227+ log {{ local_host }} local1 notice
3228 maxconn 20000
3229 user haproxy
3230 group haproxy
3231@@ -14,10 +14,19 @@
3232 retries 3
3233 timeout queue 1000
3234 timeout connect 1000
3235+{% if haproxy_client_timeout -%}
3236+ timeout client {{ haproxy_client_timeout }}
3237+{% else -%}
3238 timeout client 30000
3239+{% endif -%}
3240+
3241+{% if haproxy_server_timeout -%}
3242+ timeout server {{ haproxy_server_timeout }}
3243+{% else -%}
3244 timeout server 30000
3245+{% endif -%}
3246
3247-listen stats :8888
3248+listen stats {{ stat_port }}
3249 mode http
3250 stats enable
3251 stats hide-version
3252@@ -25,12 +34,25 @@
3253 stats uri /
3254 stats auth admin:password
3255
3256-{% if units -%}
3257-{% for service, ports in service_ports.iteritems() -%}
3258-listen {{ service }} 0.0.0.0:{{ ports[0] }}
3259- balance roundrobin
3260- {% for unit, address in units.iteritems() -%}
3261+{% if frontends -%}
3262+{% for service, ports in service_ports.items() -%}
3263+frontend tcp-in_{{ service }}
3264+ bind *:{{ ports[0] }}
3265+ {% if ipv6 -%}
3266+ bind :::{{ ports[0] }}
3267+ {% endif -%}
3268+ {% for frontend in frontends -%}
3269+ acl net_{{ frontend }} dst {{ frontends[frontend]['network'] }}
3270+ use_backend {{ service }}_{{ frontend }} if net_{{ frontend }}
3271+ {% endfor -%}
3272+ default_backend {{ service }}_{{ default_backend }}
3273+
3274+{% for frontend in frontends -%}
3275+backend {{ service }}_{{ frontend }}
3276+ balance leastconn
3277+ {% for unit, address in frontends[frontend]['backends'].items() -%}
3278 server {{ unit }} {{ address }}:{{ ports[1] }} check
3279 {% endfor %}
3280 {% endfor -%}
3281+{% endfor -%}
3282 {% endif -%}
3283
3284=== modified file 'hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend'
3285--- hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend 2014-05-20 18:55:23 +0000
3286+++ hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend 2015-03-03 23:20:09 +0000
3287@@ -1,16 +1,18 @@
3288 {% if endpoints -%}
3289-{% for ext, int in endpoints -%}
3290-Listen {{ ext }}
3291-NameVirtualHost *:{{ ext }}
3292-<VirtualHost *:{{ ext }}>
3293- ServerName {{ private_address }}
3294+{% for ext_port in ext_ports -%}
3295+Listen {{ ext_port }}
3296+{% endfor -%}
3297+{% for address, endpoint, ext, int in endpoints -%}
3298+<VirtualHost {{ address }}:{{ ext }}>
3299+ ServerName {{ endpoint }}
3300 SSLEngine on
3301- SSLCertificateFile /etc/apache2/ssl/{{ namespace }}/cert
3302- SSLCertificateKeyFile /etc/apache2/ssl/{{ namespace }}/key
3303+ SSLCertificateFile /etc/apache2/ssl/{{ namespace }}/cert_{{ endpoint }}
3304+ SSLCertificateKeyFile /etc/apache2/ssl/{{ namespace }}/key_{{ endpoint }}
3305 ProxyPass / http://localhost:{{ int }}/
3306 ProxyPassReverse / http://localhost:{{ int }}/
3307 ProxyPreserveHost on
3308 </VirtualHost>
3309+{% endfor -%}
3310 <Proxy *>
3311 Order deny,allow
3312 Allow from all
3313@@ -19,5 +21,4 @@
3314 Order allow,deny
3315 Allow from all
3316 </Location>
3317-{% endfor -%}
3318 {% endif -%}
3319
3320=== modified file 'hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend.conf'
3321--- hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend.conf 2014-06-18 17:34:53 +0000
3322+++ hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend.conf 2015-03-03 23:20:09 +0000
3323@@ -1,16 +1,18 @@
3324 {% if endpoints -%}
3325-{% for ext, int in endpoints -%}
3326-Listen {{ ext }}
3327-NameVirtualHost *:{{ ext }}
3328-<VirtualHost *:{{ ext }}>
3329- ServerName {{ private_address }}
3330+{% for ext_port in ext_ports -%}
3331+Listen {{ ext_port }}
3332+{% endfor -%}
3333+{% for address, endpoint, ext, int in endpoints -%}
3334+<VirtualHost {{ address }}:{{ ext }}>
3335+ ServerName {{ endpoint }}
3336 SSLEngine on
3337- SSLCertificateFile /etc/apache2/ssl/{{ namespace }}/cert
3338- SSLCertificateKeyFile /etc/apache2/ssl/{{ namespace }}/key
3339+ SSLCertificateFile /etc/apache2/ssl/{{ namespace }}/cert_{{ endpoint }}
3340+ SSLCertificateKeyFile /etc/apache2/ssl/{{ namespace }}/key_{{ endpoint }}
3341 ProxyPass / http://localhost:{{ int }}/
3342 ProxyPassReverse / http://localhost:{{ int }}/
3343 ProxyPreserveHost on
3344 </VirtualHost>
3345+{% endfor -%}
3346 <Proxy *>
3347 Order deny,allow
3348 Allow from all
3349@@ -19,5 +21,4 @@
3350 Order allow,deny
3351 Allow from all
3352 </Location>
3353-{% endfor -%}
3354 {% endif -%}
3355
3356=== added file 'hooks/charmhelpers/contrib/openstack/templates/zeromq'
3357--- hooks/charmhelpers/contrib/openstack/templates/zeromq 1970-01-01 00:00:00 +0000
3358+++ hooks/charmhelpers/contrib/openstack/templates/zeromq 2015-03-03 23:20:09 +0000
3359@@ -0,0 +1,14 @@
3360+{% if zmq_host -%}
3361+# ZeroMQ configuration (restart-nonce: {{ zmq_nonce }})
3362+rpc_backend = zmq
3363+rpc_zmq_host = {{ zmq_host }}
3364+{% if zmq_redis_address -%}
3365+rpc_zmq_matchmaker = oslo.messaging._drivers.matchmaker_redis.MatchMakerRedis
3366+matchmaker_heartbeat_freq = 15
3367+matchmaker_heartbeat_ttl = 30
3368+[matchmaker_redis]
3369+host = {{ zmq_redis_address }}
3370+{% else -%}
3371+rpc_zmq_matchmaker = oslo.messaging._drivers.matchmaker_ring.MatchMakerRing
3372+{% endif -%}
3373+{% endif -%}
3374
3375=== modified file 'hooks/charmhelpers/contrib/openstack/templating.py'
3376--- hooks/charmhelpers/contrib/openstack/templating.py 2014-05-20 18:55:23 +0000
3377+++ hooks/charmhelpers/contrib/openstack/templating.py 2015-03-03 23:20:09 +0000
3378@@ -1,13 +1,29 @@
3379+# Copyright 2014-2015 Canonical Limited.
3380+#
3381+# This file is part of charm-helpers.
3382+#
3383+# charm-helpers is free software: you can redistribute it and/or modify
3384+# it under the terms of the GNU Lesser General Public License version 3 as
3385+# published by the Free Software Foundation.
3386+#
3387+# charm-helpers is distributed in the hope that it will be useful,
3388+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3389+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3390+# GNU Lesser General Public License for more details.
3391+#
3392+# You should have received a copy of the GNU Lesser General Public License
3393+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3394+
3395 import os
3396
3397+import six
3398+
3399 from charmhelpers.fetch import apt_install
3400-
3401 from charmhelpers.core.hookenv import (
3402 log,
3403 ERROR,
3404 INFO
3405 )
3406-
3407 from charmhelpers.contrib.openstack.utils import OPENSTACK_CODENAMES
3408
3409 try:
3410@@ -30,20 +46,20 @@
3411 loading dir.
3412
3413 A charm may also ship a templates dir with this module
3414- and it will be appended to the bottom of the search list, eg:
3415- hooks/charmhelpers/contrib/openstack/templates.
3416-
3417- :param templates_dir: str: Base template directory containing release
3418- sub-directories.
3419- :param os_release : str: OpenStack release codename to construct template
3420- loader.
3421-
3422- :returns : jinja2.ChoiceLoader constructed with a list of
3423- jinja2.FilesystemLoaders, ordered in descending
3424- order by OpenStack release.
3425+ and it will be appended to the bottom of the search list, eg::
3426+
3427+ hooks/charmhelpers/contrib/openstack/templates
3428+
3429+ :param templates_dir (str): Base template directory containing release
3430+ sub-directories.
3431+ :param os_release (str): OpenStack release codename to construct template
3432+ loader.
3433+ :returns: jinja2.ChoiceLoader constructed with a list of
3434+ jinja2.FilesystemLoaders, ordered in descending
3435+ order by OpenStack release.
3436 """
3437 tmpl_dirs = [(rel, os.path.join(templates_dir, rel))
3438- for rel in OPENSTACK_CODENAMES.itervalues()]
3439+ for rel in six.itervalues(OPENSTACK_CODENAMES)]
3440
3441 if not os.path.isdir(templates_dir):
3442 log('Templates directory not found @ %s.' % templates_dir,
3443@@ -111,7 +127,8 @@
3444 and ease the burden of managing config templates across multiple OpenStack
3445 releases.
3446
3447- Basic usage:
3448+ Basic usage::
3449+
3450 # import some common context generates from charmhelpers
3451 from charmhelpers.contrib.openstack import context
3452
3453@@ -131,21 +148,19 @@
3454 # write out all registered configs
3455 configs.write_all()
3456
3457- Details:
3458+ **OpenStack Releases and template loading**
3459
3460- OpenStack Releases and template loading
3461- ---------------------------------------
3462 When the object is instantiated, it is associated with a specific OS
3463 release. This dictates how the template loader will be constructed.
3464
3465 The constructed loader attempts to load the template from several places
3466 in the following order:
3467- - from the most recent OS release-specific template dir (if one exists)
3468- - the base templates_dir
3469- - a template directory shipped in the charm with this helper file.
3470-
3471-
3472- For the example above, '/tmp/templates' contains the following structure:
3473+ - from the most recent OS release-specific template dir (if one exists)
3474+ - the base templates_dir
3475+ - a template directory shipped in the charm with this helper file.
3476+
3477+ For the example above, '/tmp/templates' contains the following structure::
3478+
3479 /tmp/templates/nova.conf
3480 /tmp/templates/api-paste.ini
3481 /tmp/templates/grizzly/api-paste.ini
3482@@ -169,8 +184,8 @@
3483 $CHARM/hooks/charmhelpers/contrib/openstack/templates. This allows
3484 us to ship common templates (haproxy, apache) with the helpers.
3485
3486- Context generators
3487- ---------------------------------------
3488+ **Context generators**
3489+
3490 Context generators are used to generate template contexts during hook
3491 execution. Doing so may require inspecting service relations, charm
3492 config, etc. When registered, a config file is associated with a list
3493@@ -259,7 +274,7 @@
3494 """
3495 Write out all registered config files.
3496 """
3497- [self.write(k) for k in self.templates.iterkeys()]
3498+ [self.write(k) for k in six.iterkeys(self.templates)]
3499
3500 def set_release(self, openstack_release):
3501 """
3502@@ -276,5 +291,5 @@
3503 '''
3504 interfaces = []
3505 [interfaces.extend(i.complete_contexts())
3506- for i in self.templates.itervalues()]
3507+ for i in six.itervalues(self.templates)]
3508 return interfaces
3509
3510=== modified file 'hooks/charmhelpers/contrib/openstack/utils.py'
3511--- hooks/charmhelpers/contrib/openstack/utils.py 2014-06-18 17:34:53 +0000
3512+++ hooks/charmhelpers/contrib/openstack/utils.py 2015-03-03 23:20:09 +0000
3513@@ -1,19 +1,42 @@
3514 #!/usr/bin/python
3515
3516+# Copyright 2014-2015 Canonical Limited.
3517+#
3518+# This file is part of charm-helpers.
3519+#
3520+# charm-helpers is free software: you can redistribute it and/or modify
3521+# it under the terms of the GNU Lesser General Public License version 3 as
3522+# published by the Free Software Foundation.
3523+#
3524+# charm-helpers is distributed in the hope that it will be useful,
3525+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3526+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3527+# GNU Lesser General Public License for more details.
3528+#
3529+# You should have received a copy of the GNU Lesser General Public License
3530+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3531+
3532 # Common python helper functions used for OpenStack charms.
3533 from collections import OrderedDict
3534+from functools import wraps
3535
3536 import subprocess
3537+import json
3538 import os
3539-import socket
3540 import sys
3541
3542+import six
3543+import yaml
3544+
3545+from charmhelpers.contrib.network import ip
3546+
3547 from charmhelpers.core.hookenv import (
3548 config,
3549 log as juju_log,
3550 charm_dir,
3551- ERROR,
3552- INFO
3553+ INFO,
3554+ relation_ids,
3555+ relation_set
3556 )
3557
3558 from charmhelpers.contrib.storage.linux.lvm import (
3559@@ -22,8 +45,13 @@
3560 remove_lvm_physical_volume,
3561 )
3562
3563+from charmhelpers.contrib.network.ip import (
3564+ get_ipv6_addr
3565+)
3566+
3567 from charmhelpers.core.host import lsb_release, mounts, umount
3568-from charmhelpers.fetch import apt_install
3569+from charmhelpers.fetch import apt_install, apt_cache, install_remote
3570+from charmhelpers.contrib.python.packages import pip_install
3571 from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk
3572 from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device
3573
3574@@ -42,6 +70,7 @@
3575 ('saucy', 'havana'),
3576 ('trusty', 'icehouse'),
3577 ('utopic', 'juno'),
3578+ ('vivid', 'kilo'),
3579 ])
3580
3581
3582@@ -53,6 +82,7 @@
3583 ('2013.2', 'havana'),
3584 ('2014.1', 'icehouse'),
3585 ('2014.2', 'juno'),
3586+ ('2015.1', 'kilo'),
3587 ])
3588
3589 # The ugly duckling
3590@@ -70,6 +100,11 @@
3591 ('1.13.0', 'icehouse'),
3592 ('1.12.0', 'icehouse'),
3593 ('1.11.0', 'icehouse'),
3594+ ('2.0.0', 'juno'),
3595+ ('2.1.0', 'juno'),
3596+ ('2.2.0', 'juno'),
3597+ ('2.2.1', 'kilo'),
3598+ ('2.2.2', 'kilo'),
3599 ])
3600
3601 DEFAULT_LOOPBACK_SIZE = '5G'
3602@@ -84,6 +119,8 @@
3603 '''Derive OpenStack release codename from a given installation source.'''
3604 ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
3605 rel = ''
3606+ if src is None:
3607+ return rel
3608 if src in ['distro', 'distro-proposed']:
3609 try:
3610 rel = UBUNTU_OPENSTACK_RELEASE[ubuntu_rel]
3611@@ -100,7 +137,7 @@
3612
3613 # Best guess match based on deb string provided
3614 if src.startswith('deb') or src.startswith('ppa'):
3615- for k, v in OPENSTACK_CODENAMES.iteritems():
3616+ for k, v in six.iteritems(OPENSTACK_CODENAMES):
3617 if v in src:
3618 return v
3619
3620@@ -121,7 +158,7 @@
3621
3622 def get_os_version_codename(codename):
3623 '''Determine OpenStack version number from codename.'''
3624- for k, v in OPENSTACK_CODENAMES.iteritems():
3625+ for k, v in six.iteritems(OPENSTACK_CODENAMES):
3626 if v == codename:
3627 return k
3628 e = 'Could not derive OpenStack version for '\
3629@@ -132,13 +169,8 @@
3630 def get_os_codename_package(package, fatal=True):
3631 '''Derive OpenStack release codename from an installed package.'''
3632 import apt_pkg as apt
3633- apt.init()
3634-
3635- # Tell apt to build an in-memory cache to prevent race conditions (if
3636- # another process is already building the cache).
3637- apt.config.set("Dir::Cache::pkgcache", "")
3638-
3639- cache = apt.Cache()
3640+
3641+ cache = apt_cache()
3642
3643 try:
3644 pkg = cache[package]
3645@@ -186,10 +218,10 @@
3646 else:
3647 vers_map = OPENSTACK_CODENAMES
3648
3649- for version, cname in vers_map.iteritems():
3650+ for version, cname in six.iteritems(vers_map):
3651 if cname == codename:
3652 return version
3653- #e = "Could not determine OpenStack version for package: %s" % pkg
3654+ # e = "Could not determine OpenStack version for package: %s" % pkg
3655 # error_out(e)
3656
3657
3658@@ -278,6 +310,9 @@
3659 'juno': 'trusty-updates/juno',
3660 'juno/updates': 'trusty-updates/juno',
3661 'juno/proposed': 'trusty-proposed/juno',
3662+ 'kilo': 'trusty-updates/kilo',
3663+ 'kilo/updates': 'trusty-updates/kilo',
3664+ 'kilo/proposed': 'trusty-proposed/kilo',
3665 }
3666
3667 try:
3668@@ -310,7 +345,7 @@
3669 rc_script.write(
3670 "#!/bin/bash\n")
3671 [rc_script.write('export %s=%s\n' % (u, p))
3672- for u, p in env_vars.iteritems() if u != "script_path"]
3673+ for u, p in six.iteritems(env_vars) if u != "script_path"]
3674
3675
3676 def openstack_upgrade_available(package):
3677@@ -343,8 +378,8 @@
3678 '''
3679 _none = ['None', 'none', None]
3680 if (block_device in _none):
3681- error_out('prepare_storage(): Missing required input: '
3682- 'block_device=%s.' % block_device, level=ERROR)
3683+ error_out('prepare_storage(): Missing required input: block_device=%s.'
3684+ % block_device)
3685
3686 if block_device.startswith('/dev/'):
3687 bdev = block_device
3688@@ -360,8 +395,7 @@
3689 bdev = '/dev/%s' % block_device
3690
3691 if not is_block_device(bdev):
3692- error_out('Failed to locate valid block device at %s' % bdev,
3693- level=ERROR)
3694+ error_out('Failed to locate valid block device at %s' % bdev)
3695
3696 return bdev
3697
3698@@ -388,74 +422,155 @@
3699 else:
3700 zap_disk(block_device)
3701
3702-
3703-def is_ip(address):
3704- """
3705- Returns True if address is a valid IP address.
3706- """
3707- try:
3708- # Test to see if already an IPv4 address
3709- socket.inet_aton(address)
3710- return True
3711- except socket.error:
3712- return False
3713-
3714-
3715-def ns_query(address):
3716- try:
3717- import dns.resolver
3718- except ImportError:
3719- apt_install('python-dnspython')
3720- import dns.resolver
3721-
3722- if isinstance(address, dns.name.Name):
3723- rtype = 'PTR'
3724- elif isinstance(address, basestring):
3725- rtype = 'A'
3726- else:
3727- return None
3728-
3729- answers = dns.resolver.query(address, rtype)
3730- if answers:
3731- return str(answers[0])
3732- return None
3733-
3734-
3735-def get_host_ip(hostname):
3736- """
3737- Resolves the IP for a given hostname, or returns
3738- the input if it is already an IP.
3739- """
3740- if is_ip(hostname):
3741- return hostname
3742-
3743- return ns_query(hostname)
3744-
3745-
3746-def get_hostname(address, fqdn=True):
3747- """
3748- Resolves hostname for given IP, or returns the input
3749- if it is already a hostname.
3750- """
3751- if is_ip(address):
3752- try:
3753- import dns.reversename
3754- except ImportError:
3755- apt_install('python-dnspython')
3756- import dns.reversename
3757-
3758- rev = dns.reversename.from_address(address)
3759- result = ns_query(rev)
3760- if not result:
3761- return None
3762- else:
3763- result = address
3764-
3765- if fqdn:
3766- # strip trailing .
3767- if result.endswith('.'):
3768- return result[:-1]
3769- else:
3770- return result
3771- else:
3772- return result.split('.')[0]
3773+is_ip = ip.is_ip
3774+ns_query = ip.ns_query
3775+get_host_ip = ip.get_host_ip
3776+get_hostname = ip.get_hostname
3777+
3778+
3779+def get_matchmaker_map(mm_file='/etc/oslo/matchmaker_ring.json'):
3780+ mm_map = {}
3781+ if os.path.isfile(mm_file):
3782+ with open(mm_file, 'r') as f:
3783+ mm_map = json.load(f)
3784+ return mm_map
3785+
3786+
3787+def sync_db_with_multi_ipv6_addresses(database, database_user,
3788+ relation_prefix=None):
3789+ hosts = get_ipv6_addr(dynamic_only=False)
3790+
3791+ kwargs = {'database': database,
3792+ 'username': database_user,
3793+ 'hostname': json.dumps(hosts)}
3794+
3795+ if relation_prefix:
3796+ for key in list(kwargs.keys()):
3797+ kwargs["%s_%s" % (relation_prefix, key)] = kwargs[key]
3798+ del kwargs[key]
3799+
3800+ for rid in relation_ids('shared-db'):
3801+ relation_set(relation_id=rid, **kwargs)
3802+
3803+
3804+def os_requires_version(ostack_release, pkg):
3805+ """
3806+ Decorator for hook to specify minimum supported release
3807+ """
3808+ def wrap(f):
3809+ @wraps(f)
3810+ def wrapped_f(*args):
3811+ if os_release(pkg) < ostack_release:
3812+ raise Exception("This hook is not supported on releases"
3813+ " before %s" % ostack_release)
3814+ f(*args)
3815+ return wrapped_f
3816+ return wrap
3817+
3818+
3819+def git_install_requested():
3820+ """Returns true if openstack-origin-git is specified."""
3821+ return config('openstack-origin-git') != "None"
3822+
3823+
3824+requirements_dir = None
3825+
3826+
3827+def git_clone_and_install(file_name, core_project):
3828+ """Clone/install all OpenStack repos specified in yaml config file."""
3829+ global requirements_dir
3830+
3831+ if file_name == "None":
3832+ return
3833+
3834+ yaml_file = os.path.join(charm_dir(), file_name)
3835+
3836+ # clone/install the requirements project first
3837+ installed = _git_clone_and_install_subset(yaml_file,
3838+ whitelist=['requirements'])
3839+ if 'requirements' not in installed:
3840+ error_out('requirements git repository must be specified')
3841+
3842+ # clone/install all other projects except requirements and the core project
3843+ blacklist = ['requirements', core_project]
3844+ _git_clone_and_install_subset(yaml_file, blacklist=blacklist,
3845+ update_requirements=True)
3846+
3847+ # clone/install the core project
3848+ whitelist = [core_project]
3849+ installed = _git_clone_and_install_subset(yaml_file, whitelist=whitelist,
3850+ update_requirements=True)
3851+ if core_project not in installed:
3852+ error_out('{} git repository must be specified'.format(core_project))
3853+
3854+
3855+def _git_clone_and_install_subset(yaml_file, whitelist=[], blacklist=[],
3856+ update_requirements=False):
3857+ """Clone/install subset of OpenStack repos specified in yaml config file."""
3858+ global requirements_dir
3859+ installed = []
3860+
3861+ with open(yaml_file, 'r') as fd:
3862+ projects = yaml.load(fd)
3863+ for proj, val in projects.items():
3864+ # The project subset is chosen based on the following 3 rules:
3865+ # 1) If project is in blacklist, we don't clone/install it, period.
3866+ # 2) If whitelist is empty, we clone/install everything else.
3867+ # 3) If whitelist is not empty, we clone/install everything in the
3868+ # whitelist.
3869+ if proj in blacklist:
3870+ continue
3871+ if whitelist and proj not in whitelist:
3872+ continue
3873+ repo = val['repository']
3874+ branch = val['branch']
3875+ repo_dir = _git_clone_and_install_single(repo, branch,
3876+ update_requirements)
3877+ if proj == 'requirements':
3878+ requirements_dir = repo_dir
3879+ installed.append(proj)
3880+ return installed
3881+
3882+
3883+def _git_clone_and_install_single(repo, branch, update_requirements=False):
3884+ """Clone and install a single git repository."""
3885+ dest_parent_dir = "/mnt/openstack-git/"
3886+ dest_dir = os.path.join(dest_parent_dir, os.path.basename(repo))
3887+
3888+ if not os.path.exists(dest_parent_dir):
3889+ juju_log('Host dir not mounted at {}. '
3890+ 'Creating directory there instead.'.format(dest_parent_dir))
3891+ os.mkdir(dest_parent_dir)
3892+
3893+ if not os.path.exists(dest_dir):
3894+ juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch))
3895+ repo_dir = install_remote(repo, dest=dest_parent_dir, branch=branch)
3896+ else:
3897+ repo_dir = dest_dir
3898+
3899+ if update_requirements:
3900+ if not requirements_dir:
3901+ error_out('requirements repo must be cloned before '
3902+ 'updating from global requirements.')
3903+ _git_update_requirements(repo_dir, requirements_dir)
3904+
3905+ juju_log('Installing git repo from dir: {}'.format(repo_dir))
3906+ pip_install(repo_dir)
3907+
3908+ return repo_dir
3909+
3910+
3911+def _git_update_requirements(package_dir, reqs_dir):
3912+ """Update from global requirements.
3913+
3914+ Update an OpenStack git directory's requirements.txt and
3915+ test-requirements.txt from global-requirements.txt."""
3916+ orig_dir = os.getcwd()
3917+ os.chdir(reqs_dir)
3918+ cmd = "python update.py {}".format(package_dir)
3919+ try:
3920+ subprocess.check_call(cmd.split(' '))
3921+ except subprocess.CalledProcessError:
3922+ package = os.path.basename(package_dir)
3923+ error_out("Error updating {} from global-requirements.txt".format(package))
3924+ os.chdir(orig_dir)
3925
3926=== modified file 'hooks/charmhelpers/core/__init__.py'
3927--- hooks/charmhelpers/core/__init__.py 2014-05-20 18:55:23 +0000
3928+++ hooks/charmhelpers/core/__init__.py 2015-03-03 23:20:09 +0000
3929@@ -0,0 +1,15 @@
3930+# Copyright 2014-2015 Canonical Limited.
3931+#
3932+# This file is part of charm-helpers.
3933+#
3934+# charm-helpers is free software: you can redistribute it and/or modify
3935+# it under the terms of the GNU Lesser General Public License version 3 as
3936+# published by the Free Software Foundation.
3937+#
3938+# charm-helpers is distributed in the hope that it will be useful,
3939+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3940+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3941+# GNU Lesser General Public License for more details.
3942+#
3943+# You should have received a copy of the GNU Lesser General Public License
3944+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3945
3946=== added file 'hooks/charmhelpers/core/decorators.py'
3947--- hooks/charmhelpers/core/decorators.py 1970-01-01 00:00:00 +0000
3948+++ hooks/charmhelpers/core/decorators.py 2015-03-03 23:20:09 +0000
3949@@ -0,0 +1,57 @@
3950+# Copyright 2014-2015 Canonical Limited.
3951+#
3952+# This file is part of charm-helpers.
3953+#
3954+# charm-helpers is free software: you can redistribute it and/or modify
3955+# it under the terms of the GNU Lesser General Public License version 3 as
3956+# published by the Free Software Foundation.
3957+#
3958+# charm-helpers is distributed in the hope that it will be useful,
3959+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3960+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3961+# GNU Lesser General Public License for more details.
3962+#
3963+# You should have received a copy of the GNU Lesser General Public License
3964+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3965+
3966+#
3967+# Copyright 2014 Canonical Ltd.
3968+#
3969+# Authors:
3970+# Edward Hope-Morley <opentastic@gmail.com>
3971+#
3972+
3973+import time
3974+
3975+from charmhelpers.core.hookenv import (
3976+ log,
3977+ INFO,
3978+)
3979+
3980+
3981+def retry_on_exception(num_retries, base_delay=0, exc_type=Exception):
3982+ """If the decorated function raises exception exc_type, allow num_retries
3983+ retry attempts before raise the exception.
3984+ """
3985+ def _retry_on_exception_inner_1(f):
3986+ def _retry_on_exception_inner_2(*args, **kwargs):
3987+ retries = num_retries
3988+ multiplier = 1
3989+ while True:
3990+ try:
3991+ return f(*args, **kwargs)
3992+ except exc_type:
3993+ if not retries:
3994+ raise
3995+
3996+ delay = base_delay * multiplier
3997+ multiplier += 1
3998+ log("Retrying '%s' %d more times (delay=%s)" %
3999+ (f.__name__, retries, delay), level=INFO)
4000+ retries -= 1
4001+ if delay:
4002+ time.sleep(delay)
4003+
4004+ return _retry_on_exception_inner_2
4005+
4006+ return _retry_on_exception_inner_1
4007
4008=== modified file 'hooks/charmhelpers/core/fstab.py'
4009--- hooks/charmhelpers/core/fstab.py 2014-06-18 18:54:20 +0000
4010+++ hooks/charmhelpers/core/fstab.py 2015-03-03 23:20:09 +0000
4011@@ -1,12 +1,29 @@
4012 #!/usr/bin/env python
4013 # -*- coding: utf-8 -*-
4014
4015+# Copyright 2014-2015 Canonical Limited.
4016+#
4017+# This file is part of charm-helpers.
4018+#
4019+# charm-helpers is free software: you can redistribute it and/or modify
4020+# it under the terms of the GNU Lesser General Public License version 3 as
4021+# published by the Free Software Foundation.
4022+#
4023+# charm-helpers is distributed in the hope that it will be useful,
4024+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4025+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4026+# GNU Lesser General Public License for more details.
4027+#
4028+# You should have received a copy of the GNU Lesser General Public License
4029+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4030+
4031+import io
4032+import os
4033+
4034 __author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
4035
4036-import os
4037-
4038-
4039-class Fstab(file):
4040+
4041+class Fstab(io.FileIO):
4042 """This class extends file in order to implement a file reader/writer
4043 for file `/etc/fstab`
4044 """
4045@@ -24,8 +41,8 @@
4046 options = "defaults"
4047
4048 self.options = options
4049- self.d = d
4050- self.p = p
4051+ self.d = int(d)
4052+ self.p = int(p)
4053
4054 def __eq__(self, o):
4055 return str(self) == str(o)
4056@@ -45,19 +62,22 @@
4057 self._path = path
4058 else:
4059 self._path = self.DEFAULT_PATH
4060- file.__init__(self, self._path, 'r+')
4061+ super(Fstab, self).__init__(self._path, 'rb+')
4062
4063 def _hydrate_entry(self, line):
4064+ # NOTE: use split with no arguments to split on any
4065+ # whitespace including tabs
4066 return Fstab.Entry(*filter(
4067 lambda x: x not in ('', None),
4068- line.strip("\n").split(" ")))
4069+ line.strip("\n").split()))
4070
4071 @property
4072 def entries(self):
4073 self.seek(0)
4074 for line in self.readlines():
4075+ line = line.decode('us-ascii')
4076 try:
4077- if not line.startswith("#"):
4078+ if line.strip() and not line.strip().startswith("#"):
4079 yield self._hydrate_entry(line)
4080 except ValueError:
4081 pass
4082@@ -73,18 +93,18 @@
4083 if self.get_entry_by_attr('device', entry.device):
4084 return False
4085
4086- self.write(str(entry) + '\n')
4087+ self.write((str(entry) + '\n').encode('us-ascii'))
4088 self.truncate()
4089 return entry
4090
4091 def remove_entry(self, entry):
4092 self.seek(0)
4093
4094- lines = self.readlines()
4095+ lines = [l.decode('us-ascii') for l in self.readlines()]
4096
4097 found = False
4098 for index, line in enumerate(lines):
4099- if not line.startswith("#"):
4100+ if line.strip() and not line.strip().startswith("#"):
4101 if self._hydrate_entry(line) == entry:
4102 found = True
4103 break
4104@@ -95,7 +115,7 @@
4105 lines.remove(line)
4106
4107 self.seek(0)
4108- self.write(''.join(lines))
4109+ self.write(''.join(lines).encode('us-ascii'))
4110 self.truncate()
4111 return True
4112
4113
4114=== modified file 'hooks/charmhelpers/core/hookenv.py'
4115--- hooks/charmhelpers/core/hookenv.py 2014-05-20 18:55:23 +0000
4116+++ hooks/charmhelpers/core/hookenv.py 2015-03-03 23:20:09 +0000
4117@@ -1,3 +1,19 @@
4118+# Copyright 2014-2015 Canonical Limited.
4119+#
4120+# This file is part of charm-helpers.
4121+#
4122+# charm-helpers is free software: you can redistribute it and/or modify
4123+# it under the terms of the GNU Lesser General Public License version 3 as
4124+# published by the Free Software Foundation.
4125+#
4126+# charm-helpers is distributed in the hope that it will be useful,
4127+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4128+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4129+# GNU Lesser General Public License for more details.
4130+#
4131+# You should have received a copy of the GNU Lesser General Public License
4132+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4133+
4134 "Interactions with the Juju environment"
4135 # Copyright 2013 Canonical Ltd.
4136 #
4137@@ -9,9 +25,14 @@
4138 import yaml
4139 import subprocess
4140 import sys
4141-import UserDict
4142 from subprocess import CalledProcessError
4143
4144+import six
4145+if not six.PY3:
4146+ from UserDict import UserDict
4147+else:
4148+ from collections import UserDict
4149+
4150 CRITICAL = "CRITICAL"
4151 ERROR = "ERROR"
4152 WARNING = "WARNING"
4153@@ -25,7 +46,7 @@
4154 def cached(func):
4155 """Cache return values for multiple executions of func + args
4156
4157- For example:
4158+ For example::
4159
4160 @cached
4161 def unit_get(attribute):
4162@@ -63,16 +84,18 @@
4163 command = ['juju-log']
4164 if level:
4165 command += ['-l', level]
4166+ if not isinstance(message, six.string_types):
4167+ message = repr(message)
4168 command += [message]
4169 subprocess.call(command)
4170
4171
4172-class Serializable(UserDict.IterableUserDict):
4173+class Serializable(UserDict):
4174 """Wrapper, an object that can be serialized to yaml or json"""
4175
4176 def __init__(self, obj):
4177 # wrap the object
4178- UserDict.IterableUserDict.__init__(self)
4179+ UserDict.__init__(self)
4180 self.data = obj
4181
4182 def __getattr__(self, attr):
4183@@ -156,12 +179,15 @@
4184
4185
4186 class Config(dict):
4187- """A Juju charm config dictionary that can write itself to
4188- disk (as json) and track which values have changed since
4189- the previous hook invocation.
4190-
4191- Do not instantiate this object directly - instead call
4192- ``hookenv.config()``
4193+ """A dictionary representation of the charm's config.yaml, with some
4194+ extra features:
4195+
4196+ - See which values in the dictionary have changed since the previous hook.
4197+ - For values that have changed, see what the previous value was.
4198+ - Store arbitrary data for use in a later hook.
4199+
4200+ NOTE: Do not instantiate this object directly - instead call
4201+ ``hookenv.config()``, which will return an instance of :class:`Config`.
4202
4203 Example usage::
4204
4205@@ -170,8 +196,8 @@
4206 >>> config = hookenv.config()
4207 >>> config['foo']
4208 'bar'
4209+ >>> # store a new key/value for later use
4210 >>> config['mykey'] = 'myval'
4211- >>> config.save()
4212
4213
4214 >>> # user runs `juju set mycharm foo=baz`
4215@@ -188,22 +214,40 @@
4216 >>> # keys/values that we add are preserved across hooks
4217 >>> config['mykey']
4218 'myval'
4219- >>> # don't forget to save at the end of hook!
4220- >>> config.save()
4221
4222 """
4223 CONFIG_FILE_NAME = '.juju-persistent-config'
4224
4225 def __init__(self, *args, **kw):
4226 super(Config, self).__init__(*args, **kw)
4227+ self.implicit_save = True
4228 self._prev_dict = None
4229 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
4230 if os.path.exists(self.path):
4231 self.load_previous()
4232
4233+ def __getitem__(self, key):
4234+ """For regular dict lookups, check the current juju config first,
4235+ then the previous (saved) copy. This ensures that user-saved values
4236+ will be returned by a dict lookup.
4237+
4238+ """
4239+ try:
4240+ return dict.__getitem__(self, key)
4241+ except KeyError:
4242+ return (self._prev_dict or {})[key]
4243+
4244+ def keys(self):
4245+ prev_keys = []
4246+ if self._prev_dict is not None:
4247+ prev_keys = self._prev_dict.keys()
4248+ return list(set(prev_keys + list(dict.keys(self))))
4249+
4250 def load_previous(self, path=None):
4251- """Load previous copy of config from disk so that current values
4252- can be compared to previous values.
4253+ """Load previous copy of config from disk.
4254+
4255+ In normal usage you don't need to call this method directly - it
4256+ is called automatically at object initialization.
4257
4258 :param path:
4259
4260@@ -218,8 +262,8 @@
4261 self._prev_dict = json.load(f)
4262
4263 def changed(self, key):
4264- """Return true if the value for this key has changed since
4265- the last save.
4266+ """Return True if the current value for this key is different from
4267+ the previous value.
4268
4269 """
4270 if self._prev_dict is None:
4271@@ -228,7 +272,7 @@
4272
4273 def previous(self, key):
4274 """Return previous value for this key, or None if there
4275- is no "previous" value.
4276+ is no previous value.
4277
4278 """
4279 if self._prev_dict:
4280@@ -238,11 +282,17 @@
4281 def save(self):
4282 """Save this config to disk.
4283
4284- Preserves items in _prev_dict that do not exist in self.
4285+ If the charm is using the :mod:`Services Framework <services.base>`
4286+ or :meth:'@hook <Hooks.hook>' decorator, this
4287+ is called automatically at the end of successful hook execution.
4288+ Otherwise, it should be called directly by user code.
4289+
4290+ To disable automatic saves, set ``implicit_save=False`` on this
4291+ instance.
4292
4293 """
4294 if self._prev_dict:
4295- for k, v in self._prev_dict.iteritems():
4296+ for k, v in six.iteritems(self._prev_dict):
4297 if k not in self:
4298 self[k] = v
4299 with open(self.path, 'w') as f:
4300@@ -257,7 +307,8 @@
4301 config_cmd_line.append(scope)
4302 config_cmd_line.append('--format=json')
4303 try:
4304- config_data = json.loads(subprocess.check_output(config_cmd_line))
4305+ config_data = json.loads(
4306+ subprocess.check_output(config_cmd_line).decode('UTF-8'))
4307 if scope is not None:
4308 return config_data
4309 return Config(config_data)
4310@@ -276,21 +327,22 @@
4311 if unit:
4312 _args.append(unit)
4313 try:
4314- return json.loads(subprocess.check_output(_args))
4315+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
4316 except ValueError:
4317 return None
4318- except CalledProcessError, e:
4319+ except CalledProcessError as e:
4320 if e.returncode == 2:
4321 return None
4322 raise
4323
4324
4325-def relation_set(relation_id=None, relation_settings={}, **kwargs):
4326+def relation_set(relation_id=None, relation_settings=None, **kwargs):
4327 """Set relation information for the current unit"""
4328+ relation_settings = relation_settings if relation_settings else {}
4329 relation_cmd_line = ['relation-set']
4330 if relation_id is not None:
4331 relation_cmd_line.extend(('-r', relation_id))
4332- for k, v in (relation_settings.items() + kwargs.items()):
4333+ for k, v in (list(relation_settings.items()) + list(kwargs.items())):
4334 if v is None:
4335 relation_cmd_line.append('{}='.format(k))
4336 else:
4337@@ -307,7 +359,8 @@
4338 relid_cmd_line = ['relation-ids', '--format=json']
4339 if reltype is not None:
4340 relid_cmd_line.append(reltype)
4341- return json.loads(subprocess.check_output(relid_cmd_line)) or []
4342+ return json.loads(
4343+ subprocess.check_output(relid_cmd_line).decode('UTF-8')) or []
4344 return []
4345
4346
4347@@ -318,7 +371,8 @@
4348 units_cmd_line = ['relation-list', '--format=json']
4349 if relid is not None:
4350 units_cmd_line.extend(('-r', relid))
4351- return json.loads(subprocess.check_output(units_cmd_line)) or []
4352+ return json.loads(
4353+ subprocess.check_output(units_cmd_line).decode('UTF-8')) or []
4354
4355
4356 @cached
4357@@ -358,21 +412,31 @@
4358
4359
4360 @cached
4361+def metadata():
4362+ """Get the current charm metadata.yaml contents as a python object"""
4363+ with open(os.path.join(charm_dir(), 'metadata.yaml')) as md:
4364+ return yaml.safe_load(md)
4365+
4366+
4367+@cached
4368 def relation_types():
4369 """Get a list of relation types supported by this charm"""
4370- charmdir = os.environ.get('CHARM_DIR', '')
4371- mdf = open(os.path.join(charmdir, 'metadata.yaml'))
4372- md = yaml.safe_load(mdf)
4373 rel_types = []
4374+ md = metadata()
4375 for key in ('provides', 'requires', 'peers'):
4376 section = md.get(key)
4377 if section:
4378 rel_types.extend(section.keys())
4379- mdf.close()
4380 return rel_types
4381
4382
4383 @cached
4384+def charm_name():
4385+ """Get the name of the current charm as is specified on metadata.yaml"""
4386+ return metadata().get('name')
4387+
4388+
4389+@cached
4390 def relations():
4391 """Get a nested dictionary of relation data for all related units"""
4392 rels = {}
4393@@ -427,7 +491,7 @@
4394 """Get the unit ID for the remote unit"""
4395 _args = ['unit-get', '--format=json', attribute]
4396 try:
4397- return json.loads(subprocess.check_output(_args))
4398+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
4399 except ValueError:
4400 return None
4401
4402@@ -445,27 +509,29 @@
4403 class Hooks(object):
4404 """A convenient handler for hook functions.
4405
4406- Example:
4407+ Example::
4408+
4409 hooks = Hooks()
4410
4411 # register a hook, taking its name from the function name
4412 @hooks.hook()
4413 def install():
4414- ...
4415+ pass # your code here
4416
4417 # register a hook, providing a custom hook name
4418 @hooks.hook("config-changed")
4419 def config_changed():
4420- ...
4421+ pass # your code here
4422
4423 if __name__ == "__main__":
4424 # execute a hook based on the name the program is called by
4425 hooks.execute(sys.argv)
4426 """
4427
4428- def __init__(self):
4429+ def __init__(self, config_save=True):
4430 super(Hooks, self).__init__()
4431 self._hooks = {}
4432+ self._config_save = config_save
4433
4434 def register(self, name, function):
4435 """Register a hook"""
4436@@ -476,6 +542,10 @@
4437 hook_name = os.path.basename(args[0])
4438 if hook_name in self._hooks:
4439 self._hooks[hook_name]()
4440+ if self._config_save:
4441+ cfg = config()
4442+ if cfg.implicit_save:
4443+ cfg.save()
4444 else:
4445 raise UnregisteredHookError(hook_name)
4446
4447
4448=== modified file 'hooks/charmhelpers/core/host.py'
4449--- hooks/charmhelpers/core/host.py 2014-06-18 17:34:53 +0000
4450+++ hooks/charmhelpers/core/host.py 2015-03-03 23:20:09 +0000
4451@@ -1,3 +1,19 @@
4452+# Copyright 2014-2015 Canonical Limited.
4453+#
4454+# This file is part of charm-helpers.
4455+#
4456+# charm-helpers is free software: you can redistribute it and/or modify
4457+# it under the terms of the GNU Lesser General Public License version 3 as
4458+# published by the Free Software Foundation.
4459+#
4460+# charm-helpers is distributed in the hope that it will be useful,
4461+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4462+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4463+# GNU Lesser General Public License for more details.
4464+#
4465+# You should have received a copy of the GNU Lesser General Public License
4466+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4467+
4468 """Tools for working with the host system"""
4469 # Copyright 2012 Canonical Ltd.
4470 #
4471@@ -6,17 +22,20 @@
4472 # Matthew Wedgwood <matthew.wedgwood@canonical.com>
4473
4474 import os
4475+import re
4476 import pwd
4477 import grp
4478 import random
4479 import string
4480 import subprocess
4481 import hashlib
4482-
4483+from contextlib import contextmanager
4484 from collections import OrderedDict
4485
4486-from hookenv import log
4487-from fstab import Fstab
4488+import six
4489+
4490+from .hookenv import log
4491+from .fstab import Fstab
4492
4493
4494 def service_start(service_name):
4495@@ -52,7 +71,9 @@
4496 def service_running(service):
4497 """Determine whether a system service is running"""
4498 try:
4499- output = subprocess.check_output(['service', service, 'status'])
4500+ output = subprocess.check_output(
4501+ ['service', service, 'status'],
4502+ stderr=subprocess.STDOUT).decode('UTF-8')
4503 except subprocess.CalledProcessError:
4504 return False
4505 else:
4506@@ -62,6 +83,18 @@
4507 return False
4508
4509
4510+def service_available(service_name):
4511+ """Determine whether a system service is available"""
4512+ try:
4513+ subprocess.check_output(
4514+ ['service', service_name, 'status'],
4515+ stderr=subprocess.STDOUT).decode('UTF-8')
4516+ except subprocess.CalledProcessError as e:
4517+ return 'unrecognized service' not in e.output
4518+ else:
4519+ return True
4520+
4521+
4522 def adduser(username, password=None, shell='/bin/bash', system_user=False):
4523 """Add a user to the system"""
4524 try:
4525@@ -84,6 +117,26 @@
4526 return user_info
4527
4528
4529+def add_group(group_name, system_group=False):
4530+ """Add a group to the system"""
4531+ try:
4532+ group_info = grp.getgrnam(group_name)
4533+ log('group {0} already exists!'.format(group_name))
4534+ except KeyError:
4535+ log('creating group {0}'.format(group_name))
4536+ cmd = ['addgroup']
4537+ if system_group:
4538+ cmd.append('--system')
4539+ else:
4540+ cmd.extend([
4541+ '--group',
4542+ ])
4543+ cmd.append(group_name)
4544+ subprocess.check_call(cmd)
4545+ group_info = grp.getgrnam(group_name)
4546+ return group_info
4547+
4548+
4549 def add_user_to_group(username, group):
4550 """Add a user to a group"""
4551 cmd = [
4552@@ -103,7 +156,7 @@
4553 cmd.append(from_path)
4554 cmd.append(to_path)
4555 log(" ".join(cmd))
4556- return subprocess.check_output(cmd).strip()
4557+ return subprocess.check_output(cmd).decode('UTF-8').strip()
4558
4559
4560 def symlink(source, destination):
4561@@ -118,28 +171,31 @@
4562 subprocess.check_call(cmd)
4563
4564
4565-def mkdir(path, owner='root', group='root', perms=0555, force=False):
4566+def mkdir(path, owner='root', group='root', perms=0o555, force=False):
4567 """Create a directory"""
4568 log("Making dir {} {}:{} {:o}".format(path, owner, group,
4569 perms))
4570 uid = pwd.getpwnam(owner).pw_uid
4571 gid = grp.getgrnam(group).gr_gid
4572 realpath = os.path.abspath(path)
4573- if os.path.exists(realpath):
4574- if force and not os.path.isdir(realpath):
4575+ path_exists = os.path.exists(realpath)
4576+ if path_exists and force:
4577+ if not os.path.isdir(realpath):
4578 log("Removing non-directory file {} prior to mkdir()".format(path))
4579 os.unlink(realpath)
4580- else:
4581+ os.makedirs(realpath, perms)
4582+ elif not path_exists:
4583 os.makedirs(realpath, perms)
4584 os.chown(realpath, uid, gid)
4585-
4586-
4587-def write_file(path, content, owner='root', group='root', perms=0444):
4588- """Create or overwrite a file with the contents of a string"""
4589+ os.chmod(realpath, perms)
4590+
4591+
4592+def write_file(path, content, owner='root', group='root', perms=0o444):
4593+ """Create or overwrite a file with the contents of a byte string."""
4594 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
4595 uid = pwd.getpwnam(owner).pw_uid
4596 gid = grp.getgrnam(group).gr_gid
4597- with open(path, 'w') as target:
4598+ with open(path, 'wb') as target:
4599 os.fchown(target.fileno(), uid, gid)
4600 os.fchmod(target.fileno(), perms)
4601 target.write(content)
4602@@ -165,7 +221,7 @@
4603 cmd_args.extend([device, mountpoint])
4604 try:
4605 subprocess.check_output(cmd_args)
4606- except subprocess.CalledProcessError, e:
4607+ except subprocess.CalledProcessError as e:
4608 log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
4609 return False
4610
4611@@ -179,7 +235,7 @@
4612 cmd_args = ['umount', mountpoint]
4613 try:
4614 subprocess.check_output(cmd_args)
4615- except subprocess.CalledProcessError, e:
4616+ except subprocess.CalledProcessError as e:
4617 log('Error unmounting {}\n{}'.format(mountpoint, e.output))
4618 return False
4619
4620@@ -197,38 +253,63 @@
4621 return system_mounts
4622
4623
4624-def file_hash(path):
4625- """Generate a md5 hash of the contents of 'path' or None if not found """
4626+def file_hash(path, hash_type='md5'):
4627+ """
4628+ Generate a hash checksum of the contents of 'path' or None if not found.
4629+
4630+ :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
4631+ such as md5, sha1, sha256, sha512, etc.
4632+ """
4633 if os.path.exists(path):
4634- h = hashlib.md5()
4635- with open(path, 'r') as source:
4636- h.update(source.read()) # IGNORE:E1101 - it does have update
4637+ h = getattr(hashlib, hash_type)()
4638+ with open(path, 'rb') as source:
4639+ h.update(source.read())
4640 return h.hexdigest()
4641 else:
4642 return None
4643
4644
4645+def check_hash(path, checksum, hash_type='md5'):
4646+ """
4647+ Validate a file using a cryptographic checksum.
4648+
4649+ :param str checksum: Value of the checksum used to validate the file.
4650+ :param str hash_type: Hash algorithm used to generate `checksum`.
4651+ Can be any hash alrgorithm supported by :mod:`hashlib`,
4652+ such as md5, sha1, sha256, sha512, etc.
4653+ :raises ChecksumError: If the file fails the checksum
4654+
4655+ """
4656+ actual_checksum = file_hash(path, hash_type)
4657+ if checksum != actual_checksum:
4658+ raise ChecksumError("'%s' != '%s'" % (checksum, actual_checksum))
4659+
4660+
4661+class ChecksumError(ValueError):
4662+ pass
4663+
4664+
4665 def restart_on_change(restart_map, stopstart=False):
4666 """Restart services based on configuration files changing
4667
4668- This function is used a decorator, for example
4669+ This function is used a decorator, for example::
4670
4671 @restart_on_change({
4672 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
4673 })
4674 def ceph_client_changed():
4675- ...
4676+ pass # your code here
4677
4678 In this example, the cinder-api and cinder-volume services
4679 would be restarted if /etc/ceph/ceph.conf is changed by the
4680 ceph_client_changed function.
4681 """
4682 def wrap(f):
4683- def wrapped_f(*args):
4684+ def wrapped_f(*args, **kwargs):
4685 checksums = {}
4686 for path in restart_map:
4687 checksums[path] = file_hash(path)
4688- f(*args)
4689+ f(*args, **kwargs)
4690 restarts = []
4691 for path in restart_map:
4692 if checksums[path] != file_hash(path):
4693@@ -260,7 +341,7 @@
4694 if length is None:
4695 length = random.choice(range(35, 45))
4696 alphanumeric_chars = [
4697- l for l in (string.letters + string.digits)
4698+ l for l in (string.ascii_letters + string.digits)
4699 if l not in 'l0QD1vAEIOUaeiou']
4700 random_chars = [
4701 random.choice(alphanumeric_chars) for _ in range(length)]
4702@@ -269,18 +350,24 @@
4703
4704 def list_nics(nic_type):
4705 '''Return a list of nics of given type(s)'''
4706- if isinstance(nic_type, basestring):
4707+ if isinstance(nic_type, six.string_types):
4708 int_types = [nic_type]
4709 else:
4710 int_types = nic_type
4711 interfaces = []
4712 for int_type in int_types:
4713 cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
4714- ip_output = subprocess.check_output(cmd).split('\n')
4715+ ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
4716 ip_output = (line for line in ip_output if line)
4717 for line in ip_output:
4718 if line.split()[1].startswith(int_type):
4719- interfaces.append(line.split()[1].replace(":", ""))
4720+ matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line)
4721+ if matched:
4722+ interface = matched.groups()[0]
4723+ else:
4724+ interface = line.split()[1].replace(":", "")
4725+ interfaces.append(interface)
4726+
4727 return interfaces
4728
4729
4730@@ -292,7 +379,7 @@
4731
4732 def get_nic_mtu(nic):
4733 cmd = ['ip', 'addr', 'show', nic]
4734- ip_output = subprocess.check_output(cmd).split('\n')
4735+ ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
4736 mtu = ""
4737 for line in ip_output:
4738 words = line.split()
4739@@ -303,7 +390,7 @@
4740
4741 def get_nic_hwaddr(nic):
4742 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
4743- ip_output = subprocess.check_output(cmd)
4744+ ip_output = subprocess.check_output(cmd).decode('UTF-8')
4745 hwaddr = ""
4746 words = ip_output.split()
4747 if 'link/ether' in words:
4748@@ -313,13 +400,47 @@
4749
4750 def cmp_pkgrevno(package, revno, pkgcache=None):
4751 '''Compare supplied revno with the revno of the installed package
4752- 1 => Installed revno is greater than supplied arg
4753- 0 => Installed revno is the same as supplied arg
4754- -1 => Installed revno is less than supplied arg
4755+
4756+ * 1 => Installed revno is greater than supplied arg
4757+ * 0 => Installed revno is the same as supplied arg
4758+ * -1 => Installed revno is less than supplied arg
4759+
4760+ This function imports apt_cache function from charmhelpers.fetch if
4761+ the pkgcache argument is None. Be sure to add charmhelpers.fetch if
4762+ you call this function, or pass an apt_pkg.Cache() instance.
4763 '''
4764 import apt_pkg
4765 if not pkgcache:
4766- apt_pkg.init()
4767- pkgcache = apt_pkg.Cache()
4768+ from charmhelpers.fetch import apt_cache
4769+ pkgcache = apt_cache()
4770 pkg = pkgcache[package]
4771 return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
4772+
4773+
4774+@contextmanager
4775+def chdir(d):
4776+ cur = os.getcwd()
4777+ try:
4778+ yield os.chdir(d)
4779+ finally:
4780+ os.chdir(cur)
4781+
4782+
4783+def chownr(path, owner, group, follow_links=True):
4784+ uid = pwd.getpwnam(owner).pw_uid
4785+ gid = grp.getgrnam(group).gr_gid
4786+ if follow_links:
4787+ chown = os.chown
4788+ else:
4789+ chown = os.lchown
4790+
4791+ for root, dirs, files in os.walk(path):
4792+ for name in dirs + files:
4793+ full = os.path.join(root, name)
4794+ broken_symlink = os.path.lexists(full) and not os.path.exists(full)
4795+ if not broken_symlink:
4796+ chown(full, uid, gid)
4797+
4798+
4799+def lchownr(path, owner, group):
4800+ chownr(path, owner, group, follow_links=False)
4801
4802=== added directory 'hooks/charmhelpers/core/services'
4803=== added file 'hooks/charmhelpers/core/services/__init__.py'
4804--- hooks/charmhelpers/core/services/__init__.py 1970-01-01 00:00:00 +0000
4805+++ hooks/charmhelpers/core/services/__init__.py 2015-03-03 23:20:09 +0000
4806@@ -0,0 +1,18 @@
4807+# Copyright 2014-2015 Canonical Limited.
4808+#
4809+# This file is part of charm-helpers.
4810+#
4811+# charm-helpers is free software: you can redistribute it and/or modify
4812+# it under the terms of the GNU Lesser General Public License version 3 as
4813+# published by the Free Software Foundation.
4814+#
4815+# charm-helpers is distributed in the hope that it will be useful,
4816+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4817+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4818+# GNU Lesser General Public License for more details.
4819+#
4820+# You should have received a copy of the GNU Lesser General Public License
4821+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4822+
4823+from .base import * # NOQA
4824+from .helpers import * # NOQA
4825
4826=== added file 'hooks/charmhelpers/core/services/base.py'
4827--- hooks/charmhelpers/core/services/base.py 1970-01-01 00:00:00 +0000
4828+++ hooks/charmhelpers/core/services/base.py 2015-03-03 23:20:09 +0000
4829@@ -0,0 +1,329 @@
4830+# Copyright 2014-2015 Canonical Limited.
4831+#
4832+# This file is part of charm-helpers.
4833+#
4834+# charm-helpers is free software: you can redistribute it and/or modify
4835+# it under the terms of the GNU Lesser General Public License version 3 as
4836+# published by the Free Software Foundation.
4837+#
4838+# charm-helpers is distributed in the hope that it will be useful,
4839+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4840+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4841+# GNU Lesser General Public License for more details.
4842+#
4843+# You should have received a copy of the GNU Lesser General Public License
4844+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4845+
4846+import os
4847+import re
4848+import json
4849+from collections import Iterable
4850+
4851+from charmhelpers.core import host
4852+from charmhelpers.core import hookenv
4853+
4854+
4855+__all__ = ['ServiceManager', 'ManagerCallback',
4856+ 'PortManagerCallback', 'open_ports', 'close_ports', 'manage_ports',
4857+ 'service_restart', 'service_stop']
4858+
4859+
4860+class ServiceManager(object):
4861+ def __init__(self, services=None):
4862+ """
4863+ Register a list of services, given their definitions.
4864+
4865+ Service definitions are dicts in the following formats (all keys except
4866+ 'service' are optional)::
4867+
4868+ {
4869+ "service": <service name>,
4870+ "required_data": <list of required data contexts>,
4871+ "provided_data": <list of provided data contexts>,
4872+ "data_ready": <one or more callbacks>,
4873+ "data_lost": <one or more callbacks>,
4874+ "start": <one or more callbacks>,
4875+ "stop": <one or more callbacks>,
4876+ "ports": <list of ports to manage>,
4877+ }
4878+
4879+ The 'required_data' list should contain dicts of required data (or
4880+ dependency managers that act like dicts and know how to collect the data).
4881+ Only when all items in the 'required_data' list are populated are the list
4882+ of 'data_ready' and 'start' callbacks executed. See `is_ready()` for more
4883+ information.
4884+
4885+ The 'provided_data' list should contain relation data providers, most likely
4886+ a subclass of :class:`charmhelpers.core.services.helpers.RelationContext`,
4887+ that will indicate a set of data to set on a given relation.
4888+
4889+ The 'data_ready' value should be either a single callback, or a list of
4890+ callbacks, to be called when all items in 'required_data' pass `is_ready()`.
4891+ Each callback will be called with the service name as the only parameter.
4892+ After all of the 'data_ready' callbacks are called, the 'start' callbacks
4893+ are fired.
4894+
4895+ The 'data_lost' value should be either a single callback, or a list of
4896+ callbacks, to be called when a 'required_data' item no longer passes
4897+ `is_ready()`. Each callback will be called with the service name as the
4898+ only parameter. After all of the 'data_lost' callbacks are called,
4899+ the 'stop' callbacks are fired.
4900+
4901+ The 'start' value should be either a single callback, or a list of
4902+ callbacks, to be called when starting the service, after the 'data_ready'
4903+ callbacks are complete. Each callback will be called with the service
4904+ name as the only parameter. This defaults to
4905+ `[host.service_start, services.open_ports]`.
4906+
4907+ The 'stop' value should be either a single callback, or a list of
4908+ callbacks, to be called when stopping the service. If the service is
4909+ being stopped because it no longer has all of its 'required_data', this
4910+ will be called after all of the 'data_lost' callbacks are complete.
4911+ Each callback will be called with the service name as the only parameter.
4912+ This defaults to `[services.close_ports, host.service_stop]`.
4913+
4914+ The 'ports' value should be a list of ports to manage. The default
4915+ 'start' handler will open the ports after the service is started,
4916+ and the default 'stop' handler will close the ports prior to stopping
4917+ the service.
4918+
4919+
4920+ Examples:
4921+
4922+ The following registers an Upstart service called bingod that depends on
4923+ a mongodb relation and which runs a custom `db_migrate` function prior to
4924+ restarting the service, and a Runit service called spadesd::
4925+
4926+ manager = services.ServiceManager([
4927+ {
4928+ 'service': 'bingod',
4929+ 'ports': [80, 443],
4930+ 'required_data': [MongoRelation(), config(), {'my': 'data'}],
4931+ 'data_ready': [
4932+ services.template(source='bingod.conf'),
4933+ services.template(source='bingod.ini',
4934+ target='/etc/bingod.ini',
4935+ owner='bingo', perms=0400),
4936+ ],
4937+ },
4938+ {
4939+ 'service': 'spadesd',
4940+ 'data_ready': services.template(source='spadesd_run.j2',
4941+ target='/etc/sv/spadesd/run',
4942+ perms=0555),
4943+ 'start': runit_start,
4944+ 'stop': runit_stop,
4945+ },
4946+ ])
4947+ manager.manage()
4948+ """
4949+ self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
4950+ self._ready = None
4951+ self.services = {}
4952+ for service in services or []:
4953+ service_name = service['service']
4954+ self.services[service_name] = service
4955+
4956+ def manage(self):
4957+ """
4958+ Handle the current hook by doing The Right Thing with the registered services.
4959+ """
4960+ hook_name = hookenv.hook_name()
4961+ if hook_name == 'stop':
4962+ self.stop_services()
4963+ else:
4964+ self.provide_data()
4965+ self.reconfigure_services()
4966+ cfg = hookenv.config()
4967+ if cfg.implicit_save:
4968+ cfg.save()
4969+
4970+ def provide_data(self):
4971+ """
4972+ Set the relation data for each provider in the ``provided_data`` list.
4973+
4974+ A provider must have a `name` attribute, which indicates which relation
4975+ to set data on, and a `provide_data()` method, which returns a dict of
4976+ data to set.
4977+ """
4978+ hook_name = hookenv.hook_name()
4979+ for service in self.services.values():
4980+ for provider in service.get('provided_data', []):
4981+ if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):
4982+ data = provider.provide_data()
4983+ _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data
4984+ if _ready:
4985+ hookenv.relation_set(None, data)
4986+
4987+ def reconfigure_services(self, *service_names):
4988+ """
4989+ Update all files for one or more registered services, and,
4990+ if ready, optionally restart them.
4991+
4992+ If no service names are given, reconfigures all registered services.
4993+ """
4994+ for service_name in service_names or self.services.keys():
4995+ if self.is_ready(service_name):
4996+ self.fire_event('data_ready', service_name)
4997+ self.fire_event('start', service_name, default=[
4998+ service_restart,
4999+ manage_ports])
5000+ self.save_ready(service_name)
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches