Merge lp:~facundo/charms/trusty/tanuki-spec-manager/first-draft into lp:~tanuki/charms/trusty/tanuki-spec-manager/trunk

Proposed by Facundo Batista
Status: Merged
Merged at revision: 3
Proposed branch: lp:~facundo/charms/trusty/tanuki-spec-manager/first-draft
Merge into: lp:~tanuki/charms/trusty/tanuki-spec-manager/trunk
Diff against target: 11661 lines (+11245/-0)
74 files modified
README (+15/-0)
config.yaml (+10/-0)
hooks/actions.py (+94/-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 (+31/-0)
metadata.yaml (+9/-0)
templates/upstart.conf (+29/-0)
To merge this branch: bzr merge lp:~facundo/charms/trusty/tanuki-spec-manager/first-draft
Reviewer Review Type Date Requested Status
Celso Providelo (community) Approve
Facundo Batista (community) Approve
Review via email: mp+262345@code.launchpad.net

Commit message

First draft of the charm for Spec Manager.

Description of the change

First draft of the charm for Spec Manager.

To post a comment you must log in.
Revision history for this message
Celso Providelo (cprov) wrote :

Facundo,

The diff is truncated, but you can land as long as you stop trying to install the spec_manager module (it will fail, because it's not installed atm) and remove the service action for filling website relation forcibly (remove the wsgi-relation-joined symlink and create the correct website-relation* ones).

[]

review: Needs Fixing
2. By Facundo Batista

Better website relation management.

Revision history for this message
Facundo Batista (facundo) wrote :

Done! will land

review: Approve
3. By Facundo Batista

Proper cleaning and website relation handling.

Revision history for this message
Celso Providelo (cprov) wrote :

Nice, thanks.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'README'
--- README 1970-01-01 00:00:00 +0000
+++ README 2015-06-18 17:19:58 +0000
@@ -0,0 +1,15 @@
1Overview
2--------
3
4This charm provides the Spec Manager from lp:tanuki-spec-manager
5
6Configuration
7-------------
8
9There are only two config options for this charm:
10 * config-file: A base64 encoded string with the config file to use for
11 adt-cloud-service.
12 * environment: a string, one of "development" or "production"
13
14NOTE: This expects the payload code in the form of spec-manager.tgz
15to exist with the snapshot of lp:tanuki-spec-manager that needs deploying
016
=== added file 'config.yaml'
--- config.yaml 1970-01-01 00:00:00 +0000
+++ config.yaml 2015-06-18 17:19:58 +0000
@@ -0,0 +1,10 @@
1options:
2 environment:
3 type: string
4 default: "production"
5 description: |
6 Environment (devel, staging, production, etc.) that we're running.
7 config-file:
8 type: string
9 description: |
10 base64 encoded string with the config file for adt-cloud-worker
011
=== added directory 'files'
=== added directory 'hooks'
=== added file 'hooks/actions.py'
--- hooks/actions.py 1970-01-01 00:00:00 +0000
+++ hooks/actions.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,94 @@
1import base64
2import os
3import subprocess
4
5from charmhelpers import fetch
6from charmhelpers.core import (
7 hookenv,
8 host,
9)
10from charmhelpers.core.services import helpers
11from charmhelpers.payload import (archive, execd)
12
13
14REQUIRED_PACKAGES = [
15 'python-virtualenv', 'python3-dev',
16]
17
18WSGI_USER = 'www-data'
19WSGI_GROUP = 'www-data'
20
21config = hookenv.config()
22
23SERVICE_DIR = '/srv/{}/spec-manager'.format(config['environment'])
24LOG_DIR = os.path.join(SERVICE_DIR, 'logs')
25
26
27def log_start(service_name):
28 hookenv.log('spec-manager starting')
29
30
31def basenode(service_name):
32 hookenv.log("Executing basenode")
33 execd.execd_preinstall()
34
35
36def install_packages(service_name):
37 hookenv.log('Installing dependencies...')
38 fetch.configure_sources(update=True)
39 fetch.apt_install(REQUIRED_PACKAGES, fatal=True)
40
41
42def get_cloud_service_from_tarball(service_name):
43 files_dir = os.path.join(hookenv.charm_dir(), 'files')
44 tarball = os.path.join(files_dir, 'spec-manager.tgz')
45 if not os.path.exists(SERVICE_DIR):
46 hookenv.log('Installing the code for the first time from tarball')
47 archive.extract_tarfile(tarball, os.path.dirname(SERVICE_DIR))
48
49
50def get_config_file(service_name):
51 config_file = config.get('config-file')
52 if config_file is not None:
53 with open(os.path.join(SERVICE_DIR, 'spec-manager.conf'), 'w') as f:
54 f.write(base64.b64decode(config_file))
55
56
57def install_python_packages(service_name):
58 env_dir = os.path.join(SERVICE_DIR, 'env')
59 if os.path.exists(env_dir):
60 hookenv.log('Service venv already exists, nothing to do ...')
61 return
62
63 hookenv.log('Installing python packages into the venv ...')
64 subprocess.check_call(['virtualenv', '-p', 'python3', env_dir])
65
66 pip_cache = os.path.join(hookenv.charm_dir(), 'files', 'pip-cache')
67 requirements = os.path.join(SERVICE_DIR, 'requirements_run.txt')
68
69 hookenv.log('Installing from download cache.')
70 subprocess.check_call(['%s/bin/pip' % env_dir,
71 'install',
72 '--no-index',
73 '--find-links={}'.format(pip_cache),
74 '-r', requirements])
75
76 subprocess.check_call(['%s/bin/pip' % env_dir,
77 'install', '--no-deps', '-e', SERVICE_DIR])
78
79
80def prepare_for_wsgi(service_name):
81 host.adduser(WSGI_USER)
82 host.add_group(WSGI_GROUP)
83 host.add_user_to_group(WSGI_USER, WSGI_GROUP)
84 if not os.path.exists(LOG_DIR):
85 host.mkdir(LOG_DIR, WSGI_USER, WSGI_GROUP, perms=0o755)
86
87
88class WebsiteRelation(helpers.HttpRelation):
89
90 def provide_data(self):
91 hookenv.log('Providing Website relation data.')
92 data = super(WebsiteRelation, self).provide_data()
93 data['port'] = 8000
94 return data
095
=== added directory 'hooks/charmhelpers'
=== added file 'hooks/charmhelpers/__init__.py'
--- hooks/charmhelpers/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/__init__.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,38 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17# Bootstrap charm-helpers, installing its dependencies if necessary using
18# only standard libraries.
19import subprocess
20import sys
21
22try:
23 import six # flake8: noqa
24except ImportError:
25 if sys.version_info.major == 2:
26 subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
27 else:
28 subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
29 import six # flake8: noqa
30
31try:
32 import yaml # flake8: noqa
33except ImportError:
34 if sys.version_info.major == 2:
35 subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
36 else:
37 subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
38 import yaml # flake8: noqa
039
=== added directory 'hooks/charmhelpers/contrib'
=== added file 'hooks/charmhelpers/contrib/__init__.py'
--- hooks/charmhelpers/contrib/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/__init__.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
016
=== added directory 'hooks/charmhelpers/contrib/amulet'
=== added file 'hooks/charmhelpers/contrib/amulet/__init__.py'
--- hooks/charmhelpers/contrib/amulet/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/amulet/__init__.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
016
=== added file 'hooks/charmhelpers/contrib/amulet/deployment.py'
--- hooks/charmhelpers/contrib/amulet/deployment.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/amulet/deployment.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,93 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import amulet
18import os
19import six
20
21
22class AmuletDeployment(object):
23 """Amulet deployment.
24
25 This class provides generic Amulet deployment and test runner
26 methods.
27 """
28
29 def __init__(self, series=None):
30 """Initialize the deployment environment."""
31 self.series = None
32
33 if series:
34 self.series = series
35 self.d = amulet.Deployment(series=self.series)
36 else:
37 self.d = amulet.Deployment()
38
39 def _add_services(self, this_service, other_services):
40 """Add services.
41
42 Add services to the deployment where this_service is the local charm
43 that we're testing and other_services are the other services that
44 are being used in the local amulet tests.
45 """
46 if this_service['name'] != os.path.basename(os.getcwd()):
47 s = this_service['name']
48 msg = "The charm's root directory name needs to be {}".format(s)
49 amulet.raise_status(amulet.FAIL, msg=msg)
50
51 if 'units' not in this_service:
52 this_service['units'] = 1
53
54 self.d.add(this_service['name'], units=this_service['units'])
55
56 for svc in other_services:
57 if 'location' in svc:
58 branch_location = svc['location']
59 elif self.series:
60 branch_location = 'cs:{}/{}'.format(self.series, svc['name']),
61 else:
62 branch_location = None
63
64 if 'units' not in svc:
65 svc['units'] = 1
66
67 self.d.add(svc['name'], charm=branch_location, units=svc['units'])
68
69 def _add_relations(self, relations):
70 """Add all of the relations for the services."""
71 for k, v in six.iteritems(relations):
72 self.d.relate(k, v)
73
74 def _configure_services(self, configs):
75 """Configure all of the services."""
76 for service, config in six.iteritems(configs):
77 self.d.configure(service, config)
78
79 def _deploy(self):
80 """Deploy environment and wait for all hooks to finish executing."""
81 try:
82 self.d.setup(timeout=900)
83 self.d.sentry.wait(timeout=900)
84 except amulet.helpers.TimeoutError:
85 amulet.raise_status(amulet.FAIL, msg="Deployment timed out")
86 except Exception:
87 raise
88
89 def run_tests(self):
90 """Run all of the methods that are prefixed with 'test_'."""
91 for test in dir(self):
92 if test.startswith('test_'):
93 getattr(self, test)()
094
=== added file 'hooks/charmhelpers/contrib/amulet/utils.py'
--- hooks/charmhelpers/contrib/amulet/utils.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/amulet/utils.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,194 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import ConfigParser
18import io
19import logging
20import re
21import sys
22import time
23
24import six
25
26
27class AmuletUtils(object):
28 """Amulet utilities.
29
30 This class provides common utility functions that are used by Amulet
31 tests.
32 """
33
34 def __init__(self, log_level=logging.ERROR):
35 self.log = self.get_logger(level=log_level)
36
37 def get_logger(self, name="amulet-logger", level=logging.DEBUG):
38 """Get a logger object that will log to stdout."""
39 log = logging
40 logger = log.getLogger(name)
41 fmt = log.Formatter("%(asctime)s %(funcName)s "
42 "%(levelname)s: %(message)s")
43
44 handler = log.StreamHandler(stream=sys.stdout)
45 handler.setLevel(level)
46 handler.setFormatter(fmt)
47
48 logger.addHandler(handler)
49 logger.setLevel(level)
50
51 return logger
52
53 def valid_ip(self, ip):
54 if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip):
55 return True
56 else:
57 return False
58
59 def valid_url(self, url):
60 p = re.compile(
61 r'^(?:http|ftp)s?://'
62 r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # noqa
63 r'localhost|'
64 r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
65 r'(?::\d+)?'
66 r'(?:/?|[/?]\S+)$',
67 re.IGNORECASE)
68 if p.match(url):
69 return True
70 else:
71 return False
72
73 def validate_services(self, commands):
74 """Validate services.
75
76 Verify the specified services are running on the corresponding
77 service units.
78 """
79 for k, v in six.iteritems(commands):
80 for cmd in v:
81 output, code = k.run(cmd)
82 if code != 0:
83 return "command `{}` returned {}".format(cmd, str(code))
84 return None
85
86 def _get_config(self, unit, filename):
87 """Get a ConfigParser object for parsing a unit's config file."""
88 file_contents = unit.file_contents(filename)
89 config = ConfigParser.ConfigParser()
90 config.readfp(io.StringIO(file_contents))
91 return config
92
93 def validate_config_data(self, sentry_unit, config_file, section,
94 expected):
95 """Validate config file data.
96
97 Verify that the specified section of the config file contains
98 the expected option key:value pairs.
99 """
100 config = self._get_config(sentry_unit, config_file)
101
102 if section != 'DEFAULT' and not config.has_section(section):
103 return "section [{}] does not exist".format(section)
104
105 for k in expected.keys():
106 if not config.has_option(section, k):
107 return "section [{}] is missing option {}".format(section, k)
108 if config.get(section, k) != expected[k]:
109 return "section [{}] {}:{} != expected {}:{}".format(
110 section, k, config.get(section, k), k, expected[k])
111 return None
112
113 def _validate_dict_data(self, expected, actual):
114 """Validate dictionary data.
115
116 Compare expected dictionary data vs actual dictionary data.
117 The values in the 'expected' dictionary can be strings, bools, ints,
118 longs, or can be a function that evaluate a variable and returns a
119 bool.
120 """
121 for k, v in six.iteritems(expected):
122 if k in actual:
123 if (isinstance(v, six.string_types) or
124 isinstance(v, bool) or
125 isinstance(v, six.integer_types)):
126 if v != actual[k]:
127 return "{}:{}".format(k, actual[k])
128 elif not v(actual[k]):
129 return "{}:{}".format(k, actual[k])
130 else:
131 return "key '{}' does not exist".format(k)
132 return None
133
134 def validate_relation_data(self, sentry_unit, relation, expected):
135 """Validate actual relation data based on expected relation data."""
136 actual = sentry_unit.relation(relation[0], relation[1])
137 self.log.debug('actual: {}'.format(repr(actual)))
138 return self._validate_dict_data(expected, actual)
139
140 def _validate_list_data(self, expected, actual):
141 """Compare expected list vs actual list data."""
142 for e in expected:
143 if e not in actual:
144 return "expected item {} not found in actual list".format(e)
145 return None
146
147 def not_null(self, string):
148 if string is not None:
149 return True
150 else:
151 return False
152
153 def _get_file_mtime(self, sentry_unit, filename):
154 """Get last modification time of file."""
155 return sentry_unit.file_stat(filename)['mtime']
156
157 def _get_dir_mtime(self, sentry_unit, directory):
158 """Get last modification time of directory."""
159 return sentry_unit.directory_stat(directory)['mtime']
160
161 def _get_proc_start_time(self, sentry_unit, service, pgrep_full=False):
162 """Get process' start time.
163
164 Determine start time of the process based on the last modification
165 time of the /proc/pid directory. If pgrep_full is True, the process
166 name is matched against the full command line.
167 """
168 if pgrep_full:
169 cmd = 'pgrep -o -f {}'.format(service)
170 else:
171 cmd = 'pgrep -o {}'.format(service)
172 proc_dir = '/proc/{}'.format(sentry_unit.run(cmd)[0].strip())
173 return self._get_dir_mtime(sentry_unit, proc_dir)
174
175 def service_restarted(self, sentry_unit, service, filename,
176 pgrep_full=False, sleep_time=20):
177 """Check if service was restarted.
178
179 Compare a service's start time vs a file's last modification time
180 (such as a config file for that service) to determine if the service
181 has been restarted.
182 """
183 time.sleep(sleep_time)
184 if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=
185 self._get_file_mtime(sentry_unit, filename)):
186 return True
187 else:
188 return False
189
190 def relation_error(self, name, data):
191 return 'unexpected relation data in {} - {}'.format(name, data)
192
193 def endpoint_error(self, name, data):
194 return 'unexpected endpoint data in {} - {}'.format(name, data)
0195
=== added directory 'hooks/charmhelpers/contrib/ansible'
=== added file 'hooks/charmhelpers/contrib/ansible/__init__.py'
--- hooks/charmhelpers/contrib/ansible/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/ansible/__init__.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,190 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17# Copyright 2013 Canonical Ltd.
18#
19# Authors:
20# Charm Helpers Developers <juju@lists.ubuntu.com>
21"""Charm Helpers ansible - declare the state of your machines.
22
23This helper enables you to declare your machine state, rather than
24program it procedurally (and have to test each change to your procedures).
25Your install hook can be as simple as::
26
27 {{{
28 import charmhelpers.contrib.ansible
29
30
31 def install():
32 charmhelpers.contrib.ansible.install_ansible_support()
33 charmhelpers.contrib.ansible.apply_playbook('playbooks/install.yaml')
34 }}}
35
36and won't need to change (nor will its tests) when you change the machine
37state.
38
39All of your juju config and relation-data are available as template
40variables within your playbooks and templates. An install playbook looks
41something like::
42
43 {{{
44 ---
45 - hosts: localhost
46 user: root
47
48 tasks:
49 - name: Add private repositories.
50 template:
51 src: ../templates/private-repositories.list.jinja2
52 dest: /etc/apt/sources.list.d/private.list
53
54 - name: Update the cache.
55 apt: update_cache=yes
56
57 - name: Install dependencies.
58 apt: pkg={{ item }}
59 with_items:
60 - python-mimeparse
61 - python-webob
62 - sunburnt
63
64 - name: Setup groups.
65 group: name={{ item.name }} gid={{ item.gid }}
66 with_items:
67 - { name: 'deploy_user', gid: 1800 }
68 - { name: 'service_user', gid: 1500 }
69
70 ...
71 }}}
72
73Read more online about `playbooks`_ and standard ansible `modules`_.
74
75.. _playbooks: http://www.ansibleworks.com/docs/playbooks.html
76.. _modules: http://www.ansibleworks.com/docs/modules.html
77
78"""
79import os
80import subprocess
81
82import charmhelpers.contrib.templating.contexts
83import charmhelpers.core.host
84import charmhelpers.core.hookenv
85import charmhelpers.fetch
86
87
88charm_dir = os.environ.get('CHARM_DIR', '')
89ansible_hosts_path = '/etc/ansible/hosts'
90# Ansible will automatically include any vars in the following
91# file in its inventory when run locally.
92ansible_vars_path = '/etc/ansible/host_vars/localhost'
93
94
95def install_ansible_support(from_ppa=True, ppa_location='ppa:rquillo/ansible'):
96 """Installs the ansible package.
97
98 By default it is installed from the `PPA`_ linked from
99 the ansible `website`_ or from a ppa specified by a charm config..
100
101 .. _PPA: https://launchpad.net/~rquillo/+archive/ansible
102 .. _website: http://docs.ansible.com/intro_installation.html#latest-releases-via-apt-ubuntu
103
104 If from_ppa is empty, you must ensure that the package is available
105 from a configured repository.
106 """
107 if from_ppa:
108 charmhelpers.fetch.add_source(ppa_location)
109 charmhelpers.fetch.apt_update(fatal=True)
110 charmhelpers.fetch.apt_install('ansible')
111 with open(ansible_hosts_path, 'w+') as hosts_file:
112 hosts_file.write('localhost ansible_connection=local')
113
114
115def apply_playbook(playbook, tags=None):
116 tags = tags or []
117 tags = ",".join(tags)
118 charmhelpers.contrib.templating.contexts.juju_state_to_yaml(
119 ansible_vars_path, namespace_separator='__',
120 allow_hyphens_in_keys=False)
121 # we want ansible's log output to be unbuffered
122 env = os.environ.copy()
123 env['PYTHONUNBUFFERED'] = "1"
124 call = [
125 'ansible-playbook',
126 '-c',
127 'local',
128 playbook,
129 ]
130 if tags:
131 call.extend(['--tags', '{}'.format(tags)])
132 subprocess.check_call(call, env=env)
133
134
135class AnsibleHooks(charmhelpers.core.hookenv.Hooks):
136 """Run a playbook with the hook-name as the tag.
137
138 This helper builds on the standard hookenv.Hooks helper,
139 but additionally runs the playbook with the hook-name specified
140 using --tags (ie. running all the tasks tagged with the hook-name).
141
142 Example::
143
144 hooks = AnsibleHooks(playbook_path='playbooks/my_machine_state.yaml')
145
146 # All the tasks within my_machine_state.yaml tagged with 'install'
147 # will be run automatically after do_custom_work()
148 @hooks.hook()
149 def install():
150 do_custom_work()
151
152 # For most of your hooks, you won't need to do anything other
153 # than run the tagged tasks for the hook:
154 @hooks.hook('config-changed', 'start', 'stop')
155 def just_use_playbook():
156 pass
157
158 # As a convenience, you can avoid the above noop function by specifying
159 # the hooks which are handled by ansible-only and they'll be registered
160 # for you:
161 # hooks = AnsibleHooks(
162 # 'playbooks/my_machine_state.yaml',
163 # default_hooks=['config-changed', 'start', 'stop'])
164
165 if __name__ == "__main__":
166 # execute a hook based on the name the program is called by
167 hooks.execute(sys.argv)
168
169 """
170
171 def __init__(self, playbook_path, default_hooks=None):
172 """Register any hooks handled by ansible."""
173 super(AnsibleHooks, self).__init__()
174
175 self.playbook_path = playbook_path
176
177 default_hooks = default_hooks or []
178
179 def noop(*args, **kwargs):
180 pass
181
182 for hook in default_hooks:
183 self.register(hook, noop)
184
185 def execute(self, args):
186 """Execute the hook followed by the playbook using the hook as tag."""
187 super(AnsibleHooks, self).execute(args)
188 hook_name = os.path.basename(args[0])
189 charmhelpers.contrib.ansible.apply_playbook(
190 self.playbook_path, tags=[hook_name])
0191
=== added directory 'hooks/charmhelpers/contrib/charmhelpers'
=== added file 'hooks/charmhelpers/contrib/charmhelpers/__init__.py'
--- hooks/charmhelpers/contrib/charmhelpers/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/charmhelpers/__init__.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,208 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17# Copyright 2012 Canonical Ltd. This software is licensed under the
18# GNU Affero General Public License version 3 (see the file LICENSE).
19
20import warnings
21warnings.warn("contrib.charmhelpers is deprecated", DeprecationWarning) # noqa
22
23import operator
24import tempfile
25import time
26import yaml
27import subprocess
28
29import six
30if six.PY3:
31 from urllib.request import urlopen
32 from urllib.error import (HTTPError, URLError)
33else:
34 from urllib2 import (urlopen, HTTPError, URLError)
35
36"""Helper functions for writing Juju charms in Python."""
37
38__metaclass__ = type
39__all__ = [
40 # 'get_config', # core.hookenv.config()
41 # 'log', # core.hookenv.log()
42 # 'log_entry', # core.hookenv.log()
43 # 'log_exit', # core.hookenv.log()
44 # 'relation_get', # core.hookenv.relation_get()
45 # 'relation_set', # core.hookenv.relation_set()
46 # 'relation_ids', # core.hookenv.relation_ids()
47 # 'relation_list', # core.hookenv.relation_units()
48 # 'config_get', # core.hookenv.config()
49 # 'unit_get', # core.hookenv.unit_get()
50 # 'open_port', # core.hookenv.open_port()
51 # 'close_port', # core.hookenv.close_port()
52 # 'service_control', # core.host.service()
53 'unit_info', # client-side, NOT IMPLEMENTED
54 'wait_for_machine', # client-side, NOT IMPLEMENTED
55 'wait_for_page_contents', # client-side, NOT IMPLEMENTED
56 'wait_for_relation', # client-side, NOT IMPLEMENTED
57 'wait_for_unit', # client-side, NOT IMPLEMENTED
58]
59
60
61SLEEP_AMOUNT = 0.1
62
63
64# We create a juju_status Command here because it makes testing much,
65# much easier.
66def juju_status():
67 subprocess.check_call(['juju', 'status'])
68
69# re-implemented as charmhelpers.fetch.configure_sources()
70# def configure_source(update=False):
71# source = config_get('source')
72# if ((source.startswith('ppa:') or
73# source.startswith('cloud:') or
74# source.startswith('http:'))):
75# run('add-apt-repository', source)
76# if source.startswith("http:"):
77# run('apt-key', 'import', config_get('key'))
78# if update:
79# run('apt-get', 'update')
80
81
82# DEPRECATED: client-side only
83def make_charm_config_file(charm_config):
84 charm_config_file = tempfile.NamedTemporaryFile(mode='w+')
85 charm_config_file.write(yaml.dump(charm_config))
86 charm_config_file.flush()
87 # The NamedTemporaryFile instance is returned instead of just the name
88 # because we want to take advantage of garbage collection-triggered
89 # deletion of the temp file when it goes out of scope in the caller.
90 return charm_config_file
91
92
93# DEPRECATED: client-side only
94def unit_info(service_name, item_name, data=None, unit=None):
95 if data is None:
96 data = yaml.safe_load(juju_status())
97 service = data['services'].get(service_name)
98 if service is None:
99 # XXX 2012-02-08 gmb:
100 # This allows us to cope with the race condition that we
101 # have between deploying a service and having it come up in
102 # `juju status`. We could probably do with cleaning it up so
103 # that it fails a bit more noisily after a while.
104 return ''
105 units = service['units']
106 if unit is not None:
107 item = units[unit][item_name]
108 else:
109 # It might seem odd to sort the units here, but we do it to
110 # ensure that when no unit is specified, the first unit for the
111 # service (or at least the one with the lowest number) is the
112 # one whose data gets returned.
113 sorted_unit_names = sorted(units.keys())
114 item = units[sorted_unit_names[0]][item_name]
115 return item
116
117
118# DEPRECATED: client-side only
119def get_machine_data():
120 return yaml.safe_load(juju_status())['machines']
121
122
123# DEPRECATED: client-side only
124def wait_for_machine(num_machines=1, timeout=300):
125 """Wait `timeout` seconds for `num_machines` machines to come up.
126
127 This wait_for... function can be called by other wait_for functions
128 whose timeouts might be too short in situations where only a bare
129 Juju setup has been bootstrapped.
130
131 :return: A tuple of (num_machines, time_taken). This is used for
132 testing.
133 """
134 # You may think this is a hack, and you'd be right. The easiest way
135 # to tell what environment we're working in (LXC vs EC2) is to check
136 # the dns-name of the first machine. If it's localhost we're in LXC
137 # and we can just return here.
138 if get_machine_data()[0]['dns-name'] == 'localhost':
139 return 1, 0
140 start_time = time.time()
141 while True:
142 # Drop the first machine, since it's the Zookeeper and that's
143 # not a machine that we need to wait for. This will only work
144 # for EC2 environments, which is why we return early above if
145 # we're in LXC.
146 machine_data = get_machine_data()
147 non_zookeeper_machines = [
148 machine_data[key] for key in list(machine_data.keys())[1:]]
149 if len(non_zookeeper_machines) >= num_machines:
150 all_machines_running = True
151 for machine in non_zookeeper_machines:
152 if machine.get('instance-state') != 'running':
153 all_machines_running = False
154 break
155 if all_machines_running:
156 break
157 if time.time() - start_time >= timeout:
158 raise RuntimeError('timeout waiting for service to start')
159 time.sleep(SLEEP_AMOUNT)
160 return num_machines, time.time() - start_time
161
162
163# DEPRECATED: client-side only
164def wait_for_unit(service_name, timeout=480):
165 """Wait `timeout` seconds for a given service name to come up."""
166 wait_for_machine(num_machines=1)
167 start_time = time.time()
168 while True:
169 state = unit_info(service_name, 'agent-state')
170 if 'error' in state or state == 'started':
171 break
172 if time.time() - start_time >= timeout:
173 raise RuntimeError('timeout waiting for service to start')
174 time.sleep(SLEEP_AMOUNT)
175 if state != 'started':
176 raise RuntimeError('unit did not start, agent-state: ' + state)
177
178
179# DEPRECATED: client-side only
180def wait_for_relation(service_name, relation_name, timeout=120):
181 """Wait `timeout` seconds for a given relation to come up."""
182 start_time = time.time()
183 while True:
184 relation = unit_info(service_name, 'relations').get(relation_name)
185 if relation is not None and relation['state'] == 'up':
186 break
187 if time.time() - start_time >= timeout:
188 raise RuntimeError('timeout waiting for relation to be up')
189 time.sleep(SLEEP_AMOUNT)
190
191
192# DEPRECATED: client-side only
193def wait_for_page_contents(url, contents, timeout=120, validate=None):
194 if validate is None:
195 validate = operator.contains
196 start_time = time.time()
197 while True:
198 try:
199 stream = urlopen(url)
200 except (HTTPError, URLError):
201 pass
202 else:
203 page = stream.read()
204 if validate(page, contents):
205 return page
206 if time.time() - start_time >= timeout:
207 raise RuntimeError('timeout waiting for contents of ' + url)
208 time.sleep(SLEEP_AMOUNT)
0209
=== added directory 'hooks/charmhelpers/contrib/charmsupport'
=== added file 'hooks/charmhelpers/contrib/charmsupport/__init__.py'
--- hooks/charmhelpers/contrib/charmsupport/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/charmsupport/__init__.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
016
=== added file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py'
--- hooks/charmhelpers/contrib/charmsupport/nrpe.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,324 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17"""Compatibility with the nrpe-external-master charm"""
18# Copyright 2012 Canonical Ltd.
19#
20# Authors:
21# Matthew Wedgwood <matthew.wedgwood@canonical.com>
22
23import subprocess
24import pwd
25import grp
26import os
27import re
28import shlex
29import yaml
30
31from charmhelpers.core.hookenv import (
32 config,
33 local_unit,
34 log,
35 relation_ids,
36 relation_set,
37 relations_of_type,
38)
39
40from charmhelpers.core.host import service
41
42# This module adds compatibility with the nrpe-external-master and plain nrpe
43# subordinate charms. To use it in your charm:
44#
45# 1. Update metadata.yaml
46#
47# provides:
48# (...)
49# nrpe-external-master:
50# interface: nrpe-external-master
51# scope: container
52#
53# and/or
54#
55# provides:
56# (...)
57# local-monitors:
58# interface: local-monitors
59# scope: container
60
61#
62# 2. Add the following to config.yaml
63#
64# nagios_context:
65# default: "juju"
66# type: string
67# description: |
68# Used by the nrpe subordinate charms.
69# A string that will be prepended to instance name to set the host name
70# in nagios. So for instance the hostname would be something like:
71# juju-myservice-0
72# If you're running multiple environments with the same services in them
73# this allows you to differentiate between them.
74# nagios_servicegroups:
75# default: ""
76# type: string
77# description: |
78# A comma-separated list of nagios servicegroups.
79# If left empty, the nagios_context will be used as the servicegroup
80#
81# 3. Add custom checks (Nagios plugins) to files/nrpe-external-master
82#
83# 4. Update your hooks.py with something like this:
84#
85# from charmsupport.nrpe import NRPE
86# (...)
87# def update_nrpe_config():
88# nrpe_compat = NRPE()
89# nrpe_compat.add_check(
90# shortname = "myservice",
91# description = "Check MyService",
92# check_cmd = "check_http -w 2 -c 10 http://localhost"
93# )
94# nrpe_compat.add_check(
95# "myservice_other",
96# "Check for widget failures",
97# check_cmd = "/srv/myapp/scripts/widget_check"
98# )
99# nrpe_compat.write()
100#
101# def config_changed():
102# (...)
103# update_nrpe_config()
104#
105# def nrpe_external_master_relation_changed():
106# update_nrpe_config()
107#
108# def local_monitors_relation_changed():
109# update_nrpe_config()
110#
111# 5. ln -s hooks.py nrpe-external-master-relation-changed
112# ln -s hooks.py local-monitors-relation-changed
113
114
115class CheckException(Exception):
116 pass
117
118
119class Check(object):
120 shortname_re = '[A-Za-z0-9-_]+$'
121 service_template = ("""
122#---------------------------------------------------
123# This file is Juju managed
124#---------------------------------------------------
125define service {{
126 use active-service
127 host_name {nagios_hostname}
128 service_description {nagios_hostname}[{shortname}] """
129 """{description}
130 check_command check_nrpe!{command}
131 servicegroups {nagios_servicegroup}
132}}
133""")
134
135 def __init__(self, shortname, description, check_cmd):
136 super(Check, self).__init__()
137 # XXX: could be better to calculate this from the service name
138 if not re.match(self.shortname_re, shortname):
139 raise CheckException("shortname must match {}".format(
140 Check.shortname_re))
141 self.shortname = shortname
142 self.command = "check_{}".format(shortname)
143 # Note: a set of invalid characters is defined by the
144 # Nagios server config
145 # The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()=
146 self.description = description
147 self.check_cmd = self._locate_cmd(check_cmd)
148
149 def _locate_cmd(self, check_cmd):
150 search_path = (
151 '/usr/lib/nagios/plugins',
152 '/usr/local/lib/nagios/plugins',
153 )
154 parts = shlex.split(check_cmd)
155 for path in search_path:
156 if os.path.exists(os.path.join(path, parts[0])):
157 command = os.path.join(path, parts[0])
158 if len(parts) > 1:
159 command += " " + " ".join(parts[1:])
160 return command
161 log('Check command not found: {}'.format(parts[0]))
162 return ''
163
164 def write(self, nagios_context, hostname, nagios_servicegroups=None):
165 nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format(
166 self.command)
167 with open(nrpe_check_file, 'w') as nrpe_check_config:
168 nrpe_check_config.write("# check {}\n".format(self.shortname))
169 nrpe_check_config.write("command[{}]={}\n".format(
170 self.command, self.check_cmd))
171
172 if not os.path.exists(NRPE.nagios_exportdir):
173 log('Not writing service config as {} is not accessible'.format(
174 NRPE.nagios_exportdir))
175 else:
176 self.write_service_config(nagios_context, hostname,
177 nagios_servicegroups)
178
179 def write_service_config(self, nagios_context, hostname,
180 nagios_servicegroups=None):
181 for f in os.listdir(NRPE.nagios_exportdir):
182 if re.search('.*{}.cfg'.format(self.command), f):
183 os.remove(os.path.join(NRPE.nagios_exportdir, f))
184
185 if not nagios_servicegroups:
186 nagios_servicegroups = nagios_context
187
188 templ_vars = {
189 'nagios_hostname': hostname,
190 'nagios_servicegroup': nagios_servicegroups,
191 'description': self.description,
192 'shortname': self.shortname,
193 'command': self.command,
194 }
195 nrpe_service_text = Check.service_template.format(**templ_vars)
196 nrpe_service_file = '{}/service__{}_{}.cfg'.format(
197 NRPE.nagios_exportdir, hostname, self.command)
198 with open(nrpe_service_file, 'w') as nrpe_service_config:
199 nrpe_service_config.write(str(nrpe_service_text))
200
201 def run(self):
202 subprocess.call(self.check_cmd)
203
204
205class NRPE(object):
206 nagios_logdir = '/var/log/nagios'
207 nagios_exportdir = '/var/lib/nagios/export'
208 nrpe_confdir = '/etc/nagios/nrpe.d'
209
210 def __init__(self, hostname=None):
211 super(NRPE, self).__init__()
212 self.config = config()
213 self.nagios_context = self.config['nagios_context']
214 if 'nagios_servicegroups' in self.config:
215 self.nagios_servicegroups = self.config['nagios_servicegroups']
216 else:
217 self.nagios_servicegroups = 'juju'
218 self.unit_name = local_unit().replace('/', '-')
219 if hostname:
220 self.hostname = hostname
221 else:
222 self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
223 self.checks = []
224
225 def add_check(self, *args, **kwargs):
226 self.checks.append(Check(*args, **kwargs))
227
228 def write(self):
229 try:
230 nagios_uid = pwd.getpwnam('nagios').pw_uid
231 nagios_gid = grp.getgrnam('nagios').gr_gid
232 except:
233 log("Nagios user not set up, nrpe checks not updated")
234 return
235
236 if not os.path.exists(NRPE.nagios_logdir):
237 os.mkdir(NRPE.nagios_logdir)
238 os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid)
239
240 nrpe_monitors = {}
241 monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}}
242 for nrpecheck in self.checks:
243 nrpecheck.write(self.nagios_context, self.hostname,
244 self.nagios_servicegroups)
245 nrpe_monitors[nrpecheck.shortname] = {
246 "command": nrpecheck.command,
247 }
248
249 service('restart', 'nagios-nrpe-server')
250
251 for rid in relation_ids("local-monitors"):
252 relation_set(relation_id=rid, monitors=yaml.dump(monitors))
253
254
255def get_nagios_hostcontext(relation_name='nrpe-external-master'):
256 """
257 Query relation with nrpe subordinate, return the nagios_host_context
258
259 :param str relation_name: Name of relation nrpe sub joined to
260 """
261 for rel in relations_of_type(relation_name):
262 if 'nagios_hostname' in rel:
263 return rel['nagios_host_context']
264
265
266def get_nagios_hostname(relation_name='nrpe-external-master'):
267 """
268 Query relation with nrpe subordinate, return the nagios_hostname
269
270 :param str relation_name: Name of relation nrpe sub joined to
271 """
272 for rel in relations_of_type(relation_name):
273 if 'nagios_hostname' in rel:
274 return rel['nagios_hostname']
275
276
277def get_nagios_unit_name(relation_name='nrpe-external-master'):
278 """
279 Return the nagios unit name prepended with host_context if needed
280
281 :param str relation_name: Name of relation nrpe sub joined to
282 """
283 host_context = get_nagios_hostcontext(relation_name)
284 if host_context:
285 unit = "%s:%s" % (host_context, local_unit())
286 else:
287 unit = local_unit()
288 return unit
289
290
291def add_init_service_checks(nrpe, services, unit_name):
292 """
293 Add checks for each service in list
294
295 :param NRPE nrpe: NRPE object to add check to
296 :param list services: List of services to check
297 :param str unit_name: Unit name to use in check description
298 """
299 for svc in services:
300 upstart_init = '/etc/init/%s.conf' % svc
301 sysv_init = '/etc/init.d/%s' % svc
302 if os.path.exists(upstart_init):
303 nrpe.add_check(
304 shortname=svc,
305 description='process check {%s}' % unit_name,
306 check_cmd='check_upstart_job %s' % svc
307 )
308 elif os.path.exists(sysv_init):
309 cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
310 cron_file = ('*/5 * * * * root '
311 '/usr/local/lib/nagios/plugins/check_exit_status.pl '
312 '-s /etc/init.d/%s status > '
313 '/var/lib/nagios/service-check-%s.txt\n' % (svc,
314 svc)
315 )
316 f = open(cronpath, 'w')
317 f.write(cron_file)
318 f.close()
319 nrpe.add_check(
320 shortname=svc,
321 description='process check {%s}' % unit_name,
322 check_cmd='check_status_file.py -f '
323 '/var/lib/nagios/service-check-%s.txt' % svc,
324 )
0325
=== added file 'hooks/charmhelpers/contrib/charmsupport/volumes.py'
--- hooks/charmhelpers/contrib/charmsupport/volumes.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/charmsupport/volumes.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,175 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17'''
18Functions for managing volumes in juju units. One volume is supported per unit.
19Subordinates may have their own storage, provided it is on its own partition.
20
21Configuration stanzas::
22
23 volume-ephemeral:
24 type: boolean
25 default: true
26 description: >
27 If false, a volume is mounted as sepecified in "volume-map"
28 If true, ephemeral storage will be used, meaning that log data
29 will only exist as long as the machine. YOU HAVE BEEN WARNED.
30 volume-map:
31 type: string
32 default: {}
33 description: >
34 YAML map of units to device names, e.g:
35 "{ rsyslog/0: /dev/vdb, rsyslog/1: /dev/vdb }"
36 Service units will raise a configure-error if volume-ephemeral
37 is 'true' and no volume-map value is set. Use 'juju set' to set a
38 value and 'juju resolved' to complete configuration.
39
40Usage::
41
42 from charmsupport.volumes import configure_volume, VolumeConfigurationError
43 from charmsupport.hookenv import log, ERROR
44 def post_mount_hook():
45 stop_service('myservice')
46 def post_mount_hook():
47 start_service('myservice')
48
49 if __name__ == '__main__':
50 try:
51 configure_volume(before_change=pre_mount_hook,
52 after_change=post_mount_hook)
53 except VolumeConfigurationError:
54 log('Storage could not be configured', ERROR)
55
56'''
57
58# XXX: Known limitations
59# - fstab is neither consulted nor updated
60
61import os
62from charmhelpers.core import hookenv
63from charmhelpers.core import host
64import yaml
65
66
67MOUNT_BASE = '/srv/juju/volumes'
68
69
70class VolumeConfigurationError(Exception):
71 '''Volume configuration data is missing or invalid'''
72 pass
73
74
75def get_config():
76 '''Gather and sanity-check volume configuration data'''
77 volume_config = {}
78 config = hookenv.config()
79
80 errors = False
81
82 if config.get('volume-ephemeral') in (True, 'True', 'true', 'Yes', 'yes'):
83 volume_config['ephemeral'] = True
84 else:
85 volume_config['ephemeral'] = False
86
87 try:
88 volume_map = yaml.safe_load(config.get('volume-map', '{}'))
89 except yaml.YAMLError as e:
90 hookenv.log("Error parsing YAML volume-map: {}".format(e),
91 hookenv.ERROR)
92 errors = True
93 if volume_map is None:
94 # probably an empty string
95 volume_map = {}
96 elif not isinstance(volume_map, dict):
97 hookenv.log("Volume-map should be a dictionary, not {}".format(
98 type(volume_map)))
99 errors = True
100
101 volume_config['device'] = volume_map.get(os.environ['JUJU_UNIT_NAME'])
102 if volume_config['device'] and volume_config['ephemeral']:
103 # asked for ephemeral storage but also defined a volume ID
104 hookenv.log('A volume is defined for this unit, but ephemeral '
105 'storage was requested', hookenv.ERROR)
106 errors = True
107 elif not volume_config['device'] and not volume_config['ephemeral']:
108 # asked for permanent storage but did not define volume ID
109 hookenv.log('Ephemeral storage was requested, but there is no volume '
110 'defined for this unit.', hookenv.ERROR)
111 errors = True
112
113 unit_mount_name = hookenv.local_unit().replace('/', '-')
114 volume_config['mountpoint'] = os.path.join(MOUNT_BASE, unit_mount_name)
115
116 if errors:
117 return None
118 return volume_config
119
120
121def mount_volume(config):
122 if os.path.exists(config['mountpoint']):
123 if not os.path.isdir(config['mountpoint']):
124 hookenv.log('Not a directory: {}'.format(config['mountpoint']))
125 raise VolumeConfigurationError()
126 else:
127 host.mkdir(config['mountpoint'])
128 if os.path.ismount(config['mountpoint']):
129 unmount_volume(config)
130 if not host.mount(config['device'], config['mountpoint'], persist=True):
131 raise VolumeConfigurationError()
132
133
134def unmount_volume(config):
135 if os.path.ismount(config['mountpoint']):
136 if not host.umount(config['mountpoint'], persist=True):
137 raise VolumeConfigurationError()
138
139
140def managed_mounts():
141 '''List of all mounted managed volumes'''
142 return filter(lambda mount: mount[0].startswith(MOUNT_BASE), host.mounts())
143
144
145def configure_volume(before_change=lambda: None, after_change=lambda: None):
146 '''Set up storage (or don't) according to the charm's volume configuration.
147 Returns the mount point or "ephemeral". before_change and after_change
148 are optional functions to be called if the volume configuration changes.
149 '''
150
151 config = get_config()
152 if not config:
153 hookenv.log('Failed to read volume configuration', hookenv.CRITICAL)
154 raise VolumeConfigurationError()
155
156 if config['ephemeral']:
157 if os.path.ismount(config['mountpoint']):
158 before_change()
159 unmount_volume(config)
160 after_change()
161 return 'ephemeral'
162 else:
163 # persistent storage
164 if os.path.ismount(config['mountpoint']):
165 mounts = dict(managed_mounts())
166 if mounts.get(config['mountpoint']) != config['device']:
167 before_change()
168 unmount_volume(config)
169 mount_volume(config)
170 after_change()
171 else:
172 before_change()
173 mount_volume(config)
174 after_change()
175 return config['mountpoint']
0176
=== added directory 'hooks/charmhelpers/contrib/database'
=== added file 'hooks/charmhelpers/contrib/database/__init__.py'
=== added file 'hooks/charmhelpers/contrib/database/mysql.py'
--- hooks/charmhelpers/contrib/database/mysql.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/database/mysql.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,372 @@
1"""Helper for working with a MySQL database"""
2import json
3import socket
4import re
5import sys
6import platform
7import os
8import glob
9
10from string import upper
11
12from charmhelpers.core.host import (
13 mkdir,
14 pwgen,
15 write_file
16)
17from charmhelpers.core.hookenv import (
18 relation_get,
19 related_units,
20 unit_get,
21 log,
22 DEBUG,
23 INFO,
24)
25from charmhelpers.core.hookenv import config as config_get
26from charmhelpers.fetch import (
27 apt_install,
28 apt_update,
29 filter_installed_packages,
30)
31from charmhelpers.contrib.peerstorage import (
32 peer_store,
33 peer_retrieve,
34)
35
36try:
37 import MySQLdb
38except ImportError:
39 apt_update(fatal=True)
40 apt_install(filter_installed_packages(['python-mysqldb']), fatal=True)
41 import MySQLdb
42
43
44class MySQLHelper(object):
45
46 def __init__(self, rpasswdf_template, upasswdf_template, host='localhost'):
47 self.host = host
48 # Password file path templates
49 self.root_passwd_file_template = rpasswdf_template
50 self.user_passwd_file_template = upasswdf_template
51
52 def connect(self, user='root', password=None):
53 self.connection = MySQLdb.connect(user=user, host=self.host,
54 passwd=password)
55
56 def database_exists(self, db_name):
57 cursor = self.connection.cursor()
58 try:
59 cursor.execute("SHOW DATABASES")
60 databases = [i[0] for i in cursor.fetchall()]
61 finally:
62 cursor.close()
63
64 return db_name in databases
65
66 def create_database(self, db_name):
67 cursor = self.connection.cursor()
68 try:
69 cursor.execute("CREATE DATABASE {} CHARACTER SET UTF8"
70 .format(db_name))
71 finally:
72 cursor.close()
73
74 def grant_exists(self, db_name, db_user, remote_ip):
75 cursor = self.connection.cursor()
76 priv_string = "GRANT ALL PRIVILEGES ON `{}`.* " \
77 "TO '{}'@'{}'".format(db_name, db_user, remote_ip)
78 try:
79 cursor.execute("SHOW GRANTS for '{}'@'{}'".format(db_user,
80 remote_ip))
81 grants = [i[0] for i in cursor.fetchall()]
82 except MySQLdb.OperationalError:
83 return False
84 finally:
85 cursor.close()
86
87 # TODO: review for different grants
88 return priv_string in grants
89
90 def create_grant(self, db_name, db_user, remote_ip, password):
91 cursor = self.connection.cursor()
92 try:
93 # TODO: review for different grants
94 cursor.execute("GRANT ALL PRIVILEGES ON {}.* TO '{}'@'{}' "
95 "IDENTIFIED BY '{}'".format(db_name,
96 db_user,
97 remote_ip,
98 password))
99 finally:
100 cursor.close()
101
102 def create_admin_grant(self, db_user, remote_ip, password):
103 cursor = self.connection.cursor()
104 try:
105 cursor.execute("GRANT ALL PRIVILEGES ON *.* TO '{}'@'{}' "
106 "IDENTIFIED BY '{}'".format(db_user,
107 remote_ip,
108 password))
109 finally:
110 cursor.close()
111
112 def cleanup_grant(self, db_user, remote_ip):
113 cursor = self.connection.cursor()
114 try:
115 cursor.execute("DROP FROM mysql.user WHERE user='{}' "
116 "AND HOST='{}'".format(db_user,
117 remote_ip))
118 finally:
119 cursor.close()
120
121 def execute(self, sql):
122 """Execute arbitary SQL against the database."""
123 cursor = self.connection.cursor()
124 try:
125 cursor.execute(sql)
126 finally:
127 cursor.close()
128
129 def migrate_passwords_to_peer_relation(self):
130 """Migrate any passwords storage on disk to cluster peer relation."""
131 dirname = os.path.dirname(self.root_passwd_file_template)
132 path = os.path.join(dirname, '*.passwd')
133 for f in glob.glob(path):
134 _key = os.path.basename(f)
135 with open(f, 'r') as passwd:
136 _value = passwd.read().strip()
137
138 try:
139 peer_store(_key, _value)
140 os.unlink(f)
141 except ValueError:
142 # NOTE cluster relation not yet ready - skip for now
143 pass
144
145 def get_mysql_password_on_disk(self, username=None, password=None):
146 """Retrieve, generate or store a mysql password for the provided
147 username on disk."""
148 if username:
149 template = self.user_passwd_file_template
150 passwd_file = template.format(username)
151 else:
152 passwd_file = self.root_passwd_file_template
153
154 _password = None
155 if os.path.exists(passwd_file):
156 with open(passwd_file, 'r') as passwd:
157 _password = passwd.read().strip()
158 else:
159 mkdir(os.path.dirname(passwd_file), owner='root', group='root',
160 perms=0o770)
161 # Force permissions - for some reason the chmod in makedirs fails
162 os.chmod(os.path.dirname(passwd_file), 0o770)
163 _password = password or pwgen(length=32)
164 write_file(passwd_file, _password, owner='root', group='root',
165 perms=0o660)
166
167 return _password
168
169 def get_mysql_password(self, username=None, password=None):
170 """Retrieve, generate or store a mysql password for the provided
171 username using peer relation cluster."""
172 self.migrate_passwords_to_peer_relation()
173 if username:
174 _key = 'mysql-{}.passwd'.format(username)
175 else:
176 _key = 'mysql.passwd'
177
178 try:
179 _password = peer_retrieve(_key)
180 if _password is None:
181 _password = password or pwgen(length=32)
182 peer_store(_key, _password)
183 except ValueError:
184 # cluster relation is not yet started; use on-disk
185 _password = self.get_mysql_password_on_disk(username, password)
186
187 return _password
188
189 def get_mysql_root_password(self, password=None):
190 """Retrieve or generate mysql root password for service units."""
191 return self.get_mysql_password(username=None, password=password)
192
193 def get_allowed_units(self, database, username, relation_id=None):
194 """Get list of units with access grants for database with username.
195
196 This is typically used to provide shared-db relations with a list of
197 which units have been granted access to the given database.
198 """
199 self.connect(password=self.get_mysql_root_password())
200 allowed_units = set()
201 for unit in related_units(relation_id):
202 settings = relation_get(rid=relation_id, unit=unit)
203 # First check for setting with prefix, then without
204 for attr in ["%s_hostname" % (database), 'hostname']:
205 hosts = settings.get(attr, None)
206 if hosts:
207 break
208
209 if hosts:
210 # hostname can be json-encoded list of hostnames
211 try:
212 hosts = json.loads(hosts)
213 except ValueError:
214 hosts = [hosts]
215 else:
216 hosts = [settings['private-address']]
217
218 if hosts:
219 for host in hosts:
220 if self.grant_exists(database, username, host):
221 log("Grant exists for host '%s' on db '%s'" %
222 (host, database), level=DEBUG)
223 if unit not in allowed_units:
224 allowed_units.add(unit)
225 else:
226 log("Grant does NOT exist for host '%s' on db '%s'" %
227 (host, database), level=DEBUG)
228 else:
229 log("No hosts found for grant check", level=INFO)
230
231 return allowed_units
232
233 def configure_db(self, hostname, database, username, admin=False):
234 """Configure access to database for username from hostname."""
235 if config_get('prefer-ipv6'):
236 remote_ip = hostname
237 elif hostname != unit_get('private-address'):
238 try:
239 remote_ip = socket.gethostbyname(hostname)
240 except Exception:
241 # socket.gethostbyname doesn't support ipv6
242 remote_ip = hostname
243 else:
244 remote_ip = '127.0.0.1'
245
246 self.connect(password=self.get_mysql_root_password())
247 if not self.database_exists(database):
248 self.create_database(database)
249
250 password = self.get_mysql_password(username)
251 if not self.grant_exists(database, username, remote_ip):
252 if not admin:
253 self.create_grant(database, username, remote_ip, password)
254 else:
255 self.create_admin_grant(username, remote_ip, password)
256
257 return password
258
259
260class PerconaClusterHelper(object):
261
262 # Going for the biggest page size to avoid wasted bytes. InnoDB page size is
263 # 16MB
264 DEFAULT_PAGE_SIZE = 16 * 1024 * 1024
265
266 def human_to_bytes(self, human):
267 """Convert human readable configuration options to bytes."""
268 num_re = re.compile('^[0-9]+$')
269 if num_re.match(human):
270 return human
271
272 factors = {
273 'K': 1024,
274 'M': 1048576,
275 'G': 1073741824,
276 'T': 1099511627776
277 }
278 modifier = human[-1]
279 if modifier in factors:
280 return int(human[:-1]) * factors[modifier]
281
282 if modifier == '%':
283 total_ram = self.human_to_bytes(self.get_mem_total())
284 if self.is_32bit_system() and total_ram > self.sys_mem_limit():
285 total_ram = self.sys_mem_limit()
286 factor = int(human[:-1]) * 0.01
287 pctram = total_ram * factor
288 return int(pctram - (pctram % self.DEFAULT_PAGE_SIZE))
289
290 raise ValueError("Can only convert K,M,G, or T")
291
292 def is_32bit_system(self):
293 """Determine whether system is 32 or 64 bit."""
294 try:
295 return sys.maxsize < 2 ** 32
296 except OverflowError:
297 return False
298
299 def sys_mem_limit(self):
300 """Determine the default memory limit for the current service unit."""
301 if platform.machine() in ['armv7l']:
302 _mem_limit = self.human_to_bytes('2700M') # experimentally determined
303 else:
304 # Limit for x86 based 32bit systems
305 _mem_limit = self.human_to_bytes('4G')
306
307 return _mem_limit
308
309 def get_mem_total(self):
310 """Calculate the total memory in the current service unit."""
311 with open('/proc/meminfo') as meminfo_file:
312 for line in meminfo_file:
313 key, mem = line.split(':', 2)
314 if key == 'MemTotal':
315 mtot, modifier = mem.strip().split(' ')
316 return '%s%s' % (mtot, upper(modifier[0]))
317
318 def parse_config(self):
319 """Parse charm configuration and calculate values for config files."""
320 config = config_get()
321 mysql_config = {}
322 if 'max-connections' in config:
323 mysql_config['max_connections'] = config['max-connections']
324
325 # Total memory available for dataset
326 dataset_bytes = self.human_to_bytes(config['dataset-size'])
327 mysql_config['dataset_bytes'] = dataset_bytes
328
329 if 'query-cache-type' in config:
330 # Query Cache Configuration
331 mysql_config['query_cache_size'] = config['query-cache-size']
332 if (config['query-cache-size'] == -1 and
333 config['query-cache-type'] in ['ON', 'DEMAND']):
334 # Calculate the query cache size automatically
335 qcache_bytes = (dataset_bytes * 0.20)
336 qcache_bytes = int(qcache_bytes -
337 (qcache_bytes % self.DEFAULT_PAGE_SIZE))
338 mysql_config['query_cache_size'] = qcache_bytes
339 dataset_bytes -= qcache_bytes
340
341 # 5.5 allows the words, but not 5.1
342 if config['query-cache-type'] == 'ON':
343 mysql_config['query_cache_type'] = 1
344 elif config['query-cache-type'] == 'DEMAND':
345 mysql_config['query_cache_type'] = 2
346 else:
347 mysql_config['query_cache_type'] = 0
348
349 # Set a sane default key_buffer size
350 mysql_config['key_buffer'] = self.human_to_bytes('32M')
351
352 if 'preferred-storage-engine' in config:
353 # Storage engine configuration
354 preferred_engines = config['preferred-storage-engine'].split(',')
355 chunk_size = int(dataset_bytes / len(preferred_engines))
356 mysql_config['innodb_flush_log_at_trx_commit'] = 1
357 mysql_config['sync_binlog'] = 1
358 if 'InnoDB' in preferred_engines:
359 mysql_config['innodb_buffer_pool_size'] = chunk_size
360 if config['tuning-level'] == 'fast':
361 mysql_config['innodb_flush_log_at_trx_commit'] = 2
362 else:
363 mysql_config['innodb_buffer_pool_size'] = 0
364
365 mysql_config['default_storage_engine'] = preferred_engines[0]
366 if 'MyISAM' in preferred_engines:
367 mysql_config['key_buffer'] = chunk_size
368
369 if config['tuning-level'] == 'fast':
370 mysql_config['sync_binlog'] = 0
371
372 return mysql_config
0373
=== added directory 'hooks/charmhelpers/contrib/hahelpers'
=== added file 'hooks/charmhelpers/contrib/hahelpers/__init__.py'
--- hooks/charmhelpers/contrib/hahelpers/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hahelpers/__init__.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
016
=== added file 'hooks/charmhelpers/contrib/hahelpers/apache.py'
--- hooks/charmhelpers/contrib/hahelpers/apache.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hahelpers/apache.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,82 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17#
18# Copyright 2012 Canonical Ltd.
19#
20# This file is sourced from lp:openstack-charm-helpers
21#
22# Authors:
23# James Page <james.page@ubuntu.com>
24# Adam Gandelman <adamg@ubuntu.com>
25#
26
27import subprocess
28
29from charmhelpers.core.hookenv import (
30 config as config_get,
31 relation_get,
32 relation_ids,
33 related_units as relation_list,
34 log,
35 INFO,
36)
37
38
39def get_cert(cn=None):
40 # TODO: deal with multiple https endpoints via charm config
41 cert = config_get('ssl_cert')
42 key = config_get('ssl_key')
43 if not (cert and key):
44 log("Inspecting identity-service relations for SSL certificate.",
45 level=INFO)
46 cert = key = None
47 if cn:
48 ssl_cert_attr = 'ssl_cert_{}'.format(cn)
49 ssl_key_attr = 'ssl_key_{}'.format(cn)
50 else:
51 ssl_cert_attr = 'ssl_cert'
52 ssl_key_attr = 'ssl_key'
53 for r_id in relation_ids('identity-service'):
54 for unit in relation_list(r_id):
55 if not cert:
56 cert = relation_get(ssl_cert_attr,
57 rid=r_id, unit=unit)
58 if not key:
59 key = relation_get(ssl_key_attr,
60 rid=r_id, unit=unit)
61 return (cert, key)
62
63
64def get_ca_cert():
65 ca_cert = config_get('ssl_ca')
66 if ca_cert is None:
67 log("Inspecting identity-service relations for CA SSL certificate.",
68 level=INFO)
69 for r_id in relation_ids('identity-service'):
70 for unit in relation_list(r_id):
71 if ca_cert is None:
72 ca_cert = relation_get('ca_cert',
73 rid=r_id, unit=unit)
74 return ca_cert
75
76
77def install_ca_cert(ca_cert):
78 if ca_cert:
79 with open('/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt',
80 'w') as crt:
81 crt.write(ca_cert)
82 subprocess.check_call(['update-ca-certificates', '--fresh'])
083
=== added file 'hooks/charmhelpers/contrib/hahelpers/cluster.py'
--- hooks/charmhelpers/contrib/hahelpers/cluster.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,268 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17#
18# Copyright 2012 Canonical Ltd.
19#
20# Authors:
21# James Page <james.page@ubuntu.com>
22# Adam Gandelman <adamg@ubuntu.com>
23#
24
25"""
26Helpers for clustering and determining "cluster leadership" and other
27clustering-related helpers.
28"""
29
30import subprocess
31import os
32
33from socket import gethostname as get_unit_hostname
34
35import six
36
37from charmhelpers.core.hookenv import (
38 log,
39 relation_ids,
40 related_units as relation_list,
41 relation_get,
42 config as config_get,
43 INFO,
44 ERROR,
45 WARNING,
46 unit_get,
47)
48from charmhelpers.core.decorators import (
49 retry_on_exception,
50)
51
52
53class HAIncompleteConfig(Exception):
54 pass
55
56
57class CRMResourceNotFound(Exception):
58 pass
59
60
61def is_elected_leader(resource):
62 """
63 Returns True if the charm executing this is the elected cluster leader.
64
65 It relies on two mechanisms to determine leadership:
66 1. If the charm is part of a corosync cluster, call corosync to
67 determine leadership.
68 2. If the charm is not part of a corosync cluster, the leader is
69 determined as being "the alive unit with the lowest unit numer". In
70 other words, the oldest surviving unit.
71 """
72 if is_clustered():
73 if not is_crm_leader(resource):
74 log('Deferring action to CRM leader.', level=INFO)
75 return False
76 else:
77 peers = peer_units()
78 if peers and not oldest_peer(peers):
79 log('Deferring action to oldest service unit.', level=INFO)
80 return False
81 return True
82
83
84def is_clustered():
85 for r_id in (relation_ids('ha') or []):
86 for unit in (relation_list(r_id) or []):
87 clustered = relation_get('clustered',
88 rid=r_id,
89 unit=unit)
90 if clustered:
91 return True
92 return False
93
94
95@retry_on_exception(5, base_delay=2, exc_type=CRMResourceNotFound)
96def is_crm_leader(resource, retry=False):
97 """
98 Returns True if the charm calling this is the elected corosync leader,
99 as returned by calling the external "crm" command.
100
101 We allow this operation to be retried to avoid the possibility of getting a
102 false negative. See LP #1396246 for more info.
103 """
104 cmd = ['crm', 'resource', 'show', resource]
105 try:
106 status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
107 if not isinstance(status, six.text_type):
108 status = six.text_type(status, "utf-8")
109 except subprocess.CalledProcessError:
110 status = None
111
112 if status and get_unit_hostname() in status:
113 return True
114
115 if status and "resource %s is NOT running" % (resource) in status:
116 raise CRMResourceNotFound("CRM resource %s not found" % (resource))
117
118 return False
119
120
121def is_leader(resource):
122 log("is_leader is deprecated. Please consider using is_crm_leader "
123 "instead.", level=WARNING)
124 return is_crm_leader(resource)
125
126
127def peer_units(peer_relation="cluster"):
128 peers = []
129 for r_id in (relation_ids(peer_relation) or []):
130 for unit in (relation_list(r_id) or []):
131 peers.append(unit)
132 return peers
133
134
135def peer_ips(peer_relation='cluster', addr_key='private-address'):
136 '''Return a dict of peers and their private-address'''
137 peers = {}
138 for r_id in relation_ids(peer_relation):
139 for unit in relation_list(r_id):
140 peers[unit] = relation_get(addr_key, rid=r_id, unit=unit)
141 return peers
142
143
144def oldest_peer(peers):
145 """Determines who the oldest peer is by comparing unit numbers."""
146 local_unit_no = int(os.getenv('JUJU_UNIT_NAME').split('/')[1])
147 for peer in peers:
148 remote_unit_no = int(peer.split('/')[1])
149 if remote_unit_no < local_unit_no:
150 return False
151 return True
152
153
154def eligible_leader(resource):
155 log("eligible_leader is deprecated. Please consider using "
156 "is_elected_leader instead.", level=WARNING)
157 return is_elected_leader(resource)
158
159
160def https():
161 '''
162 Determines whether enough data has been provided in configuration
163 or relation data to configure HTTPS
164 .
165 returns: boolean
166 '''
167 if config_get('use-https') == "yes":
168 return True
169 if config_get('ssl_cert') and config_get('ssl_key'):
170 return True
171 for r_id in relation_ids('identity-service'):
172 for unit in relation_list(r_id):
173 # TODO - needs fixing for new helper as ssl_cert/key suffixes with CN
174 rel_state = [
175 relation_get('https_keystone', rid=r_id, unit=unit),
176 relation_get('ca_cert', rid=r_id, unit=unit),
177 ]
178 # NOTE: works around (LP: #1203241)
179 if (None not in rel_state) and ('' not in rel_state):
180 return True
181 return False
182
183
184def determine_api_port(public_port, singlenode_mode=False):
185 '''
186 Determine correct API server listening port based on
187 existence of HTTPS reverse proxy and/or haproxy.
188
189 public_port: int: standard public port for given service
190
191 singlenode_mode: boolean: Shuffle ports when only a single unit is present
192
193 returns: int: the correct listening port for the API service
194 '''
195 i = 0
196 if singlenode_mode:
197 i += 1
198 elif len(peer_units()) > 0 or is_clustered():
199 i += 1
200 if https():
201 i += 1
202 return public_port - (i * 10)
203
204
205def determine_apache_port(public_port, singlenode_mode=False):
206 '''
207 Description: Determine correct apache listening port based on public IP +
208 state of the cluster.
209
210 public_port: int: standard public port for given service
211
212 singlenode_mode: boolean: Shuffle ports when only a single unit is present
213
214 returns: int: the correct listening port for the HAProxy service
215 '''
216 i = 0
217 if singlenode_mode:
218 i += 1
219 elif len(peer_units()) > 0 or is_clustered():
220 i += 1
221 return public_port - (i * 10)
222
223
224def get_hacluster_config(exclude_keys=None):
225 '''
226 Obtains all relevant configuration from charm configuration required
227 for initiating a relation to hacluster:
228
229 ha-bindiface, ha-mcastport, vip
230
231 param: exclude_keys: list of setting key(s) to be excluded.
232 returns: dict: A dict containing settings keyed by setting name.
233 raises: HAIncompleteConfig if settings are missing.
234 '''
235 settings = ['ha-bindiface', 'ha-mcastport', 'vip']
236 conf = {}
237 for setting in settings:
238 if exclude_keys and setting in exclude_keys:
239 continue
240
241 conf[setting] = config_get(setting)
242 missing = []
243 [missing.append(s) for s, v in six.iteritems(conf) if v is None]
244 if missing:
245 log('Insufficient config data to configure hacluster.', level=ERROR)
246 raise HAIncompleteConfig
247 return conf
248
249
250def canonical_url(configs, vip_setting='vip'):
251 '''
252 Returns the correct HTTP URL to this host given the state of HTTPS
253 configuration and hacluster.
254
255 :configs : OSTemplateRenderer: A config tempating object to inspect for
256 a complete https context.
257
258 :vip_setting: str: Setting in charm config that specifies
259 VIP address.
260 '''
261 scheme = 'http'
262 if 'https' in configs.complete_contexts():
263 scheme = 'https'
264 if is_clustered():
265 addr = config_get(vip_setting)
266 else:
267 addr = unit_get('private-address')
268 return '%s://%s' % (scheme, addr)
0269
=== added directory 'hooks/charmhelpers/contrib/network'
=== added file 'hooks/charmhelpers/contrib/network/__init__.py'
--- hooks/charmhelpers/contrib/network/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/network/__init__.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
016
=== added file 'hooks/charmhelpers/contrib/network/ip.py'
--- hooks/charmhelpers/contrib/network/ip.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/network/ip.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,367 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import glob
18import re
19import subprocess
20
21from functools import partial
22
23from charmhelpers.core.hookenv import unit_get
24from charmhelpers.fetch import apt_install
25from charmhelpers.core.hookenv import (
26 log
27)
28
29try:
30 import netifaces
31except ImportError:
32 apt_install('python-netifaces')
33 import netifaces
34
35try:
36 import netaddr
37except ImportError:
38 apt_install('python-netaddr')
39 import netaddr
40
41
42def _validate_cidr(network):
43 try:
44 netaddr.IPNetwork(network)
45 except (netaddr.core.AddrFormatError, ValueError):
46 raise ValueError("Network (%s) is not in CIDR presentation format" %
47 network)
48
49
50def no_ip_found_error_out(network):
51 errmsg = ("No IP address found in network: %s" % network)
52 raise ValueError(errmsg)
53
54
55def get_address_in_network(network, fallback=None, fatal=False):
56 """Get an IPv4 or IPv6 address within the network from the host.
57
58 :param network (str): CIDR presentation format. For example,
59 '192.168.1.0/24'.
60 :param fallback (str): If no address is found, return fallback.
61 :param fatal (boolean): If no address is found, fallback is not
62 set and fatal is True then exit(1).
63 """
64 if network is None:
65 if fallback is not None:
66 return fallback
67
68 if fatal:
69 no_ip_found_error_out(network)
70 else:
71 return None
72
73 _validate_cidr(network)
74 network = netaddr.IPNetwork(network)
75 for iface in netifaces.interfaces():
76 addresses = netifaces.ifaddresses(iface)
77 if network.version == 4 and netifaces.AF_INET in addresses:
78 addr = addresses[netifaces.AF_INET][0]['addr']
79 netmask = addresses[netifaces.AF_INET][0]['netmask']
80 cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask))
81 if cidr in network:
82 return str(cidr.ip)
83
84 if network.version == 6 and netifaces.AF_INET6 in addresses:
85 for addr in addresses[netifaces.AF_INET6]:
86 if not addr['addr'].startswith('fe80'):
87 cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'],
88 addr['netmask']))
89 if cidr in network:
90 return str(cidr.ip)
91
92 if fallback is not None:
93 return fallback
94
95 if fatal:
96 no_ip_found_error_out(network)
97
98 return None
99
100
101def is_ipv6(address):
102 """Determine whether provided address is IPv6 or not."""
103 try:
104 address = netaddr.IPAddress(address)
105 except netaddr.AddrFormatError:
106 # probably a hostname - so not an address at all!
107 return False
108
109 return address.version == 6
110
111
112def is_address_in_network(network, address):
113 """
114 Determine whether the provided address is within a network range.
115
116 :param network (str): CIDR presentation format. For example,
117 '192.168.1.0/24'.
118 :param address: An individual IPv4 or IPv6 address without a net
119 mask or subnet prefix. For example, '192.168.1.1'.
120 :returns boolean: Flag indicating whether address is in network.
121 """
122 try:
123 network = netaddr.IPNetwork(network)
124 except (netaddr.core.AddrFormatError, ValueError):
125 raise ValueError("Network (%s) is not in CIDR presentation format" %
126 network)
127
128 try:
129 address = netaddr.IPAddress(address)
130 except (netaddr.core.AddrFormatError, ValueError):
131 raise ValueError("Address (%s) is not in correct presentation format" %
132 address)
133
134 if address in network:
135 return True
136 else:
137 return False
138
139
140def _get_for_address(address, key):
141 """Retrieve an attribute of or the physical interface that
142 the IP address provided could be bound to.
143
144 :param address (str): An individual IPv4 or IPv6 address without a net
145 mask or subnet prefix. For example, '192.168.1.1'.
146 :param key: 'iface' for the physical interface name or an attribute
147 of the configured interface, for example 'netmask'.
148 :returns str: Requested attribute or None if address is not bindable.
149 """
150 address = netaddr.IPAddress(address)
151 for iface in netifaces.interfaces():
152 addresses = netifaces.ifaddresses(iface)
153 if address.version == 4 and netifaces.AF_INET in addresses:
154 addr = addresses[netifaces.AF_INET][0]['addr']
155 netmask = addresses[netifaces.AF_INET][0]['netmask']
156 network = netaddr.IPNetwork("%s/%s" % (addr, netmask))
157 cidr = network.cidr
158 if address in cidr:
159 if key == 'iface':
160 return iface
161 else:
162 return addresses[netifaces.AF_INET][0][key]
163
164 if address.version == 6 and netifaces.AF_INET6 in addresses:
165 for addr in addresses[netifaces.AF_INET6]:
166 if not addr['addr'].startswith('fe80'):
167 network = netaddr.IPNetwork("%s/%s" % (addr['addr'],
168 addr['netmask']))
169 cidr = network.cidr
170 if address in cidr:
171 if key == 'iface':
172 return iface
173 elif key == 'netmask' and cidr:
174 return str(cidr).split('/')[1]
175 else:
176 return addr[key]
177
178 return None
179
180
181get_iface_for_address = partial(_get_for_address, key='iface')
182
183
184get_netmask_for_address = partial(_get_for_address, key='netmask')
185
186
187def format_ipv6_addr(address):
188 """If address is IPv6, wrap it in '[]' otherwise return None.
189
190 This is required by most configuration files when specifying IPv6
191 addresses.
192 """
193 if is_ipv6(address):
194 return "[%s]" % address
195
196 return None
197
198
199def get_iface_addr(iface='eth0', inet_type='AF_INET', inc_aliases=False,
200 fatal=True, exc_list=None):
201 """Return the assigned IP address for a given interface, if any."""
202 # Extract nic if passed /dev/ethX
203 if '/' in iface:
204 iface = iface.split('/')[-1]
205
206 if not exc_list:
207 exc_list = []
208
209 try:
210 inet_num = getattr(netifaces, inet_type)
211 except AttributeError:
212 raise Exception("Unknown inet type '%s'" % str(inet_type))
213
214 interfaces = netifaces.interfaces()
215 if inc_aliases:
216 ifaces = []
217 for _iface in interfaces:
218 if iface == _iface or _iface.split(':')[0] == iface:
219 ifaces.append(_iface)
220
221 if fatal and not ifaces:
222 raise Exception("Invalid interface '%s'" % iface)
223
224 ifaces.sort()
225 else:
226 if iface not in interfaces:
227 if fatal:
228 raise Exception("Interface '%s' not found " % (iface))
229 else:
230 return []
231
232 else:
233 ifaces = [iface]
234
235 addresses = []
236 for netiface in ifaces:
237 net_info = netifaces.ifaddresses(netiface)
238 if inet_num in net_info:
239 for entry in net_info[inet_num]:
240 if 'addr' in entry and entry['addr'] not in exc_list:
241 addresses.append(entry['addr'])
242
243 if fatal and not addresses:
244 raise Exception("Interface '%s' doesn't have any %s addresses." %
245 (iface, inet_type))
246
247 return sorted(addresses)
248
249
250get_ipv4_addr = partial(get_iface_addr, inet_type='AF_INET')
251
252
253def get_iface_from_addr(addr):
254 """Work out on which interface the provided address is configured."""
255 for iface in netifaces.interfaces():
256 addresses = netifaces.ifaddresses(iface)
257 for inet_type in addresses:
258 for _addr in addresses[inet_type]:
259 _addr = _addr['addr']
260 # link local
261 ll_key = re.compile("(.+)%.*")
262 raw = re.match(ll_key, _addr)
263 if raw:
264 _addr = raw.group(1)
265
266 if _addr == addr:
267 log("Address '%s' is configured on iface '%s'" %
268 (addr, iface))
269 return iface
270
271 msg = "Unable to infer net iface on which '%s' is configured" % (addr)
272 raise Exception(msg)
273
274
275def sniff_iface(f):
276 """Ensure decorated function is called with a value for iface.
277
278 If no iface provided, inject net iface inferred from unit private address.
279 """
280 def iface_sniffer(*args, **kwargs):
281 if not kwargs.get('iface', None):
282 kwargs['iface'] = get_iface_from_addr(unit_get('private-address'))
283
284 return f(*args, **kwargs)
285
286 return iface_sniffer
287
288
289@sniff_iface
290def get_ipv6_addr(iface=None, inc_aliases=False, fatal=True, exc_list=None,
291 dynamic_only=True):
292 """Get assigned IPv6 address for a given interface.
293
294 Returns list of addresses found. If no address found, returns empty list.
295
296 If iface is None, we infer the current primary interface by doing a reverse
297 lookup on the unit private-address.
298
299 We currently only support scope global IPv6 addresses i.e. non-temporary
300 addresses. If no global IPv6 address is found, return the first one found
301 in the ipv6 address list.
302 """
303 addresses = get_iface_addr(iface=iface, inet_type='AF_INET6',
304 inc_aliases=inc_aliases, fatal=fatal,
305 exc_list=exc_list)
306
307 if addresses:
308 global_addrs = []
309 for addr in addresses:
310 key_scope_link_local = re.compile("^fe80::..(.+)%(.+)")
311 m = re.match(key_scope_link_local, addr)
312 if m:
313 eui_64_mac = m.group(1)
314 iface = m.group(2)
315 else:
316 global_addrs.append(addr)
317
318 if global_addrs:
319 # Make sure any found global addresses are not temporary
320 cmd = ['ip', 'addr', 'show', iface]
321 out = subprocess.check_output(cmd).decode('UTF-8')
322 if dynamic_only:
323 key = re.compile("inet6 (.+)/[0-9]+ scope global dynamic.*")
324 else:
325 key = re.compile("inet6 (.+)/[0-9]+ scope global.*")
326
327 addrs = []
328 for line in out.split('\n'):
329 line = line.strip()
330 m = re.match(key, line)
331 if m and 'temporary' not in line:
332 # Return the first valid address we find
333 for addr in global_addrs:
334 if m.group(1) == addr:
335 if not dynamic_only or \
336 m.group(1).endswith(eui_64_mac):
337 addrs.append(addr)
338
339 if addrs:
340 return addrs
341
342 if fatal:
343 raise Exception("Interface '%s' does not have a scope global "
344 "non-temporary ipv6 address." % iface)
345
346 return []
347
348
349def get_bridges(vnic_dir='/sys/devices/virtual/net'):
350 """Return a list of bridges on the system."""
351 b_regex = "%s/*/bridge" % vnic_dir
352 return [x.replace(vnic_dir, '').split('/')[1] for x in glob.glob(b_regex)]
353
354
355def get_bridge_nics(bridge, vnic_dir='/sys/devices/virtual/net'):
356 """Return a list of nics comprising a given bridge on the system."""
357 brif_regex = "%s/%s/brif/*" % (vnic_dir, bridge)
358 return [x.split('/')[-1] for x in glob.glob(brif_regex)]
359
360
361def is_bridge_member(nic):
362 """Check if a given nic is a member of a bridge."""
363 for bridge in get_bridges():
364 if nic in get_bridge_nics(bridge):
365 return True
366
367 return False
0368
=== added directory 'hooks/charmhelpers/contrib/network/ovs'
=== added file 'hooks/charmhelpers/contrib/network/ovs/__init__.py'
--- hooks/charmhelpers/contrib/network/ovs/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/network/ovs/__init__.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,96 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17''' Helpers for interacting with OpenvSwitch '''
18import subprocess
19import os
20from charmhelpers.core.hookenv import (
21 log, WARNING
22)
23from charmhelpers.core.host import (
24 service
25)
26
27
28def add_bridge(name):
29 ''' Add the named bridge to openvswitch '''
30 log('Creating bridge {}'.format(name))
31 subprocess.check_call(["ovs-vsctl", "--", "--may-exist", "add-br", name])
32
33
34def del_bridge(name):
35 ''' Delete the named bridge from openvswitch '''
36 log('Deleting bridge {}'.format(name))
37 subprocess.check_call(["ovs-vsctl", "--", "--if-exists", "del-br", name])
38
39
40def add_bridge_port(name, port, promisc=False):
41 ''' Add a port to the named openvswitch bridge '''
42 log('Adding port {} to bridge {}'.format(port, name))
43 subprocess.check_call(["ovs-vsctl", "--", "--may-exist", "add-port",
44 name, port])
45 subprocess.check_call(["ip", "link", "set", port, "up"])
46 if promisc:
47 subprocess.check_call(["ip", "link", "set", port, "promisc", "on"])
48 else:
49 subprocess.check_call(["ip", "link", "set", port, "promisc", "off"])
50
51
52def del_bridge_port(name, port):
53 ''' Delete a port from the named openvswitch bridge '''
54 log('Deleting port {} from bridge {}'.format(port, name))
55 subprocess.check_call(["ovs-vsctl", "--", "--if-exists", "del-port",
56 name, port])
57 subprocess.check_call(["ip", "link", "set", port, "down"])
58 subprocess.check_call(["ip", "link", "set", port, "promisc", "off"])
59
60
61def set_manager(manager):
62 ''' Set the controller for the local openvswitch '''
63 log('Setting manager for local ovs to {}'.format(manager))
64 subprocess.check_call(['ovs-vsctl', 'set-manager',
65 'ssl:{}'.format(manager)])
66
67
68CERT_PATH = '/etc/openvswitch/ovsclient-cert.pem'
69
70
71def get_certificate():
72 ''' Read openvswitch certificate from disk '''
73 if os.path.exists(CERT_PATH):
74 log('Reading ovs certificate from {}'.format(CERT_PATH))
75 with open(CERT_PATH, 'r') as cert:
76 full_cert = cert.read()
77 begin_marker = "-----BEGIN CERTIFICATE-----"
78 end_marker = "-----END CERTIFICATE-----"
79 begin_index = full_cert.find(begin_marker)
80 end_index = full_cert.rfind(end_marker)
81 if end_index == -1 or begin_index == -1:
82 raise RuntimeError("Certificate does not contain valid begin"
83 " and end markers.")
84 full_cert = full_cert[begin_index:(end_index + len(end_marker))]
85 return full_cert
86 else:
87 log('Certificate not found', level=WARNING)
88 return None
89
90
91def full_restart():
92 ''' Full restart and reload of openvswitch '''
93 if os.path.exists('/etc/init/openvswitch-force-reload-kmod.conf'):
94 service('start', 'openvswitch-force-reload-kmod')
95 else:
96 service('force-reload-kmod', 'openvswitch-switch')
097
=== added file 'hooks/charmhelpers/contrib/network/ufw.py'
--- hooks/charmhelpers/contrib/network/ufw.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/network/ufw.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,276 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17"""
18This module contains helpers to add and remove ufw rules.
19
20Examples:
21
22- open SSH port for subnet 10.0.3.0/24:
23
24 >>> from charmhelpers.contrib.network import ufw
25 >>> ufw.enable()
26 >>> ufw.grant_access(src='10.0.3.0/24', dst='any', port='22', proto='tcp')
27
28- open service by name as defined in /etc/services:
29
30 >>> from charmhelpers.contrib.network import ufw
31 >>> ufw.enable()
32 >>> ufw.service('ssh', 'open')
33
34- close service by port number:
35
36 >>> from charmhelpers.contrib.network import ufw
37 >>> ufw.enable()
38 >>> ufw.service('4949', 'close') # munin
39"""
40import re
41import os
42import subprocess
43from charmhelpers.core import hookenv
44
45__author__ = "Felipe Reyes <felipe.reyes@canonical.com>"
46
47
48class UFWError(Exception):
49 pass
50
51
52class UFWIPv6Error(UFWError):
53 pass
54
55
56def is_enabled():
57 """
58 Check if `ufw` is enabled
59
60 :returns: True if ufw is enabled
61 """
62 output = subprocess.check_output(['ufw', 'status'],
63 universal_newlines=True,
64 env={'LANG': 'en_US',
65 'PATH': os.environ['PATH']})
66
67 m = re.findall(r'^Status: active\n', output, re.M)
68
69 return len(m) >= 1
70
71
72def is_ipv6_ok(soft_fail=False):
73 """
74 Check if IPv6 support is present and ip6tables functional
75
76 :param soft_fail: If set to True and IPv6 support is broken, then reports
77 that the host doesn't have IPv6 support, otherwise a
78 UFWIPv6Error exception is raised.
79 :returns: True if IPv6 is working, False otherwise
80 """
81
82 # do we have IPv6 in the machine?
83 if os.path.isdir('/proc/sys/net/ipv6'):
84 # is ip6tables kernel module loaded?
85 lsmod = subprocess.check_output(['lsmod'], universal_newlines=True)
86 matches = re.findall('^ip6_tables[ ]+', lsmod, re.M)
87 if len(matches) == 0:
88 # ip6tables support isn't complete, let's try to load it
89 try:
90 subprocess.check_output(['modprobe', 'ip6_tables'],
91 universal_newlines=True)
92 # great, we could load the module
93 return True
94 except subprocess.CalledProcessError as ex:
95 hookenv.log("Couldn't load ip6_tables module: %s" % ex.output,
96 level="WARN")
97 # we are in a world where ip6tables isn't working
98 if soft_fail:
99 # so we inform that the machine doesn't have IPv6
100 return False
101 else:
102 raise UFWIPv6Error("IPv6 firewall support broken")
103 else:
104 # the module is present :)
105 return True
106
107 else:
108 # the system doesn't have IPv6
109 return False
110
111
112def disable_ipv6():
113 """
114 Disable ufw IPv6 support in /etc/default/ufw
115 """
116 exit_code = subprocess.call(['sed', '-i', 's/IPV6=.*/IPV6=no/g',
117 '/etc/default/ufw'])
118 if exit_code == 0:
119 hookenv.log('IPv6 support in ufw disabled', level='INFO')
120 else:
121 hookenv.log("Couldn't disable IPv6 support in ufw", level="ERROR")
122 raise UFWError("Couldn't disable IPv6 support in ufw")
123
124
125def enable(soft_fail=False):
126 """
127 Enable ufw
128
129 :param soft_fail: If set to True silently disables IPv6 support in ufw,
130 otherwise a UFWIPv6Error exception is raised when IP6
131 support is broken.
132 :returns: True if ufw is successfully enabled
133 """
134 if is_enabled():
135 return True
136
137 if not is_ipv6_ok(soft_fail):
138 disable_ipv6()
139
140 output = subprocess.check_output(['ufw', 'enable'],
141 universal_newlines=True,
142 env={'LANG': 'en_US',
143 'PATH': os.environ['PATH']})
144
145 m = re.findall('^Firewall is active and enabled on system startup\n',
146 output, re.M)
147 hookenv.log(output, level='DEBUG')
148
149 if len(m) == 0:
150 hookenv.log("ufw couldn't be enabled", level='WARN')
151 return False
152 else:
153 hookenv.log("ufw enabled", level='INFO')
154 return True
155
156
157def disable():
158 """
159 Disable ufw
160
161 :returns: True if ufw is successfully disabled
162 """
163 if not is_enabled():
164 return True
165
166 output = subprocess.check_output(['ufw', 'disable'],
167 universal_newlines=True,
168 env={'LANG': 'en_US',
169 'PATH': os.environ['PATH']})
170
171 m = re.findall(r'^Firewall stopped and disabled on system startup\n',
172 output, re.M)
173 hookenv.log(output, level='DEBUG')
174
175 if len(m) == 0:
176 hookenv.log("ufw couldn't be disabled", level='WARN')
177 return False
178 else:
179 hookenv.log("ufw disabled", level='INFO')
180 return True
181
182
183def modify_access(src, dst='any', port=None, proto=None, action='allow'):
184 """
185 Grant access to an address or subnet
186
187 :param src: address (e.g. 192.168.1.234) or subnet
188 (e.g. 192.168.1.0/24).
189 :param dst: destiny of the connection, if the machine has multiple IPs and
190 connections to only one of those have to accepted this is the
191 field has to be set.
192 :param port: destiny port
193 :param proto: protocol (tcp or udp)
194 :param action: `allow` or `delete`
195 """
196 if not is_enabled():
197 hookenv.log('ufw is disabled, skipping modify_access()', level='WARN')
198 return
199
200 if action == 'delete':
201 cmd = ['ufw', 'delete', 'allow']
202 else:
203 cmd = ['ufw', action]
204
205 if src is not None:
206 cmd += ['from', src]
207
208 if dst is not None:
209 cmd += ['to', dst]
210
211 if port is not None:
212 cmd += ['port', str(port)]
213
214 if proto is not None:
215 cmd += ['proto', proto]
216
217 hookenv.log('ufw {}: {}'.format(action, ' '.join(cmd)), level='DEBUG')
218 p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
219 (stdout, stderr) = p.communicate()
220
221 hookenv.log(stdout, level='INFO')
222
223 if p.returncode != 0:
224 hookenv.log(stderr, level='ERROR')
225 hookenv.log('Error running: {}, exit code: {}'.format(' '.join(cmd),
226 p.returncode),
227 level='ERROR')
228
229
230def grant_access(src, dst='any', port=None, proto=None):
231 """
232 Grant access to an address or subnet
233
234 :param src: address (e.g. 192.168.1.234) or subnet
235 (e.g. 192.168.1.0/24).
236 :param dst: destiny of the connection, if the machine has multiple IPs and
237 connections to only one of those have to accepted this is the
238 field has to be set.
239 :param port: destiny port
240 :param proto: protocol (tcp or udp)
241 """
242 return modify_access(src, dst=dst, port=port, proto=proto, action='allow')
243
244
245def revoke_access(src, dst='any', port=None, proto=None):
246 """
247 Revoke access to an address or subnet
248
249 :param src: address (e.g. 192.168.1.234) or subnet
250 (e.g. 192.168.1.0/24).
251 :param dst: destiny of the connection, if the machine has multiple IPs and
252 connections to only one of those have to accepted this is the
253 field has to be set.
254 :param port: destiny port
255 :param proto: protocol (tcp or udp)
256 """
257 return modify_access(src, dst=dst, port=port, proto=proto, action='delete')
258
259
260def service(name, action):
261 """
262 Open/close access to a service
263
264 :param name: could be a service name defined in `/etc/services` or a port
265 number.
266 :param action: `open` or `close`
267 """
268 if action == 'open':
269 subprocess.check_output(['ufw', 'allow', str(name)],
270 universal_newlines=True)
271 elif action == 'close':
272 subprocess.check_output(['ufw', 'delete', 'allow', str(name)],
273 universal_newlines=True)
274 else:
275 raise UFWError(("'{}' not supported, use 'allow' "
276 "or 'delete'").format(action))
0277
=== added directory 'hooks/charmhelpers/contrib/openstack'
=== added file 'hooks/charmhelpers/contrib/openstack/__init__.py'
--- hooks/charmhelpers/contrib/openstack/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/openstack/__init__.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
016
=== added file 'hooks/charmhelpers/contrib/openstack/alternatives.py'
--- hooks/charmhelpers/contrib/openstack/alternatives.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/openstack/alternatives.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,33 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17''' Helper for managing alternatives for file conflict resolution '''
18
19import subprocess
20import shutil
21import os
22
23
24def install_alternative(name, target, source, priority=50):
25 ''' Install alternative configuration '''
26 if (os.path.exists(target) and not os.path.islink(target)):
27 # Move existing file/directory away before installing
28 shutil.move(target, '{}.bak'.format(target))
29 cmd = [
30 'update-alternatives', '--force', '--install',
31 target, name, source, str(priority)
32 ]
33 subprocess.check_call(cmd)
034
=== added directory 'hooks/charmhelpers/contrib/openstack/amulet'
=== added file 'hooks/charmhelpers/contrib/openstack/amulet/__init__.py'
--- hooks/charmhelpers/contrib/openstack/amulet/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/openstack/amulet/__init__.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,15 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
016
=== added file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py'
--- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,108 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import six
18from charmhelpers.contrib.amulet.deployment import (
19 AmuletDeployment
20)
21
22
23class OpenStackAmuletDeployment(AmuletDeployment):
24 """OpenStack amulet deployment.
25
26 This class inherits from AmuletDeployment and has additional support
27 that is specifically for use by OpenStack charms.
28 """
29
30 def __init__(self, series=None, openstack=None, source=None, stable=True):
31 """Initialize the deployment environment."""
32 super(OpenStackAmuletDeployment, self).__init__(series)
33 self.openstack = openstack
34 self.source = source
35 self.stable = stable
36 # Note(coreycb): this needs to be changed when new next branches come
37 # out.
38 self.current_next = "trusty"
39
40 def _determine_branch_locations(self, other_services):
41 """Determine the branch locations for the other services.
42
43 Determine if the local branch being tested is derived from its
44 stable or next (dev) branch, and based on this, use the corresonding
45 stable or next branches for the other_services."""
46 base_charms = ['mysql', 'mongodb', 'rabbitmq-server']
47
48 if self.stable:
49 for svc in other_services:
50 temp = 'lp:charms/{}'
51 svc['location'] = temp.format(svc['name'])
52 else:
53 for svc in other_services:
54 if svc['name'] in base_charms:
55 temp = 'lp:charms/{}'
56 svc['location'] = temp.format(svc['name'])
57 else:
58 temp = 'lp:~openstack-charmers/charms/{}/{}/next'
59 svc['location'] = temp.format(self.current_next,
60 svc['name'])
61 return other_services
62
63 def _add_services(self, this_service, other_services):
64 """Add services to the deployment and set openstack-origin/source."""
65 other_services = self._determine_branch_locations(other_services)
66
67 super(OpenStackAmuletDeployment, self)._add_services(this_service,
68 other_services)
69
70 services = other_services
71 services.append(this_service)
72 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
73 'ceph-osd', 'ceph-radosgw']
74
75 if self.openstack:
76 for svc in services:
77 if svc['name'] not in use_source:
78 config = {'openstack-origin': self.openstack}
79 self.d.configure(svc['name'], config)
80
81 if self.source:
82 for svc in services:
83 if svc['name'] in use_source:
84 config = {'source': self.source}
85 self.d.configure(svc['name'], config)
86
87 def _configure_services(self, configs):
88 """Configure all of the services."""
89 for service, config in six.iteritems(configs):
90 self.d.configure(service, config)
91
92 def _get_openstack_release(self):
93 """Get openstack release.
94
95 Return an integer representing the enum value of the openstack
96 release.
97 """
98 (self.precise_essex, self.precise_folsom, self.precise_grizzly,
99 self.precise_havana, self.precise_icehouse,
100 self.trusty_icehouse) = range(6)
101 releases = {
102 ('precise', None): self.precise_essex,
103 ('precise', 'cloud:precise-folsom'): self.precise_folsom,
104 ('precise', 'cloud:precise-grizzly'): self.precise_grizzly,
105 ('precise', 'cloud:precise-havana'): self.precise_havana,
106 ('precise', 'cloud:precise-icehouse'): self.precise_icehouse,
107 ('trusty', None): self.trusty_icehouse}
108 return releases[(self.series, self.openstack)]
0109
=== added file 'hooks/charmhelpers/contrib/openstack/amulet/utils.py'
--- hooks/charmhelpers/contrib/openstack/amulet/utils.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,294 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import logging
18import os
19import time
20import urllib
21
22import glanceclient.v1.client as glance_client
23import keystoneclient.v2_0 as keystone_client
24import novaclient.v1_1.client as nova_client
25
26import six
27
28from charmhelpers.contrib.amulet.utils import (
29 AmuletUtils
30)
31
32DEBUG = logging.DEBUG
33ERROR = logging.ERROR
34
35
36class OpenStackAmuletUtils(AmuletUtils):
37 """OpenStack amulet utilities.
38
39 This class inherits from AmuletUtils and has additional support
40 that is specifically for use by OpenStack charms.
41 """
42
43 def __init__(self, log_level=ERROR):
44 """Initialize the deployment environment."""
45 super(OpenStackAmuletUtils, self).__init__(log_level)
46
47 def validate_endpoint_data(self, endpoints, admin_port, internal_port,
48 public_port, expected):
49 """Validate endpoint data.
50
51 Validate actual endpoint data vs expected endpoint data. The ports
52 are used to find the matching endpoint.
53 """
54 found = False
55 for ep in endpoints:
56 self.log.debug('endpoint: {}'.format(repr(ep)))
57 if (admin_port in ep.adminurl and
58 internal_port in ep.internalurl and
59 public_port in ep.publicurl):
60 found = True
61 actual = {'id': ep.id,
62 'region': ep.region,
63 'adminurl': ep.adminurl,
64 'internalurl': ep.internalurl,
65 'publicurl': ep.publicurl,
66 'service_id': ep.service_id}
67 ret = self._validate_dict_data(expected, actual)
68 if ret:
69 return 'unexpected endpoint data - {}'.format(ret)
70
71 if not found:
72 return 'endpoint not found'
73
74 def validate_svc_catalog_endpoint_data(self, expected, actual):
75 """Validate service catalog endpoint data.
76
77 Validate a list of actual service catalog endpoints vs a list of
78 expected service catalog endpoints.
79 """
80 self.log.debug('actual: {}'.format(repr(actual)))
81 for k, v in six.iteritems(expected):
82 if k in actual:
83 ret = self._validate_dict_data(expected[k][0], actual[k][0])
84 if ret:
85 return self.endpoint_error(k, ret)
86 else:
87 return "endpoint {} does not exist".format(k)
88 return ret
89
90 def validate_tenant_data(self, expected, actual):
91 """Validate tenant data.
92
93 Validate a list of actual tenant data vs list of expected tenant
94 data.
95 """
96 self.log.debug('actual: {}'.format(repr(actual)))
97 for e in expected:
98 found = False
99 for act in actual:
100 a = {'enabled': act.enabled, 'description': act.description,
101 'name': act.name, 'id': act.id}
102 if e['name'] == a['name']:
103 found = True
104 ret = self._validate_dict_data(e, a)
105 if ret:
106 return "unexpected tenant data - {}".format(ret)
107 if not found:
108 return "tenant {} does not exist".format(e['name'])
109 return ret
110
111 def validate_role_data(self, expected, actual):
112 """Validate role data.
113
114 Validate a list of actual role data vs a list of expected role
115 data.
116 """
117 self.log.debug('actual: {}'.format(repr(actual)))
118 for e in expected:
119 found = False
120 for act in actual:
121 a = {'name': act.name, 'id': act.id}
122 if e['name'] == a['name']:
123 found = True
124 ret = self._validate_dict_data(e, a)
125 if ret:
126 return "unexpected role data - {}".format(ret)
127 if not found:
128 return "role {} does not exist".format(e['name'])
129 return ret
130
131 def validate_user_data(self, expected, actual):
132 """Validate user data.
133
134 Validate a list of actual user data vs a list of expected user
135 data.
136 """
137 self.log.debug('actual: {}'.format(repr(actual)))
138 for e in expected:
139 found = False
140 for act in actual:
141 a = {'enabled': act.enabled, 'name': act.name,
142 'email': act.email, 'tenantId': act.tenantId,
143 'id': act.id}
144 if e['name'] == a['name']:
145 found = True
146 ret = self._validate_dict_data(e, a)
147 if ret:
148 return "unexpected user data - {}".format(ret)
149 if not found:
150 return "user {} does not exist".format(e['name'])
151 return ret
152
153 def validate_flavor_data(self, expected, actual):
154 """Validate flavor data.
155
156 Validate a list of actual flavors vs a list of expected flavors.
157 """
158 self.log.debug('actual: {}'.format(repr(actual)))
159 act = [a.name for a in actual]
160 return self._validate_list_data(expected, act)
161
162 def tenant_exists(self, keystone, tenant):
163 """Return True if tenant exists."""
164 return tenant in [t.name for t in keystone.tenants.list()]
165
166 def authenticate_keystone_admin(self, keystone_sentry, user, password,
167 tenant):
168 """Authenticates admin user with the keystone admin endpoint."""
169 unit = keystone_sentry
170 service_ip = unit.relation('shared-db',
171 'mysql:shared-db')['private-address']
172 ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))
173 return keystone_client.Client(username=user, password=password,
174 tenant_name=tenant, auth_url=ep)
175
176 def authenticate_keystone_user(self, keystone, user, password, tenant):
177 """Authenticates a regular user with the keystone public endpoint."""
178 ep = keystone.service_catalog.url_for(service_type='identity',
179 endpoint_type='publicURL')
180 return keystone_client.Client(username=user, password=password,
181 tenant_name=tenant, auth_url=ep)
182
183 def authenticate_glance_admin(self, keystone):
184 """Authenticates admin user with glance."""
185 ep = keystone.service_catalog.url_for(service_type='image',
186 endpoint_type='adminURL')
187 return glance_client.Client(ep, token=keystone.auth_token)
188
189 def authenticate_nova_user(self, keystone, user, password, tenant):
190 """Authenticates a regular user with nova-api."""
191 ep = keystone.service_catalog.url_for(service_type='identity',
192 endpoint_type='publicURL')
193 return nova_client.Client(username=user, api_key=password,
194 project_id=tenant, auth_url=ep)
195
196 def create_cirros_image(self, glance, image_name):
197 """Download the latest cirros image and upload it to glance."""
198 http_proxy = os.getenv('AMULET_HTTP_PROXY')
199 self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
200 if http_proxy:
201 proxies = {'http': http_proxy}
202 opener = urllib.FancyURLopener(proxies)
203 else:
204 opener = urllib.FancyURLopener()
205
206 f = opener.open("http://download.cirros-cloud.net/version/released")
207 version = f.read().strip()
208 cirros_img = "cirros-{}-x86_64-disk.img".format(version)
209 local_path = os.path.join('tests', cirros_img)
210
211 if not os.path.exists(local_path):
212 cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",
213 version, cirros_img)
214 opener.retrieve(cirros_url, local_path)
215 f.close()
216
217 with open(local_path) as f:
218 image = glance.images.create(name=image_name, is_public=True,
219 disk_format='qcow2',
220 container_format='bare', data=f)
221 count = 1
222 status = image.status
223 while status != 'active' and count < 10:
224 time.sleep(3)
225 image = glance.images.get(image.id)
226 status = image.status
227 self.log.debug('image status: {}'.format(status))
228 count += 1
229
230 if status != 'active':
231 self.log.error('image creation timed out')
232 return None
233
234 return image
235
236 def delete_image(self, glance, image):
237 """Delete the specified image."""
238 num_before = len(list(glance.images.list()))
239 glance.images.delete(image)
240
241 count = 1
242 num_after = len(list(glance.images.list()))
243 while num_after != (num_before - 1) and count < 10:
244 time.sleep(3)
245 num_after = len(list(glance.images.list()))
246 self.log.debug('number of images: {}'.format(num_after))
247 count += 1
248
249 if num_after != (num_before - 1):
250 self.log.error('image deletion timed out')
251 return False
252
253 return True
254
255 def create_instance(self, nova, image_name, instance_name, flavor):
256 """Create the specified instance."""
257 image = nova.images.find(name=image_name)
258 flavor = nova.flavors.find(name=flavor)
259 instance = nova.servers.create(name=instance_name, image=image,
260 flavor=flavor)
261
262 count = 1
263 status = instance.status
264 while status != 'ACTIVE' and count < 60:
265 time.sleep(3)
266 instance = nova.servers.get(instance.id)
267 status = instance.status
268 self.log.debug('instance status: {}'.format(status))
269 count += 1
270
271 if status != 'ACTIVE':
272 self.log.error('instance creation timed out')
273 return None
274
275 return instance
276
277 def delete_instance(self, nova, instance):
278 """Delete the specified instance."""
279 num_before = len(list(nova.servers.list()))
280 nova.servers.delete(instance)
281
282 count = 1
283 num_after = len(list(nova.servers.list()))
284 while num_after != (num_before - 1) and count < 10:
285 time.sleep(3)
286 num_after = len(list(nova.servers.list()))
287 self.log.debug('number of instances: {}'.format(num_after))
288 count += 1
289
290 if num_after != (num_before - 1):
291 self.log.error('instance deletion timed out')
292 return False
293
294 return True
0295
=== added file 'hooks/charmhelpers/contrib/openstack/context.py'
--- hooks/charmhelpers/contrib/openstack/context.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/openstack/context.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,1054 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import json
18import os
19import time
20from base64 import b64decode
21from subprocess import check_call
22
23import six
24
25from charmhelpers.fetch import (
26 apt_install,
27 filter_installed_packages,
28)
29from charmhelpers.core.hookenv import (
30 config,
31 is_relation_made,
32 local_unit,
33 log,
34 relation_get,
35 relation_ids,
36 related_units,
37 relation_set,
38 unit_get,
39 unit_private_ip,
40 charm_name,
41 DEBUG,
42 INFO,
43 WARNING,
44 ERROR,
45)
46
47from charmhelpers.core.sysctl import create as sysctl_create
48
49from charmhelpers.core.host import (
50 mkdir,
51 write_file,
52)
53from charmhelpers.contrib.hahelpers.cluster import (
54 determine_apache_port,
55 determine_api_port,
56 https,
57 is_clustered,
58)
59from charmhelpers.contrib.hahelpers.apache import (
60 get_cert,
61 get_ca_cert,
62 install_ca_cert,
63)
64from charmhelpers.contrib.openstack.neutron import (
65 neutron_plugin_attribute,
66)
67from charmhelpers.contrib.network.ip import (
68 get_address_in_network,
69 get_ipv6_addr,
70 get_netmask_for_address,
71 format_ipv6_addr,
72 is_address_in_network,
73)
74from charmhelpers.contrib.openstack.utils import get_host_ip
75
76CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
77ADDRESS_TYPES = ['admin', 'internal', 'public']
78
79
80class OSContextError(Exception):
81 pass
82
83
84def ensure_packages(packages):
85 """Install but do not upgrade required plugin packages."""
86 required = filter_installed_packages(packages)
87 if required:
88 apt_install(required, fatal=True)
89
90
91def context_complete(ctxt):
92 _missing = []
93 for k, v in six.iteritems(ctxt):
94 if v is None or v == '':
95 _missing.append(k)
96
97 if _missing:
98 log('Missing required data: %s' % ' '.join(_missing), level=INFO)
99 return False
100
101 return True
102
103
104def config_flags_parser(config_flags):
105 """Parses config flags string into dict.
106
107 The provided config_flags string may be a list of comma-separated values
108 which themselves may be comma-separated list of values.
109 """
110 if config_flags.find('==') >= 0:
111 log("config_flags is not in expected format (key=value)", level=ERROR)
112 raise OSContextError
113
114 # strip the following from each value.
115 post_strippers = ' ,'
116 # we strip any leading/trailing '=' or ' ' from the string then
117 # split on '='.
118 split = config_flags.strip(' =').split('=')
119 limit = len(split)
120 flags = {}
121 for i in range(0, limit - 1):
122 current = split[i]
123 next = split[i + 1]
124 vindex = next.rfind(',')
125 if (i == limit - 2) or (vindex < 0):
126 value = next
127 else:
128 value = next[:vindex]
129
130 if i == 0:
131 key = current
132 else:
133 # if this not the first entry, expect an embedded key.
134 index = current.rfind(',')
135 if index < 0:
136 log("Invalid config value(s) at index %s" % (i), level=ERROR)
137 raise OSContextError
138 key = current[index + 1:]
139
140 # Add to collection.
141 flags[key.strip(post_strippers)] = value.rstrip(post_strippers)
142
143 return flags
144
145
146class OSContextGenerator(object):
147 """Base class for all context generators."""
148 interfaces = []
149
150 def __call__(self):
151 raise NotImplementedError
152
153
154class SharedDBContext(OSContextGenerator):
155 interfaces = ['shared-db']
156
157 def __init__(self,
158 database=None, user=None, relation_prefix=None, ssl_dir=None):
159 """Allows inspecting relation for settings prefixed with
160 relation_prefix. This is useful for parsing access for multiple
161 databases returned via the shared-db interface (eg, nova_password,
162 quantum_password)
163 """
164 self.relation_prefix = relation_prefix
165 self.database = database
166 self.user = user
167 self.ssl_dir = ssl_dir
168
169 def __call__(self):
170 self.database = self.database or config('database')
171 self.user = self.user or config('database-user')
172 if None in [self.database, self.user]:
173 log("Could not generate shared_db context. Missing required charm "
174 "config options. (database name and user)", level=ERROR)
175 raise OSContextError
176
177 ctxt = {}
178
179 # NOTE(jamespage) if mysql charm provides a network upon which
180 # access to the database should be made, reconfigure relation
181 # with the service units local address and defer execution
182 access_network = relation_get('access-network')
183 if access_network is not None:
184 if self.relation_prefix is not None:
185 hostname_key = "{}_hostname".format(self.relation_prefix)
186 else:
187 hostname_key = "hostname"
188 access_hostname = get_address_in_network(access_network,
189 unit_get('private-address'))
190 set_hostname = relation_get(attribute=hostname_key,
191 unit=local_unit())
192 if set_hostname != access_hostname:
193 relation_set(relation_settings={hostname_key: access_hostname})
194 return ctxt # Defer any further hook execution for now....
195
196 password_setting = 'password'
197 if self.relation_prefix:
198 password_setting = self.relation_prefix + '_password'
199
200 for rid in relation_ids('shared-db'):
201 for unit in related_units(rid):
202 rdata = relation_get(rid=rid, unit=unit)
203 host = rdata.get('db_host')
204 host = format_ipv6_addr(host) or host
205 ctxt = {
206 'database_host': host,
207 'database': self.database,
208 'database_user': self.user,
209 'database_password': rdata.get(password_setting),
210 'database_type': 'mysql'
211 }
212 if context_complete(ctxt):
213 db_ssl(rdata, ctxt, self.ssl_dir)
214 return ctxt
215 return {}
216
217
218class PostgresqlDBContext(OSContextGenerator):
219 interfaces = ['pgsql-db']
220
221 def __init__(self, database=None):
222 self.database = database
223
224 def __call__(self):
225 self.database = self.database or config('database')
226 if self.database is None:
227 log('Could not generate postgresql_db context. Missing required '
228 'charm config options. (database name)', level=ERROR)
229 raise OSContextError
230
231 ctxt = {}
232 for rid in relation_ids(self.interfaces[0]):
233 for unit in related_units(rid):
234 rel_host = relation_get('host', rid=rid, unit=unit)
235 rel_user = relation_get('user', rid=rid, unit=unit)
236 rel_passwd = relation_get('password', rid=rid, unit=unit)
237 ctxt = {'database_host': rel_host,
238 'database': self.database,
239 'database_user': rel_user,
240 'database_password': rel_passwd,
241 'database_type': 'postgresql'}
242 if context_complete(ctxt):
243 return ctxt
244
245 return {}
246
247
248def db_ssl(rdata, ctxt, ssl_dir):
249 if 'ssl_ca' in rdata and ssl_dir:
250 ca_path = os.path.join(ssl_dir, 'db-client.ca')
251 with open(ca_path, 'w') as fh:
252 fh.write(b64decode(rdata['ssl_ca']))
253
254 ctxt['database_ssl_ca'] = ca_path
255 elif 'ssl_ca' in rdata:
256 log("Charm not setup for ssl support but ssl ca found", level=INFO)
257 return ctxt
258
259 if 'ssl_cert' in rdata:
260 cert_path = os.path.join(
261 ssl_dir, 'db-client.cert')
262 if not os.path.exists(cert_path):
263 log("Waiting 1m for ssl client cert validity", level=INFO)
264 time.sleep(60)
265
266 with open(cert_path, 'w') as fh:
267 fh.write(b64decode(rdata['ssl_cert']))
268
269 ctxt['database_ssl_cert'] = cert_path
270 key_path = os.path.join(ssl_dir, 'db-client.key')
271 with open(key_path, 'w') as fh:
272 fh.write(b64decode(rdata['ssl_key']))
273
274 ctxt['database_ssl_key'] = key_path
275
276 return ctxt
277
278
279class IdentityServiceContext(OSContextGenerator):
280 interfaces = ['identity-service']
281
282 def __call__(self):
283 log('Generating template context for identity-service', level=DEBUG)
284 ctxt = {}
285 for rid in relation_ids('identity-service'):
286 for unit in related_units(rid):
287 rdata = relation_get(rid=rid, unit=unit)
288 serv_host = rdata.get('service_host')
289 serv_host = format_ipv6_addr(serv_host) or serv_host
290 auth_host = rdata.get('auth_host')
291 auth_host = format_ipv6_addr(auth_host) or auth_host
292 svc_protocol = rdata.get('service_protocol') or 'http'
293 auth_protocol = rdata.get('auth_protocol') or 'http'
294 ctxt = {'service_port': rdata.get('service_port'),
295 'service_host': serv_host,
296 'auth_host': auth_host,
297 'auth_port': rdata.get('auth_port'),
298 'admin_tenant_name': rdata.get('service_tenant'),
299 'admin_user': rdata.get('service_username'),
300 'admin_password': rdata.get('service_password'),
301 'service_protocol': svc_protocol,
302 'auth_protocol': auth_protocol}
303 if context_complete(ctxt):
304 # NOTE(jamespage) this is required for >= icehouse
305 # so a missing value just indicates keystone needs
306 # upgrading
307 ctxt['admin_tenant_id'] = rdata.get('service_tenant_id')
308 return ctxt
309
310 return {}
311
312
313class AMQPContext(OSContextGenerator):
314
315 def __init__(self, ssl_dir=None, rel_name='amqp', relation_prefix=None):
316 self.ssl_dir = ssl_dir
317 self.rel_name = rel_name
318 self.relation_prefix = relation_prefix
319 self.interfaces = [rel_name]
320
321 def __call__(self):
322 log('Generating template context for amqp', level=DEBUG)
323 conf = config()
324 if self.relation_prefix:
325 user_setting = '%s-rabbit-user' % (self.relation_prefix)
326 vhost_setting = '%s-rabbit-vhost' % (self.relation_prefix)
327 else:
328 user_setting = 'rabbit-user'
329 vhost_setting = 'rabbit-vhost'
330
331 try:
332 username = conf[user_setting]
333 vhost = conf[vhost_setting]
334 except KeyError as e:
335 log('Could not generate shared_db context. Missing required charm '
336 'config options: %s.' % e, level=ERROR)
337 raise OSContextError
338
339 ctxt = {}
340 for rid in relation_ids(self.rel_name):
341 ha_vip_only = False
342 for unit in related_units(rid):
343 if relation_get('clustered', rid=rid, unit=unit):
344 ctxt['clustered'] = True
345 vip = relation_get('vip', rid=rid, unit=unit)
346 vip = format_ipv6_addr(vip) or vip
347 ctxt['rabbitmq_host'] = vip
348 else:
349 host = relation_get('private-address', rid=rid, unit=unit)
350 host = format_ipv6_addr(host) or host
351 ctxt['rabbitmq_host'] = host
352
353 ctxt.update({
354 'rabbitmq_user': username,
355 'rabbitmq_password': relation_get('password', rid=rid,
356 unit=unit),
357 'rabbitmq_virtual_host': vhost,
358 })
359
360 ssl_port = relation_get('ssl_port', rid=rid, unit=unit)
361 if ssl_port:
362 ctxt['rabbit_ssl_port'] = ssl_port
363
364 ssl_ca = relation_get('ssl_ca', rid=rid, unit=unit)
365 if ssl_ca:
366 ctxt['rabbit_ssl_ca'] = ssl_ca
367
368 if relation_get('ha_queues', rid=rid, unit=unit) is not None:
369 ctxt['rabbitmq_ha_queues'] = True
370
371 ha_vip_only = relation_get('ha-vip-only',
372 rid=rid, unit=unit) is not None
373
374 if context_complete(ctxt):
375 if 'rabbit_ssl_ca' in ctxt:
376 if not self.ssl_dir:
377 log("Charm not setup for ssl support but ssl ca "
378 "found", level=INFO)
379 break
380
381 ca_path = os.path.join(
382 self.ssl_dir, 'rabbit-client-ca.pem')
383 with open(ca_path, 'w') as fh:
384 fh.write(b64decode(ctxt['rabbit_ssl_ca']))
385 ctxt['rabbit_ssl_ca'] = ca_path
386
387 # Sufficient information found = break out!
388 break
389
390 # Used for active/active rabbitmq >= grizzly
391 if (('clustered' not in ctxt or ha_vip_only) and
392 len(related_units(rid)) > 1):
393 rabbitmq_hosts = []
394 for unit in related_units(rid):
395 host = relation_get('private-address', rid=rid, unit=unit)
396 host = format_ipv6_addr(host) or host
397 rabbitmq_hosts.append(host)
398
399 ctxt['rabbitmq_hosts'] = ','.join(sorted(rabbitmq_hosts))
400
401 if not context_complete(ctxt):
402 return {}
403
404 return ctxt
405
406
407class CephContext(OSContextGenerator):
408 """Generates context for /etc/ceph/ceph.conf templates."""
409 interfaces = ['ceph']
410
411 def __call__(self):
412 if not relation_ids('ceph'):
413 return {}
414
415 log('Generating template context for ceph', level=DEBUG)
416 mon_hosts = []
417 auth = None
418 key = None
419 use_syslog = str(config('use-syslog')).lower()
420 for rid in relation_ids('ceph'):
421 for unit in related_units(rid):
422 auth = relation_get('auth', rid=rid, unit=unit)
423 key = relation_get('key', rid=rid, unit=unit)
424 ceph_pub_addr = relation_get('ceph-public-address', rid=rid,
425 unit=unit)
426 unit_priv_addr = relation_get('private-address', rid=rid,
427 unit=unit)
428 ceph_addr = ceph_pub_addr or unit_priv_addr
429 ceph_addr = format_ipv6_addr(ceph_addr) or ceph_addr
430 mon_hosts.append(ceph_addr)
431
432 ctxt = {'mon_hosts': ' '.join(sorted(mon_hosts)),
433 'auth': auth,
434 'key': key,
435 'use_syslog': use_syslog}
436
437 if not os.path.isdir('/etc/ceph'):
438 os.mkdir('/etc/ceph')
439
440 if not context_complete(ctxt):
441 return {}
442
443 ensure_packages(['ceph-common'])
444 return ctxt
445
446
447class HAProxyContext(OSContextGenerator):
448 """Provides half a context for the haproxy template, which describes
449 all peers to be included in the cluster. Each charm needs to include
450 its own context generator that describes the port mapping.
451 """
452 interfaces = ['cluster']
453
454 def __init__(self, singlenode_mode=False):
455 self.singlenode_mode = singlenode_mode
456
457 def __call__(self):
458 if not relation_ids('cluster') and not self.singlenode_mode:
459 return {}
460
461 if config('prefer-ipv6'):
462 addr = get_ipv6_addr(exc_list=[config('vip')])[0]
463 else:
464 addr = get_host_ip(unit_get('private-address'))
465
466 l_unit = local_unit().replace('/', '-')
467 cluster_hosts = {}
468
469 # NOTE(jamespage): build out map of configured network endpoints
470 # and associated backends
471 for addr_type in ADDRESS_TYPES:
472 cfg_opt = 'os-{}-network'.format(addr_type)
473 laddr = get_address_in_network(config(cfg_opt))
474 if laddr:
475 netmask = get_netmask_for_address(laddr)
476 cluster_hosts[laddr] = {'network': "{}/{}".format(laddr,
477 netmask),
478 'backends': {l_unit: laddr}}
479 for rid in relation_ids('cluster'):
480 for unit in related_units(rid):
481 _laddr = relation_get('{}-address'.format(addr_type),
482 rid=rid, unit=unit)
483 if _laddr:
484 _unit = unit.replace('/', '-')
485 cluster_hosts[laddr]['backends'][_unit] = _laddr
486
487 # NOTE(jamespage) add backend based on private address - this
488 # with either be the only backend or the fallback if no acls
489 # match in the frontend
490 cluster_hosts[addr] = {}
491 netmask = get_netmask_for_address(addr)
492 cluster_hosts[addr] = {'network': "{}/{}".format(addr, netmask),
493 'backends': {l_unit: addr}}
494 for rid in relation_ids('cluster'):
495 for unit in related_units(rid):
496 _laddr = relation_get('private-address',
497 rid=rid, unit=unit)
498 if _laddr:
499 _unit = unit.replace('/', '-')
500 cluster_hosts[addr]['backends'][_unit] = _laddr
501
502 ctxt = {
503 'frontends': cluster_hosts,
504 'default_backend': addr
505 }
506
507 if config('haproxy-server-timeout'):
508 ctxt['haproxy_server_timeout'] = config('haproxy-server-timeout')
509
510 if config('haproxy-client-timeout'):
511 ctxt['haproxy_client_timeout'] = config('haproxy-client-timeout')
512
513 if config('prefer-ipv6'):
514 ctxt['ipv6'] = True
515 ctxt['local_host'] = 'ip6-localhost'
516 ctxt['haproxy_host'] = '::'
517 ctxt['stat_port'] = ':::8888'
518 else:
519 ctxt['local_host'] = '127.0.0.1'
520 ctxt['haproxy_host'] = '0.0.0.0'
521 ctxt['stat_port'] = ':8888'
522
523 for frontend in cluster_hosts:
524 if (len(cluster_hosts[frontend]['backends']) > 1 or
525 self.singlenode_mode):
526 # Enable haproxy when we have enough peers.
527 log('Ensuring haproxy enabled in /etc/default/haproxy.',
528 level=DEBUG)
529 with open('/etc/default/haproxy', 'w') as out:
530 out.write('ENABLED=1\n')
531
532 return ctxt
533
534 log('HAProxy context is incomplete, this unit has no peers.',
535 level=INFO)
536 return {}
537
538
539class ImageServiceContext(OSContextGenerator):
540 interfaces = ['image-service']
541
542 def __call__(self):
543 """Obtains the glance API server from the image-service relation.
544 Useful in nova and cinder (currently).
545 """
546 log('Generating template context for image-service.', level=DEBUG)
547 rids = relation_ids('image-service')
548 if not rids:
549 return {}
550
551 for rid in rids:
552 for unit in related_units(rid):
553 api_server = relation_get('glance-api-server',
554 rid=rid, unit=unit)
555 if api_server:
556 return {'glance_api_servers': api_server}
557
558 log("ImageService context is incomplete. Missing required relation "
559 "data.", level=INFO)
560 return {}
561
562
563class ApacheSSLContext(OSContextGenerator):
564 """Generates a context for an apache vhost configuration that configures
565 HTTPS reverse proxying for one or many endpoints. Generated context
566 looks something like::
567
568 {
569 'namespace': 'cinder',
570 'private_address': 'iscsi.mycinderhost.com',
571 'endpoints': [(8776, 8766), (8777, 8767)]
572 }
573
574 The endpoints list consists of a tuples mapping external ports
575 to internal ports.
576 """
577 interfaces = ['https']
578
579 # charms should inherit this context and set external ports
580 # and service namespace accordingly.
581 external_ports = []
582 service_namespace = None
583
584 def enable_modules(self):
585 cmd = ['a2enmod', 'ssl', 'proxy', 'proxy_http']
586 check_call(cmd)
587
588 def configure_cert(self, cn=None):
589 ssl_dir = os.path.join('/etc/apache2/ssl/', self.service_namespace)
590 mkdir(path=ssl_dir)
591 cert, key = get_cert(cn)
592 if cn:
593 cert_filename = 'cert_{}'.format(cn)
594 key_filename = 'key_{}'.format(cn)
595 else:
596 cert_filename = 'cert'
597 key_filename = 'key'
598
599 write_file(path=os.path.join(ssl_dir, cert_filename),
600 content=b64decode(cert))
601 write_file(path=os.path.join(ssl_dir, key_filename),
602 content=b64decode(key))
603
604 def configure_ca(self):
605 ca_cert = get_ca_cert()
606 if ca_cert:
607 install_ca_cert(b64decode(ca_cert))
608
609 def canonical_names(self):
610 """Figure out which canonical names clients will access this service.
611 """
612 cns = []
613 for r_id in relation_ids('identity-service'):
614 for unit in related_units(r_id):
615 rdata = relation_get(rid=r_id, unit=unit)
616 for k in rdata:
617 if k.startswith('ssl_key_'):
618 cns.append(k.lstrip('ssl_key_'))
619
620 return sorted(list(set(cns)))
621
622 def get_network_addresses(self):
623 """For each network configured, return corresponding address and vip
624 (if available).
625
626 Returns a list of tuples of the form:
627
628 [(address_in_net_a, vip_in_net_a),
629 (address_in_net_b, vip_in_net_b),
630 ...]
631
632 or, if no vip(s) available:
633
634 [(address_in_net_a, address_in_net_a),
635 (address_in_net_b, address_in_net_b),
636 ...]
637 """
638 addresses = []
639 if config('vip'):
640 vips = config('vip').split()
641 else:
642 vips = []
643
644 for net_type in ['os-internal-network', 'os-admin-network',
645 'os-public-network']:
646 addr = get_address_in_network(config(net_type),
647 unit_get('private-address'))
648 if len(vips) > 1 and is_clustered():
649 if not config(net_type):
650 log("Multiple networks configured but net_type "
651 "is None (%s)." % net_type, level=WARNING)
652 continue
653
654 for vip in vips:
655 if is_address_in_network(config(net_type), vip):
656 addresses.append((addr, vip))
657 break
658
659 elif is_clustered() and config('vip'):
660 addresses.append((addr, config('vip')))
661 else:
662 addresses.append((addr, addr))
663
664 return sorted(addresses)
665
666 def __call__(self):
667 if isinstance(self.external_ports, six.string_types):
668 self.external_ports = [self.external_ports]
669
670 if not self.external_ports or not https():
671 return {}
672
673 self.configure_ca()
674 self.enable_modules()
675
676 ctxt = {'namespace': self.service_namespace,
677 'endpoints': [],
678 'ext_ports': []}
679
680 for cn in self.canonical_names():
681 self.configure_cert(cn)
682
683 addresses = self.get_network_addresses()
684 for address, endpoint in sorted(set(addresses)):
685 for api_port in self.external_ports:
686 ext_port = determine_apache_port(api_port,
687 singlenode_mode=True)
688 int_port = determine_api_port(api_port, singlenode_mode=True)
689 portmap = (address, endpoint, int(ext_port), int(int_port))
690 ctxt['endpoints'].append(portmap)
691 ctxt['ext_ports'].append(int(ext_port))
692
693 ctxt['ext_ports'] = sorted(list(set(ctxt['ext_ports'])))
694 return ctxt
695
696
697class NeutronContext(OSContextGenerator):
698 interfaces = []
699
700 @property
701 def plugin(self):
702 return None
703
704 @property
705 def network_manager(self):
706 return None
707
708 @property
709 def packages(self):
710 return neutron_plugin_attribute(self.plugin, 'packages',
711 self.network_manager)
712
713 @property
714 def neutron_security_groups(self):
715 return None
716
717 def _ensure_packages(self):
718 for pkgs in self.packages:
719 ensure_packages(pkgs)
720
721 def _save_flag_file(self):
722 if self.network_manager == 'quantum':
723 _file = '/etc/nova/quantum_plugin.conf'
724 else:
725 _file = '/etc/nova/neutron_plugin.conf'
726
727 with open(_file, 'wb') as out:
728 out.write(self.plugin + '\n')
729
730 def ovs_ctxt(self):
731 driver = neutron_plugin_attribute(self.plugin, 'driver',
732 self.network_manager)
733 config = neutron_plugin_attribute(self.plugin, 'config',
734 self.network_manager)
735 ovs_ctxt = {'core_plugin': driver,
736 'neutron_plugin': 'ovs',
737 'neutron_security_groups': self.neutron_security_groups,
738 'local_ip': unit_private_ip(),
739 'config': config}
740
741 return ovs_ctxt
742
743 def nvp_ctxt(self):
744 driver = neutron_plugin_attribute(self.plugin, 'driver',
745 self.network_manager)
746 config = neutron_plugin_attribute(self.plugin, 'config',
747 self.network_manager)
748 nvp_ctxt = {'core_plugin': driver,
749 'neutron_plugin': 'nvp',
750 'neutron_security_groups': self.neutron_security_groups,
751 'local_ip': unit_private_ip(),
752 'config': config}
753
754 return nvp_ctxt
755
756 def n1kv_ctxt(self):
757 driver = neutron_plugin_attribute(self.plugin, 'driver',
758 self.network_manager)
759 n1kv_config = neutron_plugin_attribute(self.plugin, 'config',
760 self.network_manager)
761 n1kv_user_config_flags = config('n1kv-config-flags')
762 restrict_policy_profiles = config('n1kv-restrict-policy-profiles')
763 n1kv_ctxt = {'core_plugin': driver,
764 'neutron_plugin': 'n1kv',
765 'neutron_security_groups': self.neutron_security_groups,
766 'local_ip': unit_private_ip(),
767 'config': n1kv_config,
768 'vsm_ip': config('n1kv-vsm-ip'),
769 'vsm_username': config('n1kv-vsm-username'),
770 'vsm_password': config('n1kv-vsm-password'),
771 'restrict_policy_profiles': restrict_policy_profiles}
772
773 if n1kv_user_config_flags:
774 flags = config_flags_parser(n1kv_user_config_flags)
775 n1kv_ctxt['user_config_flags'] = flags
776
777 return n1kv_ctxt
778
779 def calico_ctxt(self):
780 driver = neutron_plugin_attribute(self.plugin, 'driver',
781 self.network_manager)
782 config = neutron_plugin_attribute(self.plugin, 'config',
783 self.network_manager)
784 calico_ctxt = {'core_plugin': driver,
785 'neutron_plugin': 'Calico',
786 'neutron_security_groups': self.neutron_security_groups,
787 'local_ip': unit_private_ip(),
788 'config': config}
789
790 return calico_ctxt
791
792 def neutron_ctxt(self):
793 if https():
794 proto = 'https'
795 else:
796 proto = 'http'
797
798 if is_clustered():
799 host = config('vip')
800 else:
801 host = unit_get('private-address')
802
803 ctxt = {'network_manager': self.network_manager,
804 'neutron_url': '%s://%s:%s' % (proto, host, '9696')}
805 return ctxt
806
807 def __call__(self):
808 self._ensure_packages()
809
810 if self.network_manager not in ['quantum', 'neutron']:
811 return {}
812
813 if not self.plugin:
814 return {}
815
816 ctxt = self.neutron_ctxt()
817
818 if self.plugin == 'ovs':
819 ctxt.update(self.ovs_ctxt())
820 elif self.plugin in ['nvp', 'nsx']:
821 ctxt.update(self.nvp_ctxt())
822 elif self.plugin == 'n1kv':
823 ctxt.update(self.n1kv_ctxt())
824 elif self.plugin == 'Calico':
825 ctxt.update(self.calico_ctxt())
826
827 alchemy_flags = config('neutron-alchemy-flags')
828 if alchemy_flags:
829 flags = config_flags_parser(alchemy_flags)
830 ctxt['neutron_alchemy_flags'] = flags
831
832 self._save_flag_file()
833 return ctxt
834
835
836class OSConfigFlagContext(OSContextGenerator):
837 """Provides support for user-defined config flags.
838
839 Users can define a comma-seperated list of key=value pairs
840 in the charm configuration and apply them at any point in
841 any file by using a template flag.
842
843 Sometimes users might want config flags inserted within a
844 specific section so this class allows users to specify the
845 template flag name, allowing for multiple template flags
846 (sections) within the same context.
847
848 NOTE: the value of config-flags may be a comma-separated list of
849 key=value pairs and some Openstack config files support
850 comma-separated lists as values.
851 """
852
853 def __init__(self, charm_flag='config-flags',
854 template_flag='user_config_flags'):
855 """
856 :param charm_flag: config flags in charm configuration.
857 :param template_flag: insert point for user-defined flags in template
858 file.
859 """
860 super(OSConfigFlagContext, self).__init__()
861 self._charm_flag = charm_flag
862 self._template_flag = template_flag
863
864 def __call__(self):
865 config_flags = config(self._charm_flag)
866 if not config_flags:
867 return {}
868
869 return {self._template_flag:
870 config_flags_parser(config_flags)}
871
872
873class SubordinateConfigContext(OSContextGenerator):
874
875 """
876 Responsible for inspecting relations to subordinates that
877 may be exporting required config via a json blob.
878
879 The subordinate interface allows subordinates to export their
880 configuration requirements to the principle for multiple config
881 files and multiple serivces. Ie, a subordinate that has interfaces
882 to both glance and nova may export to following yaml blob as json::
883
884 glance:
885 /etc/glance/glance-api.conf:
886 sections:
887 DEFAULT:
888 - [key1, value1]
889 /etc/glance/glance-registry.conf:
890 MYSECTION:
891 - [key2, value2]
892 nova:
893 /etc/nova/nova.conf:
894 sections:
895 DEFAULT:
896 - [key3, value3]
897
898
899 It is then up to the principle charms to subscribe this context to
900 the service+config file it is interestd in. Configuration data will
901 be available in the template context, in glance's case, as::
902
903 ctxt = {
904 ... other context ...
905 'subordinate_config': {
906 'DEFAULT': {
907 'key1': 'value1',
908 },
909 'MYSECTION': {
910 'key2': 'value2',
911 },
912 }
913 }
914 """
915
916 def __init__(self, service, config_file, interface):
917 """
918 :param service : Service name key to query in any subordinate
919 data found
920 :param config_file : Service's config file to query sections
921 :param interface : Subordinate interface to inspect
922 """
923 self.service = service
924 self.config_file = config_file
925 self.interface = interface
926
927 def __call__(self):
928 ctxt = {'sections': {}}
929 for rid in relation_ids(self.interface):
930 for unit in related_units(rid):
931 sub_config = relation_get('subordinate_configuration',
932 rid=rid, unit=unit)
933 if sub_config and sub_config != '':
934 try:
935 sub_config = json.loads(sub_config)
936 except:
937 log('Could not parse JSON from subordinate_config '
938 'setting from %s' % rid, level=ERROR)
939 continue
940
941 if self.service not in sub_config:
942 log('Found subordinate_config on %s but it contained'
943 'nothing for %s service' % (rid, self.service),
944 level=INFO)
945 continue
946
947 sub_config = sub_config[self.service]
948 if self.config_file not in sub_config:
949 log('Found subordinate_config on %s but it contained'
950 'nothing for %s' % (rid, self.config_file),
951 level=INFO)
952 continue
953
954 sub_config = sub_config[self.config_file]
955 for k, v in six.iteritems(sub_config):
956 if k == 'sections':
957 for section, config_dict in six.iteritems(v):
958 log("adding section '%s'" % (section),
959 level=DEBUG)
960 ctxt[k][section] = config_dict
961 else:
962 ctxt[k] = v
963
964 log("%d section(s) found" % (len(ctxt['sections'])), level=DEBUG)
965 return ctxt
966
967
968class LogLevelContext(OSContextGenerator):
969
970 def __call__(self):
971 ctxt = {}
972 ctxt['debug'] = \
973 False if config('debug') is None else config('debug')
974 ctxt['verbose'] = \
975 False if config('verbose') is None else config('verbose')
976
977 return ctxt
978
979
980class SyslogContext(OSContextGenerator):
981
982 def __call__(self):
983 ctxt = {'use_syslog': config('use-syslog')}
984 return ctxt
985
986
987class BindHostContext(OSContextGenerator):
988
989 def __call__(self):
990 if config('prefer-ipv6'):
991 return {'bind_host': '::'}
992 else:
993 return {'bind_host': '0.0.0.0'}
994
995
996class WorkerConfigContext(OSContextGenerator):
997
998 @property
999 def num_cpus(self):
1000 try:
1001 from psutil import NUM_CPUS
1002 except ImportError:
1003 apt_install('python-psutil', fatal=True)
1004 from psutil import NUM_CPUS
1005
1006 return NUM_CPUS
1007
1008 def __call__(self):
1009 multiplier = config('worker-multiplier') or 0
1010 ctxt = {"workers": self.num_cpus * multiplier}
1011 return ctxt
1012
1013
1014class ZeroMQContext(OSContextGenerator):
1015 interfaces = ['zeromq-configuration']
1016
1017 def __call__(self):
1018 ctxt = {}
1019 if is_relation_made('zeromq-configuration', 'host'):
1020 for rid in relation_ids('zeromq-configuration'):
1021 for unit in related_units(rid):
1022 ctxt['zmq_nonce'] = relation_get('nonce', unit, rid)
1023 ctxt['zmq_host'] = relation_get('host', unit, rid)
1024
1025 return ctxt
1026
1027
1028class NotificationDriverContext(OSContextGenerator):
1029
1030 def __init__(self, zmq_relation='zeromq-configuration',
1031 amqp_relation='amqp'):
1032 """
1033 :param zmq_relation: Name of Zeromq relation to check
1034 """
1035 self.zmq_relation = zmq_relation
1036 self.amqp_relation = amqp_relation
1037
1038 def __call__(self):
1039 ctxt = {'notifications': 'False'}
1040 if is_relation_made(self.amqp_relation):
1041 ctxt['notifications'] = "True"
1042
1043 return ctxt
1044
1045
1046class SysctlContext(OSContextGenerator):
1047 """This context check if the 'sysctl' option exists on configuration
1048 then creates a file with the loaded contents"""
1049 def __call__(self):
1050 sysctl_dict = config('sysctl')
1051 if sysctl_dict:
1052 sysctl_create(sysctl_dict,
1053 '/etc/sysctl.d/50-{0}.conf'.format(charm_name()))
1054 return {'sysctl': sysctl_dict}
01055
=== added file 'hooks/charmhelpers/contrib/openstack/ip.py'
--- hooks/charmhelpers/contrib/openstack/ip.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/openstack/ip.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,109 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17from charmhelpers.core.hookenv import (
18 config,
19 unit_get,
20)
21from charmhelpers.contrib.network.ip import (
22 get_address_in_network,
23 is_address_in_network,
24 is_ipv6,
25 get_ipv6_addr,
26)
27from charmhelpers.contrib.hahelpers.cluster import is_clustered
28
29PUBLIC = 'public'
30INTERNAL = 'int'
31ADMIN = 'admin'
32
33ADDRESS_MAP = {
34 PUBLIC: {
35 'config': 'os-public-network',
36 'fallback': 'public-address'
37 },
38 INTERNAL: {
39 'config': 'os-internal-network',
40 'fallback': 'private-address'
41 },
42 ADMIN: {
43 'config': 'os-admin-network',
44 'fallback': 'private-address'
45 }
46}
47
48
49def canonical_url(configs, endpoint_type=PUBLIC):
50 """Returns the correct HTTP URL to this host given the state of HTTPS
51 configuration, hacluster and charm configuration.
52
53 :param configs: OSTemplateRenderer config templating object to inspect
54 for a complete https context.
55 :param endpoint_type: str endpoint type to resolve.
56 :param returns: str base URL for services on the current service unit.
57 """
58 scheme = 'http'
59 if 'https' in configs.complete_contexts():
60 scheme = 'https'
61 address = resolve_address(endpoint_type)
62 if is_ipv6(address):
63 address = "[{}]".format(address)
64 return '%s://%s' % (scheme, address)
65
66
67def resolve_address(endpoint_type=PUBLIC):
68 """Return unit address depending on net config.
69
70 If unit is clustered with vip(s) and has net splits defined, return vip on
71 correct network. If clustered with no nets defined, return primary vip.
72
73 If not clustered, return unit address ensuring address is on configured net
74 split if one is configured.
75
76 :param endpoint_type: Network endpoing type
77 """
78 resolved_address = None
79 vips = config('vip')
80 if vips:
81 vips = vips.split()
82
83 net_type = ADDRESS_MAP[endpoint_type]['config']
84 net_addr = config(net_type)
85 net_fallback = ADDRESS_MAP[endpoint_type]['fallback']
86 clustered = is_clustered()
87 if clustered:
88 if not net_addr:
89 # If no net-splits defined, we expect a single vip
90 resolved_address = vips[0]
91 else:
92 for vip in vips:
93 if is_address_in_network(net_addr, vip):
94 resolved_address = vip
95 break
96 else:
97 if config('prefer-ipv6'):
98 fallback_addr = get_ipv6_addr(exc_list=vips)[0]
99 else:
100 fallback_addr = unit_get(net_fallback)
101
102 resolved_address = get_address_in_network(net_addr, fallback_addr)
103
104 if resolved_address is None:
105 raise ValueError("Unable to resolve a suitable IP address based on "
106 "charm state and configuration. (net_type=%s, "
107 "clustered=%s)" % (net_type, clustered))
108
109 return resolved_address
0110
=== added file 'hooks/charmhelpers/contrib/openstack/neutron.py'
--- hooks/charmhelpers/contrib/openstack/neutron.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/openstack/neutron.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,239 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17# Various utilies for dealing with Neutron and the renaming from Quantum.
18
19from subprocess import check_output
20
21from charmhelpers.core.hookenv import (
22 config,
23 log,
24 ERROR,
25)
26
27from charmhelpers.contrib.openstack.utils import os_release
28
29
30def headers_package():
31 """Ensures correct linux-headers for running kernel are installed,
32 for building DKMS package"""
33 kver = check_output(['uname', '-r']).decode('UTF-8').strip()
34 return 'linux-headers-%s' % kver
35
36QUANTUM_CONF_DIR = '/etc/quantum'
37
38
39def kernel_version():
40 """ Retrieve the current major kernel version as a tuple e.g. (3, 13) """
41 kver = check_output(['uname', '-r']).decode('UTF-8').strip()
42 kver = kver.split('.')
43 return (int(kver[0]), int(kver[1]))
44
45
46def determine_dkms_package():
47 """ Determine which DKMS package should be used based on kernel version """
48 # NOTE: 3.13 kernels have support for GRE and VXLAN native
49 if kernel_version() >= (3, 13):
50 return []
51 else:
52 return ['openvswitch-datapath-dkms']
53
54
55# legacy
56
57
58def quantum_plugins():
59 from charmhelpers.contrib.openstack import context
60 return {
61 'ovs': {
62 'config': '/etc/quantum/plugins/openvswitch/'
63 'ovs_quantum_plugin.ini',
64 'driver': 'quantum.plugins.openvswitch.ovs_quantum_plugin.'
65 'OVSQuantumPluginV2',
66 'contexts': [
67 context.SharedDBContext(user=config('neutron-database-user'),
68 database=config('neutron-database'),
69 relation_prefix='neutron',
70 ssl_dir=QUANTUM_CONF_DIR)],
71 'services': ['quantum-plugin-openvswitch-agent'],
72 'packages': [[headers_package()] + determine_dkms_package(),
73 ['quantum-plugin-openvswitch-agent']],
74 'server_packages': ['quantum-server',
75 'quantum-plugin-openvswitch'],
76 'server_services': ['quantum-server']
77 },
78 'nvp': {
79 'config': '/etc/quantum/plugins/nicira/nvp.ini',
80 'driver': 'quantum.plugins.nicira.nicira_nvp_plugin.'
81 'QuantumPlugin.NvpPluginV2',
82 'contexts': [
83 context.SharedDBContext(user=config('neutron-database-user'),
84 database=config('neutron-database'),
85 relation_prefix='neutron',
86 ssl_dir=QUANTUM_CONF_DIR)],
87 'services': [],
88 'packages': [],
89 'server_packages': ['quantum-server',
90 'quantum-plugin-nicira'],
91 'server_services': ['quantum-server']
92 }
93 }
94
95NEUTRON_CONF_DIR = '/etc/neutron'
96
97
98def neutron_plugins():
99 from charmhelpers.contrib.openstack import context
100 release = os_release('nova-common')
101 plugins = {
102 'ovs': {
103 'config': '/etc/neutron/plugins/openvswitch/'
104 'ovs_neutron_plugin.ini',
105 'driver': 'neutron.plugins.openvswitch.ovs_neutron_plugin.'
106 'OVSNeutronPluginV2',
107 'contexts': [
108 context.SharedDBContext(user=config('neutron-database-user'),
109 database=config('neutron-database'),
110 relation_prefix='neutron',
111 ssl_dir=NEUTRON_CONF_DIR)],
112 'services': ['neutron-plugin-openvswitch-agent'],
113 'packages': [[headers_package()] + determine_dkms_package(),
114 ['neutron-plugin-openvswitch-agent']],
115 'server_packages': ['neutron-server',
116 'neutron-plugin-openvswitch'],
117 'server_services': ['neutron-server']
118 },
119 'nvp': {
120 'config': '/etc/neutron/plugins/nicira/nvp.ini',
121 'driver': 'neutron.plugins.nicira.nicira_nvp_plugin.'
122 'NeutronPlugin.NvpPluginV2',
123 'contexts': [
124 context.SharedDBContext(user=config('neutron-database-user'),
125 database=config('neutron-database'),
126 relation_prefix='neutron',
127 ssl_dir=NEUTRON_CONF_DIR)],
128 'services': [],
129 'packages': [],
130 'server_packages': ['neutron-server',
131 'neutron-plugin-nicira'],
132 'server_services': ['neutron-server']
133 },
134 'nsx': {
135 'config': '/etc/neutron/plugins/vmware/nsx.ini',
136 'driver': 'vmware',
137 'contexts': [
138 context.SharedDBContext(user=config('neutron-database-user'),
139 database=config('neutron-database'),
140 relation_prefix='neutron',
141 ssl_dir=NEUTRON_CONF_DIR)],
142 'services': [],
143 'packages': [],
144 'server_packages': ['neutron-server',
145 'neutron-plugin-vmware'],
146 'server_services': ['neutron-server']
147 },
148 'n1kv': {
149 'config': '/etc/neutron/plugins/cisco/cisco_plugins.ini',
150 'driver': 'neutron.plugins.cisco.network_plugin.PluginV2',
151 'contexts': [
152 context.SharedDBContext(user=config('neutron-database-user'),
153 database=config('neutron-database'),
154 relation_prefix='neutron',
155 ssl_dir=NEUTRON_CONF_DIR)],
156 'services': [],
157 'packages': [[headers_package()] + determine_dkms_package(),
158 ['neutron-plugin-cisco']],
159 'server_packages': ['neutron-server',
160 'neutron-plugin-cisco'],
161 'server_services': ['neutron-server']
162 },
163 'Calico': {
164 'config': '/etc/neutron/plugins/ml2/ml2_conf.ini',
165 'driver': 'neutron.plugins.ml2.plugin.Ml2Plugin',
166 'contexts': [
167 context.SharedDBContext(user=config('neutron-database-user'),
168 database=config('neutron-database'),
169 relation_prefix='neutron',
170 ssl_dir=NEUTRON_CONF_DIR)],
171 'services': ['calico-felix',
172 'bird',
173 'neutron-dhcp-agent',
174 'nova-api-metadata'],
175 'packages': [[headers_package()] + determine_dkms_package(),
176 ['calico-compute',
177 'bird',
178 'neutron-dhcp-agent',
179 'nova-api-metadata']],
180 'server_packages': ['neutron-server', 'calico-control'],
181 'server_services': ['neutron-server']
182 }
183 }
184 if release >= 'icehouse':
185 # NOTE: patch in ml2 plugin for icehouse onwards
186 plugins['ovs']['config'] = '/etc/neutron/plugins/ml2/ml2_conf.ini'
187 plugins['ovs']['driver'] = 'neutron.plugins.ml2.plugin.Ml2Plugin'
188 plugins['ovs']['server_packages'] = ['neutron-server',
189 'neutron-plugin-ml2']
190 # NOTE: patch in vmware renames nvp->nsx for icehouse onwards
191 plugins['nvp'] = plugins['nsx']
192 return plugins
193
194
195def neutron_plugin_attribute(plugin, attr, net_manager=None):
196 manager = net_manager or network_manager()
197 if manager == 'quantum':
198 plugins = quantum_plugins()
199 elif manager == 'neutron':
200 plugins = neutron_plugins()
201 else:
202 log("Network manager '%s' does not support plugins." % (manager),
203 level=ERROR)
204 raise Exception
205
206 try:
207 _plugin = plugins[plugin]
208 except KeyError:
209 log('Unrecognised plugin for %s: %s' % (manager, plugin), level=ERROR)
210 raise Exception
211
212 try:
213 return _plugin[attr]
214 except KeyError:
215 return None
216
217
218def network_manager():
219 '''
220 Deals with the renaming of Quantum to Neutron in H and any situations
221 that require compatability (eg, deploying H with network-manager=quantum,
222 upgrading from G).
223 '''
224 release = os_release('nova-common')
225 manager = config('network-manager').lower()
226
227 if manager not in ['quantum', 'neutron']:
228 return manager
229
230 if release in ['essex']:
231 # E does not support neutron
232 log('Neutron networking not supported in Essex.', level=ERROR)
233 raise Exception
234 elif release in ['folsom', 'grizzly']:
235 # neutron is named quantum in F and G
236 return 'quantum'
237 else:
238 # ensure accurate naming for all releases post-H
239 return 'neutron'
0240
=== added directory 'hooks/charmhelpers/contrib/openstack/templates'
=== added file 'hooks/charmhelpers/contrib/openstack/templates/__init__.py'
--- hooks/charmhelpers/contrib/openstack/templates/__init__.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/openstack/templates/__init__.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,18 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17# dummy __init__.py to fool syncer into thinking this is a syncable python
18# module
019
=== added file 'hooks/charmhelpers/contrib/openstack/templating.py'
--- hooks/charmhelpers/contrib/openstack/templating.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/contrib/openstack/templating.py 2015-06-18 17:19:58 +0000
@@ -0,0 +1,295 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# This file is part of charm-helpers.
4#
5# charm-helpers is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3 as
7# published by the Free Software Foundation.
8#
9# charm-helpers is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
16
17import os
18
19import six
20
21from charmhelpers.fetch import apt_install
22from charmhelpers.core.hookenv import (
23 log,
24 ERROR,
25 INFO
26)
27from charmhelpers.contrib.openstack.utils import OPENSTACK_CODENAMES
28
29try:
30 from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
31except ImportError:
32 # python-jinja2 may not be installed yet, or we're running unittests.
33 FileSystemLoader = ChoiceLoader = Environment = exceptions = None
34
35
36class OSConfigException(Exception):
37 pass
38
39
40def get_loader(templates_dir, os_release):
41 """
42 Create a jinja2.ChoiceLoader containing template dirs up to
43 and including os_release. If directory template directory
44 is missing at templates_dir, it will be omitted from the loader.
45 templates_dir is added to the bottom of the search list as a base
46 loading dir.
47
48 A charm may also ship a templates dir with this module
49 and it will be appended to the bottom of the search list, eg::
50
51 hooks/charmhelpers/contrib/openstack/templates
52
53 :param templates_dir (str): Base template directory containing release
54 sub-directories.
55 :param os_release (str): OpenStack release codename to construct template
56 loader.
57 :returns: jinja2.ChoiceLoader constructed with a list of
58 jinja2.FilesystemLoaders, ordered in descending
59 order by OpenStack release.
60 """
61 tmpl_dirs = [(rel, os.path.join(templates_dir, rel))
62 for rel in six.itervalues(OPENSTACK_CODENAMES)]
63
64 if not os.path.isdir(templates_dir):
65 log('Templates directory not found @ %s.' % templates_dir,
66 level=ERROR)
67 raise OSConfigException
68
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: