Merge lp:~pwlars/charms/wily/hwcert-pxe-server/init-pxe-server into lp:~canonical-hw-cert/charms/wily/hwcert-pxe-server/trunk

Proposed by Paul Larson
Status: Merged
Approved by: Paul Larson
Approved revision: 1
Merged at revision: 1
Proposed branch: lp:~pwlars/charms/wily/hwcert-pxe-server/init-pxe-server
Merge into: lp:~canonical-hw-cert/charms/wily/hwcert-pxe-server/trunk
Diff against target: 11628 lines (+11223/-0)
73 files modified
README (+14/-0)
config.yaml (+5/-0)
hooks/actions.py (+113/-0)
hooks/charmhelpers/__init__.py (+38/-0)
hooks/charmhelpers/contrib/__init__.py (+15/-0)
hooks/charmhelpers/contrib/amulet/__init__.py (+15/-0)
hooks/charmhelpers/contrib/amulet/deployment.py (+93/-0)
hooks/charmhelpers/contrib/amulet/utils.py (+194/-0)
hooks/charmhelpers/contrib/ansible/__init__.py (+190/-0)
hooks/charmhelpers/contrib/charmhelpers/__init__.py (+208/-0)
hooks/charmhelpers/contrib/charmsupport/__init__.py (+15/-0)
hooks/charmhelpers/contrib/charmsupport/nrpe.py (+324/-0)
hooks/charmhelpers/contrib/charmsupport/volumes.py (+175/-0)
hooks/charmhelpers/contrib/database/mysql.py (+372/-0)
hooks/charmhelpers/contrib/hahelpers/__init__.py (+15/-0)
hooks/charmhelpers/contrib/hahelpers/apache.py (+82/-0)
hooks/charmhelpers/contrib/hahelpers/cluster.py (+268/-0)
hooks/charmhelpers/contrib/network/__init__.py (+15/-0)
hooks/charmhelpers/contrib/network/ip.py (+367/-0)
hooks/charmhelpers/contrib/network/ovs/__init__.py (+96/-0)
hooks/charmhelpers/contrib/network/ufw.py (+276/-0)
hooks/charmhelpers/contrib/openstack/__init__.py (+15/-0)
hooks/charmhelpers/contrib/openstack/alternatives.py (+33/-0)
hooks/charmhelpers/contrib/openstack/amulet/__init__.py (+15/-0)
hooks/charmhelpers/contrib/openstack/amulet/deployment.py (+108/-0)
hooks/charmhelpers/contrib/openstack/amulet/utils.py (+294/-0)
hooks/charmhelpers/contrib/openstack/context.py (+1054/-0)
hooks/charmhelpers/contrib/openstack/ip.py (+109/-0)
hooks/charmhelpers/contrib/openstack/neutron.py (+239/-0)
hooks/charmhelpers/contrib/openstack/templates/__init__.py (+18/-0)
hooks/charmhelpers/contrib/openstack/templating.py (+295/-0)
hooks/charmhelpers/contrib/openstack/utils.py (+641/-0)
hooks/charmhelpers/contrib/peerstorage/__init__.py (+148/-0)
hooks/charmhelpers/contrib/python/__init__.py (+15/-0)
hooks/charmhelpers/contrib/python/debug.py (+56/-0)
hooks/charmhelpers/contrib/python/packages.py (+96/-0)
hooks/charmhelpers/contrib/python/rpdb.py (+58/-0)
hooks/charmhelpers/contrib/python/version.py (+34/-0)
hooks/charmhelpers/contrib/saltstack/__init__.py (+118/-0)
hooks/charmhelpers/contrib/ssl/__init__.py (+94/-0)
hooks/charmhelpers/contrib/ssl/service.py (+283/-0)
hooks/charmhelpers/contrib/storage/__init__.py (+15/-0)
hooks/charmhelpers/contrib/storage/linux/__init__.py (+15/-0)
hooks/charmhelpers/contrib/storage/linux/ceph.py (+444/-0)
hooks/charmhelpers/contrib/storage/linux/loopback.py (+78/-0)
hooks/charmhelpers/contrib/storage/linux/lvm.py (+105/-0)
hooks/charmhelpers/contrib/storage/linux/utils.py (+70/-0)
hooks/charmhelpers/contrib/templating/__init__.py (+15/-0)
hooks/charmhelpers/contrib/templating/contexts.py (+134/-0)
hooks/charmhelpers/contrib/templating/jinja.py (+39/-0)
hooks/charmhelpers/contrib/templating/pyformat.py (+29/-0)
hooks/charmhelpers/contrib/unison/__init__.py (+297/-0)
hooks/charmhelpers/core/__init__.py (+15/-0)
hooks/charmhelpers/core/decorators.py (+57/-0)
hooks/charmhelpers/core/fstab.py (+134/-0)
hooks/charmhelpers/core/hookenv.py (+568/-0)
hooks/charmhelpers/core/host.py (+446/-0)
hooks/charmhelpers/core/services/__init__.py (+18/-0)
hooks/charmhelpers/core/services/base.py (+329/-0)
hooks/charmhelpers/core/services/helpers.py (+259/-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 (+439/-0)
hooks/charmhelpers/fetch/archiveurl.py (+161/-0)
hooks/charmhelpers/fetch/bzrurl.py (+78/-0)
hooks/charmhelpers/fetch/giturl.py (+71/-0)
hooks/charmhelpers/payload/__init__.py (+17/-0)
hooks/charmhelpers/payload/archive.py (+73/-0)
hooks/charmhelpers/payload/execd.py (+66/-0)
hooks/hooks.py (+3/-0)
hooks/services.py (+27/-0)
metadata.yaml (+7/-0)
To merge this branch: bzr merge lp:~pwlars/charms/wily/hwcert-pxe-server/init-pxe-server
Reviewer Review Type Date Requested Status
Paul Larson Approve
Review via email: mp+283880@code.launchpad.net

Description of the change

small charm to implement pxe/netboot server. It seems to work much better with wily, I had a lot of issues getting it working with trusty. This should hopefully give a shorter path to xenial too though, which we should definitely be looking to transition to.

To post a comment you must log in.
Revision history for this message
Paul Larson (pwlars) :
review: Approve

Preview Diff

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

Subscribers

People subscribed via source and target branches