Merge ~xavpaice/charm-nagios:fix-lp1677580 into ~nagios-charmers/charm-nagios:master

Proposed by Xav Paice
Status: Merged
Approved by: James Hebden
Approved revision: dbc7c76615b10b763c5f5ca355a83ac8afee8b8b
Merged at revision: 38f049516d4865a1e3c1fec5289f6f189fff0631
Proposed branch: ~xavpaice/charm-nagios:fix-lp1677580
Merge into: ~nagios-charmers/charm-nagios:master
Diff against target: 6573 lines (+5335/-405)
39 files modified
Makefile (+14/-0)
bin/charm_helpers_sync.py (+252/-0)
charm-helpers.yaml (+2/-1)
config.yaml (+21/-0)
hooks/charmhelpers/__init__.py (+97/-0)
hooks/charmhelpers/contrib/__init__.py (+13/-0)
hooks/charmhelpers/contrib/ssl/__init__.py (+15/-1)
hooks/charmhelpers/contrib/ssl/service.py (+28/-18)
hooks/charmhelpers/core/__init__.py (+13/-0)
hooks/charmhelpers/core/decorators.py (+55/-0)
hooks/charmhelpers/core/files.py (+43/-0)
hooks/charmhelpers/core/fstab.py (+132/-0)
hooks/charmhelpers/core/hookenv.py (+742/-35)
hooks/charmhelpers/core/host.py (+755/-104)
hooks/charmhelpers/core/host_factory/__init__.py (+0/-0)
hooks/charmhelpers/core/host_factory/centos.py (+72/-0)
hooks/charmhelpers/core/host_factory/ubuntu.py (+89/-0)
hooks/charmhelpers/core/hugepage.py (+69/-0)
hooks/charmhelpers/core/kernel.py (+72/-0)
hooks/charmhelpers/core/kernel_factory/__init__.py (+0/-0)
hooks/charmhelpers/core/kernel_factory/centos.py (+17/-0)
hooks/charmhelpers/core/kernel_factory/ubuntu.py (+13/-0)
hooks/charmhelpers/core/services/__init__.py (+16/-0)
hooks/charmhelpers/core/services/base.py (+351/-0)
hooks/charmhelpers/core/services/helpers.py (+290/-0)
hooks/charmhelpers/core/strutils.py (+123/-0)
hooks/charmhelpers/core/sysctl.py (+54/-0)
hooks/charmhelpers/core/templating.py (+84/-0)
hooks/charmhelpers/core/unitdata.py (+518/-0)
hooks/charmhelpers/fetch/__init__.py (+135/-211)
hooks/charmhelpers/fetch/archiveurl.py (+126/-9)
hooks/charmhelpers/fetch/bzrurl.py (+52/-25)
hooks/charmhelpers/fetch/centos.py (+171/-0)
hooks/charmhelpers/fetch/giturl.py (+69/-0)
hooks/charmhelpers/fetch/snap.py (+134/-0)
hooks/charmhelpers/fetch/ubuntu.py (+583/-0)
hooks/charmhelpers/osplatform.py (+25/-0)
hooks/templates/localhost_nagios2.cfg.tmpl (+70/-0)
hooks/upgrade-charm (+20/-1)
Reviewer Review Type Date Requested Status
James Hebden (community) Approve
Review via email: mp+329236@code.launchpad.net
To post a comment you must log in.
Revision history for this message
James Hebden (ec0) wrote :

LGTM

Revision history for this message
James Hebden (ec0) wrote :

Yep, LGTM.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/Makefile b/Makefile
index c75b2e9..9d48829 100644
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,7 @@
1#!/usr/bin/make
2PYTHON := /usr/bin/python3
3export PYTHONPATH := hooks
4
1default:5default:
2 echo Nothing to do6 echo Nothing to do
37
@@ -12,3 +16,13 @@ test:
12 tests/22-extraconfig-test16 tests/22-extraconfig-test
13 tests/23-livestatus-test17 tests/23-livestatus-test
14 tests/24-pagerduty-test18 tests/24-pagerduty-test
19
20bin/charm_helpers_sync.py:
21 @mkdir -p bin
22 @bzr cat lp:charm-helpers/tools/charm_helpers_sync/charm_helpers_sync.py \
23 > bin/charm_helpers_sync.py
24
25sync: bin/charm_helpers_sync.py
26 @$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers.yaml
27
28
diff --git a/bin/charm_helpers_sync.py b/bin/charm_helpers_sync.py
15new file mode 10064429new file mode 100644
index 0000000..bd79460
--- /dev/null
+++ b/bin/charm_helpers_sync.py
@@ -0,0 +1,252 @@
1#!/usr/bin/python
2
3# Copyright 2014-2015 Canonical Limited.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17# Authors:
18# Adam Gandelman <adamg@ubuntu.com>
19
20import logging
21import optparse
22import os
23import subprocess
24import shutil
25import sys
26import tempfile
27import yaml
28from fnmatch import fnmatch
29
30import six
31
32CHARM_HELPERS_BRANCH = 'lp:charm-helpers'
33
34
35def parse_config(conf_file):
36 if not os.path.isfile(conf_file):
37 logging.error('Invalid config file: %s.' % conf_file)
38 return False
39 return yaml.load(open(conf_file).read())
40
41
42def clone_helpers(work_dir, branch):
43 dest = os.path.join(work_dir, 'charm-helpers')
44 logging.info('Checking out %s to %s.' % (branch, dest))
45 cmd = ['bzr', 'checkout', '--lightweight', branch, dest]
46 subprocess.check_call(cmd)
47 return dest
48
49
50def _module_path(module):
51 return os.path.join(*module.split('.'))
52
53
54def _src_path(src, module):
55 return os.path.join(src, 'charmhelpers', _module_path(module))
56
57
58def _dest_path(dest, module):
59 return os.path.join(dest, _module_path(module))
60
61
62def _is_pyfile(path):
63 return os.path.isfile(path + '.py')
64
65
66def ensure_init(path):
67 '''
68 ensure directories leading up to path are importable, omitting
69 parent directory, eg path='/hooks/helpers/foo'/:
70 hooks/
71 hooks/helpers/__init__.py
72 hooks/helpers/foo/__init__.py
73 '''
74 for d, dirs, files in os.walk(os.path.join(*path.split('/')[:2])):
75 _i = os.path.join(d, '__init__.py')
76 if not os.path.exists(_i):
77 logging.info('Adding missing __init__.py: %s' % _i)
78 open(_i, 'wb').close()
79
80
81def sync_pyfile(src, dest):
82 src = src + '.py'
83 src_dir = os.path.dirname(src)
84 logging.info('Syncing pyfile: %s -> %s.' % (src, dest))
85 if not os.path.exists(dest):
86 os.makedirs(dest)
87 shutil.copy(src, dest)
88 if os.path.isfile(os.path.join(src_dir, '__init__.py')):
89 shutil.copy(os.path.join(src_dir, '__init__.py'),
90 dest)
91 ensure_init(dest)
92
93
94def get_filter(opts=None):
95 opts = opts or []
96 if 'inc=*' in opts:
97 # do not filter any files, include everything
98 return None
99
100 def _filter(dir, ls):
101 incs = [opt.split('=').pop() for opt in opts if 'inc=' in opt]
102 _filter = []
103 for f in ls:
104 _f = os.path.join(dir, f)
105
106 if not os.path.isdir(_f) and not _f.endswith('.py') and incs:
107 if True not in [fnmatch(_f, inc) for inc in incs]:
108 logging.debug('Not syncing %s, does not match include '
109 'filters (%s)' % (_f, incs))
110 _filter.append(f)
111 else:
112 logging.debug('Including file, which matches include '
113 'filters (%s): %s' % (incs, _f))
114 elif (os.path.isfile(_f) and not _f.endswith('.py')):
115 logging.debug('Not syncing file: %s' % f)
116 _filter.append(f)
117 elif (os.path.isdir(_f) and not
118 os.path.isfile(os.path.join(_f, '__init__.py'))):
119 logging.debug('Not syncing directory: %s' % f)
120 _filter.append(f)
121 return _filter
122 return _filter
123
124
125def sync_directory(src, dest, opts=None):
126 if os.path.exists(dest):
127 logging.debug('Removing existing directory: %s' % dest)
128 shutil.rmtree(dest)
129 logging.info('Syncing directory: %s -> %s.' % (src, dest))
130
131 shutil.copytree(src, dest, ignore=get_filter(opts))
132 ensure_init(dest)
133
134
135def sync(src, dest, module, opts=None):
136
137 # Sync charmhelpers/__init__.py for bootstrap code.
138 sync_pyfile(_src_path(src, '__init__'), dest)
139
140 # Sync other __init__.py files in the path leading to module.
141 m = []
142 steps = module.split('.')[:-1]
143 while steps:
144 m.append(steps.pop(0))
145 init = '.'.join(m + ['__init__'])
146 sync_pyfile(_src_path(src, init),
147 os.path.dirname(_dest_path(dest, init)))
148
149 # Sync the module, or maybe a .py file.
150 if os.path.isdir(_src_path(src, module)):
151 sync_directory(_src_path(src, module), _dest_path(dest, module), opts)
152 elif _is_pyfile(_src_path(src, module)):
153 sync_pyfile(_src_path(src, module),
154 os.path.dirname(_dest_path(dest, module)))
155 else:
156 logging.warn('Could not sync: %s. Neither a pyfile or directory, '
157 'does it even exist?' % module)
158
159
160def parse_sync_options(options):
161 if not options:
162 return []
163 return options.split(',')
164
165
166def extract_options(inc, global_options=None):
167 global_options = global_options or []
168 if global_options and isinstance(global_options, six.string_types):
169 global_options = [global_options]
170 if '|' not in inc:
171 return (inc, global_options)
172 inc, opts = inc.split('|')
173 return (inc, parse_sync_options(opts) + global_options)
174
175
176def sync_helpers(include, src, dest, options=None):
177 if not os.path.isdir(dest):
178 os.makedirs(dest)
179
180 global_options = parse_sync_options(options)
181
182 for inc in include:
183 if isinstance(inc, str):
184 inc, opts = extract_options(inc, global_options)
185 sync(src, dest, inc, opts)
186 elif isinstance(inc, dict):
187 # could also do nested dicts here.
188 for k, v in six.iteritems(inc):
189 if isinstance(v, list):
190 for m in v:
191 inc, opts = extract_options(m, global_options)
192 sync(src, dest, '%s.%s' % (k, inc), opts)
193
194
195if __name__ == '__main__':
196 parser = optparse.OptionParser()
197 parser.add_option('-c', '--config', action='store', dest='config',
198 default=None, help='helper config file')
199 parser.add_option('-D', '--debug', action='store_true', dest='debug',
200 default=False, help='debug')
201 parser.add_option('-b', '--branch', action='store', dest='branch',
202 help='charm-helpers bzr branch (overrides config)')
203 parser.add_option('-d', '--destination', action='store', dest='dest_dir',
204 help='sync destination dir (overrides config)')
205 (opts, args) = parser.parse_args()
206
207 if opts.debug:
208 logging.basicConfig(level=logging.DEBUG)
209 else:
210 logging.basicConfig(level=logging.INFO)
211
212 if opts.config:
213 logging.info('Loading charm helper config from %s.' % opts.config)
214 config = parse_config(opts.config)
215 if not config:
216 logging.error('Could not parse config from %s.' % opts.config)
217 sys.exit(1)
218 else:
219 config = {}
220
221 if 'branch' not in config:
222 config['branch'] = CHARM_HELPERS_BRANCH
223 if opts.branch:
224 config['branch'] = opts.branch
225 if opts.dest_dir:
226 config['destination'] = opts.dest_dir
227
228 if 'destination' not in config:
229 logging.error('No destination dir. specified as option or config.')
230 sys.exit(1)
231
232 if 'include' not in config:
233 if not args:
234 logging.error('No modules to sync specified as option or config.')
235 sys.exit(1)
236 config['include'] = []
237 [config['include'].append(a) for a in args]
238
239 sync_options = None
240 if 'options' in config:
241 sync_options = config['options']
242 tmpd = tempfile.mkdtemp()
243 try:
244 checkout = clone_helpers(tmpd, config['branch'])
245 sync_helpers(config['include'], checkout, config['destination'],
246 options=sync_options)
247 except Exception as e:
248 logging.error("Could not sync: %s" % e)
249 raise e
250 finally:
251 logging.debug('Cleaning up %s' % tmpd)
252 shutil.rmtree(tmpd)
diff --git a/charm-helpers.yaml b/charm-helpers.yaml
index 4c97181..e5f7760 100644
--- a/charm-helpers.yaml
+++ b/charm-helpers.yaml
@@ -1,6 +1,7 @@
1destination: hooks/charmhelpers1destination: hooks/charmhelpers
2branch: lp:~openstack-charmers/charm-helpers/ssl-everywhere2branch: lp:charm-helpers
3include:3include:
4 - core4 - core
5 - fetch5 - fetch
6 - osplatform
6 - contrib.ssl7 - contrib.ssl
diff --git a/config.yaml b/config.yaml
index 3eb834a..77dc2a1 100644
--- a/config.yaml
+++ b/config.yaml
@@ -144,3 +144,24 @@ options:
144 Password to use for Nagios administrative access. If not144 Password to use for Nagios administrative access. If not
145 provided, a password will be generated (see documentation for145 provided, a password will be generated (see documentation for
146 instructions on retrieving the generated password.)146 instructions on retrieving the generated password.)
147 monitor_self:
148 type: boolean
149 default: true
150 description: |
151 If true, enable monitoring of the nagios unit itself.
152 nagios_host_context:
153 default: "juju"
154 type: string
155 description: |
156 a string that will be prepended to instance name to set the host name
157 in nagios. So for instance the hostname would be something like:
158 juju-postgresql-0
159 If you're running multiple environments with the same services in them
160 this allows you to differentiate between them.
161 load_monitor:
162 default: '5.0!4.0!3.0!10.0!6.0!4.0'
163 type: string
164 description: |
165 A string to pass to the Nagios load monitoring command. Default is
166 to report warning at 5.0, 4.0 and 3.0 averages, critical at 10.0,
167 6.0 and 4.0.
diff --git a/hooks/charmhelpers/__init__.py b/hooks/charmhelpers/__init__.py
index e69de29..e7aa471 100644
--- a/hooks/charmhelpers/__init__.py
+++ b/hooks/charmhelpers/__init__.py
@@ -0,0 +1,97 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15# Bootstrap charm-helpers, installing its dependencies if necessary using
16# only standard libraries.
17from __future__ import print_function
18from __future__ import absolute_import
19
20import functools
21import inspect
22import subprocess
23import sys
24
25try:
26 import six # flake8: noqa
27except ImportError:
28 if sys.version_info.major == 2:
29 subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
30 else:
31 subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
32 import six # flake8: noqa
33
34try:
35 import yaml # flake8: noqa
36except ImportError:
37 if sys.version_info.major == 2:
38 subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
39 else:
40 subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
41 import yaml # flake8: noqa
42
43
44# Holds a list of mapping of mangled function names that have been deprecated
45# using the @deprecate decorator below. This is so that the warning is only
46# printed once for each usage of the function.
47__deprecated_functions = {}
48
49
50def deprecate(warning, date=None, log=None):
51 """Add a deprecation warning the first time the function is used.
52 The date, which is a string in semi-ISO8660 format indicate the year-month
53 that the function is officially going to be removed.
54
55 usage:
56
57 @deprecate('use core/fetch/add_source() instead', '2017-04')
58 def contributed_add_source_thing(...):
59 ...
60
61 And it then prints to the log ONCE that the function is deprecated.
62 The reason for passing the logging function (log) is so that hookenv.log
63 can be used for a charm if needed.
64
65 :param warning: String to indicat where it has moved ot.
66 :param date: optional sting, in YYYY-MM format to indicate when the
67 function will definitely (probably) be removed.
68 :param log: The log function to call to log. If not, logs to stdout
69 """
70 def wrap(f):
71
72 @functools.wraps(f)
73 def wrapped_f(*args, **kwargs):
74 try:
75 module = inspect.getmodule(f)
76 file = inspect.getsourcefile(f)
77 lines = inspect.getsourcelines(f)
78 f_name = "{}-{}-{}..{}-{}".format(
79 module.__name__, file, lines[0], lines[-1], f.__name__)
80 except (IOError, TypeError):
81 # assume it was local, so just use the name of the function
82 f_name = f.__name__
83 if f_name not in __deprecated_functions:
84 __deprecated_functions[f_name] = True
85 s = "DEPRECATION WARNING: Function {} is being removed".format(
86 f.__name__)
87 if date:
88 s = "{} on/around {}".format(s, date)
89 if warning:
90 s = "{} : {}".format(s, warning)
91 if log:
92 log(s)
93 else:
94 print(s)
95 return f(*args, **kwargs)
96 return wrapped_f
97 return wrap
diff --git a/hooks/charmhelpers/contrib/__init__.py b/hooks/charmhelpers/contrib/__init__.py
index e69de29..d7567b8 100644
--- a/hooks/charmhelpers/contrib/__init__.py
+++ b/hooks/charmhelpers/contrib/__init__.py
@@ -0,0 +1,13 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
diff --git a/hooks/charmhelpers/contrib/ssl/__init__.py b/hooks/charmhelpers/contrib/ssl/__init__.py
index 2999c0a..1d238b5 100644
--- a/hooks/charmhelpers/contrib/ssl/__init__.py
+++ b/hooks/charmhelpers/contrib/ssl/__init__.py
@@ -1,3 +1,17 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
1import subprocess15import subprocess
2from charmhelpers.core import hookenv16from charmhelpers.core import hookenv
317
@@ -74,5 +88,5 @@ def generate_selfsigned(keyfile, certfile, keysize="1024", config=None, subject=
74 subprocess.check_call(cmd)88 subprocess.check_call(cmd)
75 return True89 return True
76 except Exception as e:90 except Exception as e:
77 print "Execution of openssl command failed:\n{}".format(e)91 print("Execution of openssl command failed:\n{}".format(e))
78 return False92 return False
diff --git a/hooks/charmhelpers/contrib/ssl/service.py b/hooks/charmhelpers/contrib/ssl/service.py
index 295f721..06b534f 100644
--- a/hooks/charmhelpers/contrib/ssl/service.py
+++ b/hooks/charmhelpers/contrib/ssl/service.py
@@ -1,13 +1,23 @@
1import logging1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
2import os15import os
3from os.path import join as path_join16from os.path import join as path_join
4from os.path import exists17from os.path import exists
5import subprocess18import subprocess
619
720from charmhelpers.core.hookenv import log, DEBUG
8log = logging.getLogger("service_ca")
9
10logging.basicConfig(level=logging.DEBUG)
1121
12STD_CERT = "standard"22STD_CERT = "standard"
1323
@@ -46,7 +56,7 @@ class ServiceCA(object):
46 ###############56 ###############
4757
48 def init(self):58 def init(self):
49 log.debug("initializing service ca")59 log("initializing service ca", level=DEBUG)
50 if not exists(self.ca_dir):60 if not exists(self.ca_dir):
51 self._init_ca_dir(self.ca_dir)61 self._init_ca_dir(self.ca_dir)
52 self._init_ca()62 self._init_ca()
@@ -75,23 +85,23 @@ class ServiceCA(object):
75 os.mkdir(sd)85 os.mkdir(sd)
7686
77 if not exists(path_join(ca_dir, 'serial')):87 if not exists(path_join(ca_dir, 'serial')):
78 with open(path_join(ca_dir, 'serial'), 'wb') as fh:88 with open(path_join(ca_dir, 'serial'), 'w') as fh:
79 fh.write('02\n')89 fh.write('02\n')
8090
81 if not exists(path_join(ca_dir, 'index.txt')):91 if not exists(path_join(ca_dir, 'index.txt')):
82 with open(path_join(ca_dir, 'index.txt'), 'wb') as fh:92 with open(path_join(ca_dir, 'index.txt'), 'w') as fh:
83 fh.write('')93 fh.write('')
8494
85 def _init_ca(self):95 def _init_ca(self):
86 """Generate the root ca's cert and key.96 """Generate the root ca's cert and key.
87 """97 """
88 if not exists(path_join(self.ca_dir, 'ca.cnf')):98 if not exists(path_join(self.ca_dir, 'ca.cnf')):
89 with open(path_join(self.ca_dir, 'ca.cnf'), 'wb') as fh:99 with open(path_join(self.ca_dir, 'ca.cnf'), 'w') as fh:
90 fh.write(100 fh.write(
91 CA_CONF_TEMPLATE % (self.get_conf_variables()))101 CA_CONF_TEMPLATE % (self.get_conf_variables()))
92102
93 if not exists(path_join(self.ca_dir, 'signing.cnf')):103 if not exists(path_join(self.ca_dir, 'signing.cnf')):
94 with open(path_join(self.ca_dir, 'signing.cnf'), 'wb') as fh:104 with open(path_join(self.ca_dir, 'signing.cnf'), 'w') as fh:
95 fh.write(105 fh.write(
96 SIGNING_CONF_TEMPLATE % (self.get_conf_variables()))106 SIGNING_CONF_TEMPLATE % (self.get_conf_variables()))
97107
@@ -103,7 +113,7 @@ class ServiceCA(object):
103 '-keyout', self.ca_key, '-out', self.ca_cert,113 '-keyout', self.ca_key, '-out', self.ca_cert,
104 '-outform', 'PEM']114 '-outform', 'PEM']
105 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)115 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
106 log.debug("CA Init:\n %s", output)116 log("CA Init:\n %s" % output, level=DEBUG)
107117
108 def get_conf_variables(self):118 def get_conf_variables(self):
109 return dict(119 return dict(
@@ -127,7 +137,7 @@ class ServiceCA(object):
127 return self.get_certificate(common_name)137 return self.get_certificate(common_name)
128138
129 def get_certificate(self, common_name):139 def get_certificate(self, common_name):
130 if not common_name in self:140 if common_name not in self:
131 raise ValueError("No certificate for %s" % common_name)141 raise ValueError("No certificate for %s" % common_name)
132 key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name)142 key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name)
133 crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)143 crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
@@ -147,15 +157,15 @@ class ServiceCA(object):
147 subj = '/O=%(org_name)s/OU=%(org_unit_name)s/CN=%(common_name)s' % (157 subj = '/O=%(org_name)s/OU=%(org_unit_name)s/CN=%(common_name)s' % (
148 template_vars)158 template_vars)
149159
150 log.debug("CA Create Cert %s", common_name)160 log("CA Create Cert %s" % common_name, level=DEBUG)
151 cmd = ['openssl', 'req', '-sha1', '-newkey', 'rsa:2048',161 cmd = ['openssl', 'req', '-sha1', '-newkey', 'rsa:2048',
152 '-nodes', '-days', self.default_expiry,162 '-nodes', '-days', self.default_expiry,
153 '-keyout', key_p, '-out', csr_p, '-subj', subj]163 '-keyout', key_p, '-out', csr_p, '-subj', subj]
154 subprocess.check_call(cmd)164 subprocess.check_call(cmd, stderr=subprocess.PIPE)
155 cmd = ['openssl', 'rsa', '-in', key_p, '-out', key_p]165 cmd = ['openssl', 'rsa', '-in', key_p, '-out', key_p]
156 subprocess.check_call(cmd)166 subprocess.check_call(cmd, stderr=subprocess.PIPE)
157167
158 log.debug("CA Sign Cert %s", common_name)168 log("CA Sign Cert %s" % common_name, level=DEBUG)
159 if self.cert_type == MYSQL_CERT:169 if self.cert_type == MYSQL_CERT:
160 cmd = ['openssl', 'x509', '-req',170 cmd = ['openssl', 'x509', '-req',
161 '-in', csr_p, '-days', self.default_expiry,171 '-in', csr_p, '-days', self.default_expiry,
@@ -166,8 +176,8 @@ class ServiceCA(object):
166 '-extensions', 'req_extensions',176 '-extensions', 'req_extensions',
167 '-days', self.default_expiry, '-notext',177 '-days', self.default_expiry, '-notext',
168 '-in', csr_p, '-out', crt_p, '-subj', subj, '-batch']178 '-in', csr_p, '-out', crt_p, '-subj', subj, '-batch']
169 log.debug("running %s", " ".join(cmd))179 log("running %s" % " ".join(cmd), level=DEBUG)
170 subprocess.check_call(cmd)180 subprocess.check_call(cmd, stderr=subprocess.PIPE)
171181
172 def get_ca_bundle(self):182 def get_ca_bundle(self):
173 with open(self.ca_cert) as fh:183 with open(self.ca_cert) as fh:
diff --git a/hooks/charmhelpers/core/__init__.py b/hooks/charmhelpers/core/__init__.py
index e69de29..d7567b8 100644
--- a/hooks/charmhelpers/core/__init__.py
+++ b/hooks/charmhelpers/core/__init__.py
@@ -0,0 +1,13 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
diff --git a/hooks/charmhelpers/core/decorators.py b/hooks/charmhelpers/core/decorators.py
0new file mode 10064414new file mode 100644
index 0000000..6ad41ee
--- /dev/null
+++ b/hooks/charmhelpers/core/decorators.py
@@ -0,0 +1,55 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15#
16# Copyright 2014 Canonical Ltd.
17#
18# Authors:
19# Edward Hope-Morley <opentastic@gmail.com>
20#
21
22import time
23
24from charmhelpers.core.hookenv import (
25 log,
26 INFO,
27)
28
29
30def retry_on_exception(num_retries, base_delay=0, exc_type=Exception):
31 """If the decorated function raises exception exc_type, allow num_retries
32 retry attempts before raise the exception.
33 """
34 def _retry_on_exception_inner_1(f):
35 def _retry_on_exception_inner_2(*args, **kwargs):
36 retries = num_retries
37 multiplier = 1
38 while True:
39 try:
40 return f(*args, **kwargs)
41 except exc_type:
42 if not retries:
43 raise
44
45 delay = base_delay * multiplier
46 multiplier += 1
47 log("Retrying '%s' %d more times (delay=%s)" %
48 (f.__name__, retries, delay), level=INFO)
49 retries -= 1
50 if delay:
51 time.sleep(delay)
52
53 return _retry_on_exception_inner_2
54
55 return _retry_on_exception_inner_1
diff --git a/hooks/charmhelpers/core/files.py b/hooks/charmhelpers/core/files.py
0new file mode 10064456new file mode 100644
index 0000000..fdd82b7
--- /dev/null
+++ b/hooks/charmhelpers/core/files.py
@@ -0,0 +1,43 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# Copyright 2014-2015 Canonical Limited.
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17
18__author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>'
19
20import os
21import subprocess
22
23
24def sed(filename, before, after, flags='g'):
25 """
26 Search and replaces the given pattern on filename.
27
28 :param filename: relative or absolute file path.
29 :param before: expression to be replaced (see 'man sed')
30 :param after: expression to replace with (see 'man sed')
31 :param flags: sed-compatible regex flags in example, to make
32 the search and replace case insensitive, specify ``flags="i"``.
33 The ``g`` flag is always specified regardless, so you do not
34 need to remember to include it when overriding this parameter.
35 :returns: If the sed command exit code was zero then return,
36 otherwise raise CalledProcessError.
37 """
38 expression = r's/{0}/{1}/{2}'.format(before,
39 after, flags)
40
41 return subprocess.check_call(["sed", "-i", "-r", "-e",
42 expression,
43 os.path.expanduser(filename)])
diff --git a/hooks/charmhelpers/core/fstab.py b/hooks/charmhelpers/core/fstab.py
0new file mode 10064444new file mode 100644
index 0000000..d9fa915
--- /dev/null
+++ b/hooks/charmhelpers/core/fstab.py
@@ -0,0 +1,132 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# Copyright 2014-2015 Canonical Limited.
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17
18import io
19import os
20
21__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
22
23
24class Fstab(io.FileIO):
25 """This class extends file in order to implement a file reader/writer
26 for file `/etc/fstab`
27 """
28
29 class Entry(object):
30 """Entry class represents a non-comment line on the `/etc/fstab` file
31 """
32 def __init__(self, device, mountpoint, filesystem,
33 options, d=0, p=0):
34 self.device = device
35 self.mountpoint = mountpoint
36 self.filesystem = filesystem
37
38 if not options:
39 options = "defaults"
40
41 self.options = options
42 self.d = int(d)
43 self.p = int(p)
44
45 def __eq__(self, o):
46 return str(self) == str(o)
47
48 def __str__(self):
49 return "{} {} {} {} {} {}".format(self.device,
50 self.mountpoint,
51 self.filesystem,
52 self.options,
53 self.d,
54 self.p)
55
56 DEFAULT_PATH = os.path.join(os.path.sep, 'etc', 'fstab')
57
58 def __init__(self, path=None):
59 if path:
60 self._path = path
61 else:
62 self._path = self.DEFAULT_PATH
63 super(Fstab, self).__init__(self._path, 'rb+')
64
65 def _hydrate_entry(self, line):
66 # NOTE: use split with no arguments to split on any
67 # whitespace including tabs
68 return Fstab.Entry(*filter(
69 lambda x: x not in ('', None),
70 line.strip("\n").split()))
71
72 @property
73 def entries(self):
74 self.seek(0)
75 for line in self.readlines():
76 line = line.decode('us-ascii')
77 try:
78 if line.strip() and not line.strip().startswith("#"):
79 yield self._hydrate_entry(line)
80 except ValueError:
81 pass
82
83 def get_entry_by_attr(self, attr, value):
84 for entry in self.entries:
85 e_attr = getattr(entry, attr)
86 if e_attr == value:
87 return entry
88 return None
89
90 def add_entry(self, entry):
91 if self.get_entry_by_attr('device', entry.device):
92 return False
93
94 self.write((str(entry) + '\n').encode('us-ascii'))
95 self.truncate()
96 return entry
97
98 def remove_entry(self, entry):
99 self.seek(0)
100
101 lines = [l.decode('us-ascii') for l in self.readlines()]
102
103 found = False
104 for index, line in enumerate(lines):
105 if line.strip() and not line.strip().startswith("#"):
106 if self._hydrate_entry(line) == entry:
107 found = True
108 break
109
110 if not found:
111 return False
112
113 lines.remove(line)
114
115 self.seek(0)
116 self.write(''.join(lines).encode('us-ascii'))
117 self.truncate()
118 return True
119
120 @classmethod
121 def remove_by_mountpoint(cls, mountpoint, path=None):
122 fstab = cls(path=path)
123 entry = fstab.get_entry_by_attr('mountpoint', mountpoint)
124 if entry:
125 return fstab.remove_entry(entry)
126 return False
127
128 @classmethod
129 def add(cls, device, mountpoint, filesystem, options=None, path=None):
130 return cls(path=path).add_entry(Fstab.Entry(device,
131 mountpoint, filesystem,
132 options=options))
diff --git a/hooks/charmhelpers/core/hookenv.py b/hooks/charmhelpers/core/hookenv.py
index 505c202..12f37b2 100644
--- a/hooks/charmhelpers/core/hookenv.py
+++ b/hooks/charmhelpers/core/hookenv.py
@@ -1,22 +1,49 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
1"Interactions with the Juju environment"15"Interactions with the Juju environment"
2# Copyright 2013 Canonical Ltd.16# Copyright 2013 Canonical Ltd.
3#17#
4# Authors:18# Authors:
5# Charm Helpers Developers <juju@lists.ubuntu.com>19# Charm Helpers Developers <juju@lists.ubuntu.com>
620
21from __future__ import print_function
22import copy
23from distutils.version import LooseVersion
24from functools import wraps
25import glob
7import os26import os
8import json27import json
9import yaml28import yaml
10import subprocess29import subprocess
11import sys30import sys
12import UserDict31import errno
32import tempfile
13from subprocess import CalledProcessError33from subprocess import CalledProcessError
1434
35import six
36if not six.PY3:
37 from UserDict import UserDict
38else:
39 from collections import UserDict
40
15CRITICAL = "CRITICAL"41CRITICAL = "CRITICAL"
16ERROR = "ERROR"42ERROR = "ERROR"
17WARNING = "WARNING"43WARNING = "WARNING"
18INFO = "INFO"44INFO = "INFO"
19DEBUG = "DEBUG"45DEBUG = "DEBUG"
46TRACE = "TRACE"
20MARKER = object()47MARKER = object()
2148
22cache = {}49cache = {}
@@ -25,7 +52,7 @@ cache = {}
25def cached(func):52def cached(func):
26 """Cache return values for multiple executions of func + args53 """Cache return values for multiple executions of func + args
2754
28 For example:55 For example::
2956
30 @cached57 @cached
31 def unit_get(attribute):58 def unit_get(attribute):
@@ -35,15 +62,18 @@ def cached(func):
3562
36 will cache the result of unit_get + 'test' for future calls.63 will cache the result of unit_get + 'test' for future calls.
37 """64 """
65 @wraps(func)
38 def wrapper(*args, **kwargs):66 def wrapper(*args, **kwargs):
39 global cache67 global cache
40 key = str((func, args, kwargs))68 key = str((func, args, kwargs))
41 try:69 try:
42 return cache[key]70 return cache[key]
43 except KeyError:71 except KeyError:
44 res = func(*args, **kwargs)72 pass # Drop out of the exception handler scope.
45 cache[key] = res73 res = func(*args, **kwargs)
46 return res74 cache[key] = res
75 return res
76 wrapper._wrapped = func
47 return wrapper77 return wrapper
4878
4979
@@ -63,16 +93,29 @@ def log(message, level=None):
63 command = ['juju-log']93 command = ['juju-log']
64 if level:94 if level:
65 command += ['-l', level]95 command += ['-l', level]
96 if not isinstance(message, six.string_types):
97 message = repr(message)
66 command += [message]98 command += [message]
67 subprocess.call(command)99 # Missing juju-log should not cause failures in unit tests
100 # Send log output to stderr
101 try:
102 subprocess.call(command)
103 except OSError as e:
104 if e.errno == errno.ENOENT:
105 if level:
106 message = "{}: {}".format(level, message)
107 message = "juju-log: {}".format(message)
108 print(message, file=sys.stderr)
109 else:
110 raise
68111
69112
70class Serializable(UserDict.IterableUserDict):113class Serializable(UserDict):
71 """Wrapper, an object that can be serialized to yaml or json"""114 """Wrapper, an object that can be serialized to yaml or json"""
72115
73 def __init__(self, obj):116 def __init__(self, obj):
74 # wrap the object117 # wrap the object
75 UserDict.IterableUserDict.__init__(self)118 UserDict.__init__(self)
76 self.data = obj119 self.data = obj
77120
78 def __getattr__(self, attr):121 def __getattr__(self, attr):
@@ -130,9 +173,19 @@ def relation_type():
130 return os.environ.get('JUJU_RELATION', None)173 return os.environ.get('JUJU_RELATION', None)
131174
132175
133def relation_id():176@cached
134 """The relation ID for the current relation hook"""177def relation_id(relation_name=None, service_or_unit=None):
135 return os.environ.get('JUJU_RELATION_ID', None)178 """The relation ID for the current or a specified relation"""
179 if not relation_name and not service_or_unit:
180 return os.environ.get('JUJU_RELATION_ID', None)
181 elif relation_name and service_or_unit:
182 service_name = service_or_unit.split('/')[0]
183 for relid in relation_ids(relation_name):
184 remote_service = remote_service_name(relid)
185 if remote_service == service_name:
186 return relid
187 else:
188 raise ValueError('Must specify neither or both of relation_name and service_or_unit')
136189
137190
138def local_unit():191def local_unit():
@@ -142,7 +195,7 @@ def local_unit():
142195
143def remote_unit():196def remote_unit():
144 """The remote unit for the current relation hook"""197 """The remote unit for the current relation hook"""
145 return os.environ['JUJU_REMOTE_UNIT']198 return os.environ.get('JUJU_REMOTE_UNIT', None)
146199
147200
148def service_name():201def service_name():
@@ -150,9 +203,149 @@ def service_name():
150 return local_unit().split('/')[0]203 return local_unit().split('/')[0]
151204
152205
206def principal_unit():
207 """Returns the principal unit of this unit, otherwise None"""
208 # Juju 2.2 and above provides JUJU_PRINCIPAL_UNIT
209 principal_unit = os.environ.get('JUJU_PRINCIPAL_UNIT', None)
210 # If it's empty, then this unit is the principal
211 if principal_unit == '':
212 return os.environ['JUJU_UNIT_NAME']
213 elif principal_unit is not None:
214 return principal_unit
215 # For Juju 2.1 and below, let's try work out the principle unit by
216 # the various charms' metadata.yaml.
217 for reltype in relation_types():
218 for rid in relation_ids(reltype):
219 for unit in related_units(rid):
220 md = _metadata_unit(unit)
221 subordinate = md.pop('subordinate', None)
222 if not subordinate:
223 return unit
224 return None
225
226
227@cached
228def remote_service_name(relid=None):
229 """The remote service name for a given relation-id (or the current relation)"""
230 if relid is None:
231 unit = remote_unit()
232 else:
233 units = related_units(relid)
234 unit = units[0] if units else None
235 return unit.split('/')[0] if unit else None
236
237
153def hook_name():238def hook_name():
154 """The name of the currently executing hook"""239 """The name of the currently executing hook"""
155 return os.path.basename(sys.argv[0])240 return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0]))
241
242
243class Config(dict):
244 """A dictionary representation of the charm's config.yaml, with some
245 extra features:
246
247 - See which values in the dictionary have changed since the previous hook.
248 - For values that have changed, see what the previous value was.
249 - Store arbitrary data for use in a later hook.
250
251 NOTE: Do not instantiate this object directly - instead call
252 ``hookenv.config()``, which will return an instance of :class:`Config`.
253
254 Example usage::
255
256 >>> # inside a hook
257 >>> from charmhelpers.core import hookenv
258 >>> config = hookenv.config()
259 >>> config['foo']
260 'bar'
261 >>> # store a new key/value for later use
262 >>> config['mykey'] = 'myval'
263
264
265 >>> # user runs `juju set mycharm foo=baz`
266 >>> # now we're inside subsequent config-changed hook
267 >>> config = hookenv.config()
268 >>> config['foo']
269 'baz'
270 >>> # test to see if this val has changed since last hook
271 >>> config.changed('foo')
272 True
273 >>> # what was the previous value?
274 >>> config.previous('foo')
275 'bar'
276 >>> # keys/values that we add are preserved across hooks
277 >>> config['mykey']
278 'myval'
279
280 """
281 CONFIG_FILE_NAME = '.juju-persistent-config'
282
283 def __init__(self, *args, **kw):
284 super(Config, self).__init__(*args, **kw)
285 self.implicit_save = True
286 self._prev_dict = None
287 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
288 if os.path.exists(self.path):
289 self.load_previous()
290 atexit(self._implicit_save)
291
292 def load_previous(self, path=None):
293 """Load previous copy of config from disk.
294
295 In normal usage you don't need to call this method directly - it
296 is called automatically at object initialization.
297
298 :param path:
299
300 File path from which to load the previous config. If `None`,
301 config is loaded from the default location. If `path` is
302 specified, subsequent `save()` calls will write to the same
303 path.
304
305 """
306 self.path = path or self.path
307 with open(self.path) as f:
308 self._prev_dict = json.load(f)
309 for k, v in copy.deepcopy(self._prev_dict).items():
310 if k not in self:
311 self[k] = v
312
313 def changed(self, key):
314 """Return True if the current value for this key is different from
315 the previous value.
316
317 """
318 if self._prev_dict is None:
319 return True
320 return self.previous(key) != self.get(key)
321
322 def previous(self, key):
323 """Return previous value for this key, or None if there
324 is no previous value.
325
326 """
327 if self._prev_dict:
328 return self._prev_dict.get(key)
329 return None
330
331 def save(self):
332 """Save this config to disk.
333
334 If the charm is using the :mod:`Services Framework <services.base>`
335 or :meth:'@hook <Hooks.hook>' decorator, this
336 is called automatically at the end of successful hook execution.
337 Otherwise, it should be called directly by user code.
338
339 To disable automatic saves, set ``implicit_save=False`` on this
340 instance.
341
342 """
343 with open(self.path, 'w') as f:
344 json.dump(self, f)
345
346 def _implicit_save(self):
347 if self.implicit_save:
348 self.save()
156349
157350
158@cached351@cached
@@ -161,9 +354,15 @@ def config(scope=None):
161 config_cmd_line = ['config-get']354 config_cmd_line = ['config-get']
162 if scope is not None:355 if scope is not None:
163 config_cmd_line.append(scope)356 config_cmd_line.append(scope)
357 else:
358 config_cmd_line.append('--all')
164 config_cmd_line.append('--format=json')359 config_cmd_line.append('--format=json')
165 try:360 try:
166 return json.loads(subprocess.check_output(config_cmd_line))361 config_data = json.loads(
362 subprocess.check_output(config_cmd_line).decode('UTF-8'))
363 if scope is not None:
364 return config_data
365 return Config(config_data)
167 except ValueError:366 except ValueError:
168 return None367 return None
169368
@@ -179,30 +378,62 @@ def relation_get(attribute=None, unit=None, rid=None):
179 if unit:378 if unit:
180 _args.append(unit)379 _args.append(unit)
181 try:380 try:
182 return json.loads(subprocess.check_output(_args))381 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
183 except ValueError:382 except ValueError:
184 return None383 return None
185 except CalledProcessError, e:384 except CalledProcessError as e:
186 if e.returncode == 2:385 if e.returncode == 2:
187 return None386 return None
188 raise387 raise
189388
190389
191def relation_set(relation_id=None, relation_settings={}, **kwargs):390def relation_set(relation_id=None, relation_settings=None, **kwargs):
192 """Set relation information for the current unit"""391 """Set relation information for the current unit"""
392 relation_settings = relation_settings if relation_settings else {}
193 relation_cmd_line = ['relation-set']393 relation_cmd_line = ['relation-set']
394 accepts_file = "--file" in subprocess.check_output(
395 relation_cmd_line + ["--help"], universal_newlines=True)
194 if relation_id is not None:396 if relation_id is not None:
195 relation_cmd_line.extend(('-r', relation_id))397 relation_cmd_line.extend(('-r', relation_id))
196 for k, v in (relation_settings.items() + kwargs.items()):398 settings = relation_settings.copy()
197 if v is None:399 settings.update(kwargs)
198 relation_cmd_line.append('{}='.format(k))400 for key, value in settings.items():
199 else:401 # Force value to be a string: it always should, but some call
200 relation_cmd_line.append('{}={}'.format(k, v))402 # sites pass in things like dicts or numbers.
201 subprocess.check_call(relation_cmd_line)403 if value is not None:
404 settings[key] = "{}".format(value)
405 if accepts_file:
406 # --file was introduced in Juju 1.23.2. Use it by default if
407 # available, since otherwise we'll break if the relation data is
408 # too big. Ideally we should tell relation-set to read the data from
409 # stdin, but that feature is broken in 1.23.2: Bug #1454678.
410 with tempfile.NamedTemporaryFile(delete=False) as settings_file:
411 settings_file.write(yaml.safe_dump(settings).encode("utf-8"))
412 subprocess.check_call(
413 relation_cmd_line + ["--file", settings_file.name])
414 os.remove(settings_file.name)
415 else:
416 for key, value in settings.items():
417 if value is None:
418 relation_cmd_line.append('{}='.format(key))
419 else:
420 relation_cmd_line.append('{}={}'.format(key, value))
421 subprocess.check_call(relation_cmd_line)
202 # Flush cache of any relation-gets for local unit422 # Flush cache of any relation-gets for local unit
203 flush(local_unit())423 flush(local_unit())
204424
205425
426def relation_clear(r_id=None):
427 ''' Clears any relation data already set on relation r_id '''
428 settings = relation_get(rid=r_id,
429 unit=local_unit())
430 for setting in settings:
431 if setting not in ['public-address', 'private-address']:
432 settings[setting] = None
433 relation_set(relation_id=r_id,
434 **settings)
435
436
206@cached437@cached
207def relation_ids(reltype=None):438def relation_ids(reltype=None):
208 """A list of relation_ids"""439 """A list of relation_ids"""
@@ -210,7 +441,8 @@ def relation_ids(reltype=None):
210 relid_cmd_line = ['relation-ids', '--format=json']441 relid_cmd_line = ['relation-ids', '--format=json']
211 if reltype is not None:442 if reltype is not None:
212 relid_cmd_line.append(reltype)443 relid_cmd_line.append(reltype)
213 return json.loads(subprocess.check_output(relid_cmd_line)) or []444 return json.loads(
445 subprocess.check_output(relid_cmd_line).decode('UTF-8')) or []
214 return []446 return []
215447
216448
@@ -221,7 +453,8 @@ def related_units(relid=None):
221 units_cmd_line = ['relation-list', '--format=json']453 units_cmd_line = ['relation-list', '--format=json']
222 if relid is not None:454 if relid is not None:
223 units_cmd_line.extend(('-r', relid))455 units_cmd_line.extend(('-r', relid))
224 return json.loads(subprocess.check_output(units_cmd_line)) or []456 return json.loads(
457 subprocess.check_output(units_cmd_line).decode('UTF-8')) or []
225458
226459
227@cached460@cached
@@ -261,21 +494,116 @@ def relations_of_type(reltype=None):
261494
262495
263@cached496@cached
497def metadata():
498 """Get the current charm metadata.yaml contents as a python object"""
499 with open(os.path.join(charm_dir(), 'metadata.yaml')) as md:
500 return yaml.safe_load(md)
501
502
503def _metadata_unit(unit):
504 """Given the name of a unit (e.g. apache2/0), get the unit charm's
505 metadata.yaml. Very similar to metadata() but allows us to inspect
506 other units. Unit needs to be co-located, such as a subordinate or
507 principal/primary.
508
509 :returns: metadata.yaml as a python object.
510
511 """
512 basedir = os.sep.join(charm_dir().split(os.sep)[:-2])
513 unitdir = 'unit-{}'.format(unit.replace(os.sep, '-'))
514 with open(os.path.join(basedir, unitdir, 'charm', 'metadata.yaml')) as md:
515 return yaml.safe_load(md)
516
517
518@cached
264def relation_types():519def relation_types():
265 """Get a list of relation types supported by this charm"""520 """Get a list of relation types supported by this charm"""
266 charmdir = os.environ.get('CHARM_DIR', '')
267 mdf = open(os.path.join(charmdir, 'metadata.yaml'))
268 md = yaml.safe_load(mdf)
269 rel_types = []521 rel_types = []
522 md = metadata()
270 for key in ('provides', 'requires', 'peers'):523 for key in ('provides', 'requires', 'peers'):
271 section = md.get(key)524 section = md.get(key)
272 if section:525 if section:
273 rel_types.extend(section.keys())526 rel_types.extend(section.keys())
274 mdf.close()
275 return rel_types527 return rel_types
276528
277529
278@cached530@cached
531def peer_relation_id():
532 '''Get the peers relation id if a peers relation has been joined, else None.'''
533 md = metadata()
534 section = md.get('peers')
535 if section:
536 for key in section:
537 relids = relation_ids(key)
538 if relids:
539 return relids[0]
540 return None
541
542
543@cached
544def relation_to_interface(relation_name):
545 """
546 Given the name of a relation, return the interface that relation uses.
547
548 :returns: The interface name, or ``None``.
549 """
550 return relation_to_role_and_interface(relation_name)[1]
551
552
553@cached
554def relation_to_role_and_interface(relation_name):
555 """
556 Given the name of a relation, return the role and the name of the interface
557 that relation uses (where role is one of ``provides``, ``requires``, or ``peers``).
558
559 :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
560 """
561 _metadata = metadata()
562 for role in ('provides', 'requires', 'peers'):
563 interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
564 if interface:
565 return role, interface
566 return None, None
567
568
569@cached
570def role_and_interface_to_relations(role, interface_name):
571 """
572 Given a role and interface name, return a list of relation names for the
573 current charm that use that interface under that role (where role is one
574 of ``provides``, ``requires``, or ``peers``).
575
576 :returns: A list of relation names.
577 """
578 _metadata = metadata()
579 results = []
580 for relation_name, relation in _metadata.get(role, {}).items():
581 if relation['interface'] == interface_name:
582 results.append(relation_name)
583 return results
584
585
586@cached
587def interface_to_relations(interface_name):
588 """
589 Given an interface, return a list of relation names for the current
590 charm that use that interface.
591
592 :returns: A list of relation names.
593 """
594 results = []
595 for role in ('provides', 'requires', 'peers'):
596 results.extend(role_and_interface_to_relations(role, interface_name))
597 return results
598
599
600@cached
601def charm_name():
602 """Get the name of the current charm as is specified on metadata.yaml"""
603 return metadata().get('name')
604
605
606@cached
279def relations():607def relations():
280 """Get a nested dictionary of relation data for all related units"""608 """Get a nested dictionary of relation data for all related units"""
281 rels = {}609 rels = {}
@@ -325,21 +653,72 @@ def close_port(port, protocol="TCP"):
325 subprocess.check_call(_args)653 subprocess.check_call(_args)
326654
327655
656def open_ports(start, end, protocol="TCP"):
657 """Opens a range of service network ports"""
658 _args = ['open-port']
659 _args.append('{}-{}/{}'.format(start, end, protocol))
660 subprocess.check_call(_args)
661
662
663def close_ports(start, end, protocol="TCP"):
664 """Close a range of service network ports"""
665 _args = ['close-port']
666 _args.append('{}-{}/{}'.format(start, end, protocol))
667 subprocess.check_call(_args)
668
669
328@cached670@cached
329def unit_get(attribute):671def unit_get(attribute):
330 """Get the unit ID for the remote unit"""672 """Get the unit ID for the remote unit"""
331 _args = ['unit-get', '--format=json', attribute]673 _args = ['unit-get', '--format=json', attribute]
332 try:674 try:
333 return json.loads(subprocess.check_output(_args))675 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
334 except ValueError:676 except ValueError:
335 return None677 return None
336678
337679
680def unit_public_ip():
681 """Get this unit's public IP address"""
682 return unit_get('public-address')
683
684
338def unit_private_ip():685def unit_private_ip():
339 """Get this unit's private IP address"""686 """Get this unit's private IP address"""
340 return unit_get('private-address')687 return unit_get('private-address')
341688
342689
690@cached
691def storage_get(attribute=None, storage_id=None):
692 """Get storage attributes"""
693 _args = ['storage-get', '--format=json']
694 if storage_id:
695 _args.extend(('-s', storage_id))
696 if attribute:
697 _args.append(attribute)
698 try:
699 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
700 except ValueError:
701 return None
702
703
704@cached
705def storage_list(storage_name=None):
706 """List the storage IDs for the unit"""
707 _args = ['storage-list', '--format=json']
708 if storage_name:
709 _args.append(storage_name)
710 try:
711 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
712 except ValueError:
713 return None
714 except OSError as e:
715 import errno
716 if e.errno == errno.ENOENT:
717 # storage-list does not exist
718 return []
719 raise
720
721
343class UnregisteredHookError(Exception):722class UnregisteredHookError(Exception):
344 """Raised when an undefined hook is called"""723 """Raised when an undefined hook is called"""
345 pass724 pass
@@ -348,37 +727,50 @@ class UnregisteredHookError(Exception):
348class Hooks(object):727class Hooks(object):
349 """A convenient handler for hook functions.728 """A convenient handler for hook functions.
350729
351 Example:730 Example::
731
352 hooks = Hooks()732 hooks = Hooks()
353733
354 # register a hook, taking its name from the function name734 # register a hook, taking its name from the function name
355 @hooks.hook()735 @hooks.hook()
356 def install():736 def install():
357 ...737 pass # your code here
358738
359 # register a hook, providing a custom hook name739 # register a hook, providing a custom hook name
360 @hooks.hook("config-changed")740 @hooks.hook("config-changed")
361 def config_changed():741 def config_changed():
362 ...742 pass # your code here
363743
364 if __name__ == "__main__":744 if __name__ == "__main__":
365 # execute a hook based on the name the program is called by745 # execute a hook based on the name the program is called by
366 hooks.execute(sys.argv)746 hooks.execute(sys.argv)
367 """747 """
368748
369 def __init__(self):749 def __init__(self, config_save=None):
370 super(Hooks, self).__init__()750 super(Hooks, self).__init__()
371 self._hooks = {}751 self._hooks = {}
372752
753 # For unknown reasons, we allow the Hooks constructor to override
754 # config().implicit_save.
755 if config_save is not None:
756 config().implicit_save = config_save
757
373 def register(self, name, function):758 def register(self, name, function):
374 """Register a hook"""759 """Register a hook"""
375 self._hooks[name] = function760 self._hooks[name] = function
376761
377 def execute(self, args):762 def execute(self, args):
378 """Execute a registered hook based on args[0]"""763 """Execute a registered hook based on args[0]"""
764 _run_atstart()
379 hook_name = os.path.basename(args[0])765 hook_name = os.path.basename(args[0])
380 if hook_name in self._hooks:766 if hook_name in self._hooks:
381 self._hooks[hook_name]()767 try:
768 self._hooks[hook_name]()
769 except SystemExit as x:
770 if x.code is None or x.code == 0:
771 _run_atexit()
772 raise
773 _run_atexit()
382 else:774 else:
383 raise UnregisteredHookError(hook_name)775 raise UnregisteredHookError(hook_name)
384776
@@ -398,4 +790,319 @@ class Hooks(object):
398790
399def charm_dir():791def charm_dir():
400 """Return the root directory of the current charm"""792 """Return the root directory of the current charm"""
793 d = os.environ.get('JUJU_CHARM_DIR')
794 if d is not None:
795 return d
401 return os.environ.get('CHARM_DIR')796 return os.environ.get('CHARM_DIR')
797
798
799@cached
800def action_get(key=None):
801 """Gets the value of an action parameter, or all key/value param pairs"""
802 cmd = ['action-get']
803 if key is not None:
804 cmd.append(key)
805 cmd.append('--format=json')
806 action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
807 return action_data
808
809
810def action_set(values):
811 """Sets the values to be returned after the action finishes"""
812 cmd = ['action-set']
813 for k, v in list(values.items()):
814 cmd.append('{}={}'.format(k, v))
815 subprocess.check_call(cmd)
816
817
818def action_fail(message):
819 """Sets the action status to failed and sets the error message.
820
821 The results set by action_set are preserved."""
822 subprocess.check_call(['action-fail', message])
823
824
825def action_name():
826 """Get the name of the currently executing action."""
827 return os.environ.get('JUJU_ACTION_NAME')
828
829
830def action_uuid():
831 """Get the UUID of the currently executing action."""
832 return os.environ.get('JUJU_ACTION_UUID')
833
834
835def action_tag():
836 """Get the tag for the currently executing action."""
837 return os.environ.get('JUJU_ACTION_TAG')
838
839
840def status_set(workload_state, message):
841 """Set the workload state with a message
842
843 Use status-set to set the workload state with a message which is visible
844 to the user via juju status. If the status-set command is not found then
845 assume this is juju < 1.23 and juju-log the message unstead.
846
847 workload_state -- valid juju workload state.
848 message -- status update message
849 """
850 valid_states = ['maintenance', 'blocked', 'waiting', 'active']
851 if workload_state not in valid_states:
852 raise ValueError(
853 '{!r} is not a valid workload state'.format(workload_state)
854 )
855 cmd = ['status-set', workload_state, message]
856 try:
857 ret = subprocess.call(cmd)
858 if ret == 0:
859 return
860 except OSError as e:
861 if e.errno != errno.ENOENT:
862 raise
863 log_message = 'status-set failed: {} {}'.format(workload_state,
864 message)
865 log(log_message, level='INFO')
866
867
868def status_get():
869 """Retrieve the previously set juju workload state and message
870
871 If the status-get command is not found then assume this is juju < 1.23 and
872 return 'unknown', ""
873
874 """
875 cmd = ['status-get', "--format=json", "--include-data"]
876 try:
877 raw_status = subprocess.check_output(cmd)
878 except OSError as e:
879 if e.errno == errno.ENOENT:
880 return ('unknown', "")
881 else:
882 raise
883 else:
884 status = json.loads(raw_status.decode("UTF-8"))
885 return (status["status"], status["message"])
886
887
888def translate_exc(from_exc, to_exc):
889 def inner_translate_exc1(f):
890 @wraps(f)
891 def inner_translate_exc2(*args, **kwargs):
892 try:
893 return f(*args, **kwargs)
894 except from_exc:
895 raise to_exc
896
897 return inner_translate_exc2
898
899 return inner_translate_exc1
900
901
902def application_version_set(version):
903 """Charm authors may trigger this command from any hook to output what
904 version of the application is running. This could be a package version,
905 for instance postgres version 9.5. It could also be a build number or
906 version control revision identifier, for instance git sha 6fb7ba68. """
907
908 cmd = ['application-version-set']
909 cmd.append(version)
910 try:
911 subprocess.check_call(cmd)
912 except OSError:
913 log("Application Version: {}".format(version))
914
915
916@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
917def is_leader():
918 """Does the current unit hold the juju leadership
919
920 Uses juju to determine whether the current unit is the leader of its peers
921 """
922 cmd = ['is-leader', '--format=json']
923 return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
924
925
926@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
927def leader_get(attribute=None):
928 """Juju leader get value(s)"""
929 cmd = ['leader-get', '--format=json'] + [attribute or '-']
930 return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
931
932
933@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
934def leader_set(settings=None, **kwargs):
935 """Juju leader set value(s)"""
936 # Don't log secrets.
937 # log("Juju leader-set '%s'" % (settings), level=DEBUG)
938 cmd = ['leader-set']
939 settings = settings or {}
940 settings.update(kwargs)
941 for k, v in settings.items():
942 if v is None:
943 cmd.append('{}='.format(k))
944 else:
945 cmd.append('{}={}'.format(k, v))
946 subprocess.check_call(cmd)
947
948
949@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
950def payload_register(ptype, klass, pid):
951 """ is used while a hook is running to let Juju know that a
952 payload has been started."""
953 cmd = ['payload-register']
954 for x in [ptype, klass, pid]:
955 cmd.append(x)
956 subprocess.check_call(cmd)
957
958
959@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
960def payload_unregister(klass, pid):
961 """ is used while a hook is running to let Juju know
962 that a payload has been manually stopped. The <class> and <id> provided
963 must match a payload that has been previously registered with juju using
964 payload-register."""
965 cmd = ['payload-unregister']
966 for x in [klass, pid]:
967 cmd.append(x)
968 subprocess.check_call(cmd)
969
970
971@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
972def payload_status_set(klass, pid, status):
973 """is used to update the current status of a registered payload.
974 The <class> and <id> provided must match a payload that has been previously
975 registered with juju using payload-register. The <status> must be one of the
976 follow: starting, started, stopping, stopped"""
977 cmd = ['payload-status-set']
978 for x in [klass, pid, status]:
979 cmd.append(x)
980 subprocess.check_call(cmd)
981
982
983@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
984def resource_get(name):
985 """used to fetch the resource path of the given name.
986
987 <name> must match a name of defined resource in metadata.yaml
988
989 returns either a path or False if resource not available
990 """
991 if not name:
992 return False
993
994 cmd = ['resource-get', name]
995 try:
996 return subprocess.check_output(cmd).decode('UTF-8')
997 except subprocess.CalledProcessError:
998 return False
999
1000
1001@cached
1002def juju_version():
1003 """Full version string (eg. '1.23.3.1-trusty-amd64')"""
1004 # Per https://bugs.launchpad.net/juju-core/+bug/1455368/comments/1
1005 jujud = glob.glob('/var/lib/juju/tools/machine-*/jujud')[0]
1006 return subprocess.check_output([jujud, 'version'],
1007 universal_newlines=True).strip()
1008
1009
1010@cached
1011def has_juju_version(minimum_version):
1012 """Return True if the Juju version is at least the provided version"""
1013 return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
1014
1015
1016_atexit = []
1017_atstart = []
1018
1019
1020def atstart(callback, *args, **kwargs):
1021 '''Schedule a callback to run before the main hook.
1022
1023 Callbacks are run in the order they were added.
1024
1025 This is useful for modules and classes to perform initialization
1026 and inject behavior. In particular:
1027
1028 - Run common code before all of your hooks, such as logging
1029 the hook name or interesting relation data.
1030 - Defer object or module initialization that requires a hook
1031 context until we know there actually is a hook context,
1032 making testing easier.
1033 - Rather than requiring charm authors to include boilerplate to
1034 invoke your helper's behavior, have it run automatically if
1035 your object is instantiated or module imported.
1036
1037 This is not at all useful after your hook framework as been launched.
1038 '''
1039 global _atstart
1040 _atstart.append((callback, args, kwargs))
1041
1042
1043def atexit(callback, *args, **kwargs):
1044 '''Schedule a callback to run on successful hook completion.
1045
1046 Callbacks are run in the reverse order that they were added.'''
1047 _atexit.append((callback, args, kwargs))
1048
1049
1050def _run_atstart():
1051 '''Hook frameworks must invoke this before running the main hook body.'''
1052 global _atstart
1053 for callback, args, kwargs in _atstart:
1054 callback(*args, **kwargs)
1055 del _atstart[:]
1056
1057
1058def _run_atexit():
1059 '''Hook frameworks must invoke this after the main hook body has
1060 successfully completed. Do not invoke it if the hook fails.'''
1061 global _atexit
1062 for callback, args, kwargs in reversed(_atexit):
1063 callback(*args, **kwargs)
1064 del _atexit[:]
1065
1066
1067@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1068def network_get_primary_address(binding):
1069 '''
1070 Retrieve the primary network address for a named binding
1071
1072 :param binding: string. The name of a relation of extra-binding
1073 :return: string. The primary IP address for the named binding
1074 :raise: NotImplementedError if run on Juju < 2.0
1075 '''
1076 cmd = ['network-get', '--primary-address', binding]
1077 return subprocess.check_output(cmd).decode('UTF-8').strip()
1078
1079
1080def add_metric(*args, **kwargs):
1081 """Add metric values. Values may be expressed with keyword arguments. For
1082 metric names containing dashes, these may be expressed as one or more
1083 'key=value' positional arguments. May only be called from the collect-metrics
1084 hook."""
1085 _args = ['add-metric']
1086 _kvpairs = []
1087 _kvpairs.extend(args)
1088 _kvpairs.extend(['{}={}'.format(k, v) for k, v in kwargs.items()])
1089 _args.extend(sorted(_kvpairs))
1090 try:
1091 subprocess.check_call(_args)
1092 return
1093 except EnvironmentError as e:
1094 if e.errno != errno.ENOENT:
1095 raise
1096 log_message = 'add-metric failed: {}'.format(' '.join(_kvpairs))
1097 log(log_message, level='INFO')
1098
1099
1100def meter_status():
1101 """Get the meter status, if running in the meter-status-changed hook."""
1102 return os.environ.get('JUJU_METER_STATUS')
1103
1104
1105def meter_info():
1106 """Get the meter status information, if running in the meter-status-changed
1107 hook."""
1108 return os.environ.get('JUJU_METER_INFO')
diff --git a/hooks/charmhelpers/core/host.py b/hooks/charmhelpers/core/host.py
index cfd2684..5656e2f 100644
--- a/hooks/charmhelpers/core/host.py
+++ b/hooks/charmhelpers/core/host.py
@@ -1,3 +1,17 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
1"""Tools for working with the host system"""15"""Tools for working with the host system"""
2# Copyright 2012 Canonical Ltd.16# Copyright 2012 Canonical Ltd.
3#17#
@@ -6,68 +20,332 @@
6# Matthew Wedgwood <matthew.wedgwood@canonical.com>20# Matthew Wedgwood <matthew.wedgwood@canonical.com>
721
8import os22import os
23import re
9import pwd24import pwd
25import glob
10import grp26import grp
11import random27import random
12import string28import string
13import subprocess29import subprocess
14import hashlib30import hashlib
31import functools
32import itertools
33import six
1534
35from contextlib import contextmanager
16from collections import OrderedDict36from collections import OrderedDict
37from .hookenv import log, DEBUG
38from .fstab import Fstab
39from charmhelpers.osplatform import get_platform
40
41__platform__ = get_platform()
42if __platform__ == "ubuntu":
43 from charmhelpers.core.host_factory.ubuntu import (
44 service_available,
45 add_new_group,
46 lsb_release,
47 cmp_pkgrevno,
48 CompareHostReleases,
49 ) # flake8: noqa -- ignore F401 for this import
50elif __platform__ == "centos":
51 from charmhelpers.core.host_factory.centos import (
52 service_available,
53 add_new_group,
54 lsb_release,
55 cmp_pkgrevno,
56 CompareHostReleases,
57 ) # flake8: noqa -- ignore F401 for this import
58
59UPDATEDB_PATH = '/etc/updatedb.conf'
60
61def service_start(service_name, **kwargs):
62 """Start a system service.
63
64 The specified service name is managed via the system level init system.
65 Some init systems (e.g. upstart) require that additional arguments be
66 provided in order to directly control service instances whereas other init
67 systems allow for addressing instances of a service directly by name (e.g.
68 systemd).
69
70 The kwargs allow for the additional parameters to be passed to underlying
71 init systems for those systems which require/allow for them. For example,
72 the ceph-osd upstart script requires the id parameter to be passed along
73 in order to identify which running daemon should be reloaded. The follow-
74 ing example stops the ceph-osd service for instance id=4:
75
76 service_stop('ceph-osd', id=4)
77
78 :param service_name: the name of the service to stop
79 :param **kwargs: additional parameters to pass to the init system when
80 managing services. These will be passed as key=value
81 parameters to the init system's commandline. kwargs
82 are ignored for systemd enabled systems.
83 """
84 return service('start', service_name, **kwargs)
1785
18from hookenv import log
1986
87def service_stop(service_name, **kwargs):
88 """Stop a system service.
89
90 The specified service name is managed via the system level init system.
91 Some init systems (e.g. upstart) require that additional arguments be
92 provided in order to directly control service instances whereas other init
93 systems allow for addressing instances of a service directly by name (e.g.
94 systemd).
95
96 The kwargs allow for the additional parameters to be passed to underlying
97 init systems for those systems which require/allow for them. For example,
98 the ceph-osd upstart script requires the id parameter to be passed along
99 in order to identify which running daemon should be reloaded. The follow-
100 ing example stops the ceph-osd service for instance id=4:
101
102 service_stop('ceph-osd', id=4)
103
104 :param service_name: the name of the service to stop
105 :param **kwargs: additional parameters to pass to the init system when
106 managing services. These will be passed as key=value
107 parameters to the init system's commandline. kwargs
108 are ignored for systemd enabled systems.
109 """
110 return service('stop', service_name, **kwargs)
20111
21def service_start(service_name):
22 """Start a system service"""
23 return service('start', service_name)
24112
113def service_restart(service_name, **kwargs):
114 """Restart a system service.
25115
26def service_stop(service_name):116 The specified service name is managed via the system level init system.
27 """Stop a system service"""117 Some init systems (e.g. upstart) require that additional arguments be
28 return service('stop', service_name)118 provided in order to directly control service instances whereas other init
119 systems allow for addressing instances of a service directly by name (e.g.
120 systemd).
29121
122 The kwargs allow for the additional parameters to be passed to underlying
123 init systems for those systems which require/allow for them. For example,
124 the ceph-osd upstart script requires the id parameter to be passed along
125 in order to identify which running daemon should be restarted. The follow-
126 ing example restarts the ceph-osd service for instance id=4:
30127
31def service_restart(service_name):128 service_restart('ceph-osd', id=4)
32 """Restart a system service"""129
130 :param service_name: the name of the service to restart
131 :param **kwargs: additional parameters to pass to the init system when
132 managing services. These will be passed as key=value
133 parameters to the init system's commandline. kwargs
134 are ignored for init systems not allowing additional
135 parameters via the commandline (systemd).
136 """
33 return service('restart', service_name)137 return service('restart', service_name)
34138
35139
36def service_reload(service_name, restart_on_failure=False):140def service_reload(service_name, restart_on_failure=False, **kwargs):
37 """Reload a system service, optionally falling back to restart if reload fails"""141 """Reload a system service, optionally falling back to restart if
38 service_result = service('reload', service_name)142 reload fails.
143
144 The specified service name is managed via the system level init system.
145 Some init systems (e.g. upstart) require that additional arguments be
146 provided in order to directly control service instances whereas other init
147 systems allow for addressing instances of a service directly by name (e.g.
148 systemd).
149
150 The kwargs allow for the additional parameters to be passed to underlying
151 init systems for those systems which require/allow for them. For example,
152 the ceph-osd upstart script requires the id parameter to be passed along
153 in order to identify which running daemon should be reloaded. The follow-
154 ing example restarts the ceph-osd service for instance id=4:
155
156 service_reload('ceph-osd', id=4)
157
158 :param service_name: the name of the service to reload
159 :param restart_on_failure: boolean indicating whether to fallback to a
160 restart if the reload fails.
161 :param **kwargs: additional parameters to pass to the init system when
162 managing services. These will be passed as key=value
163 parameters to the init system's commandline. kwargs
164 are ignored for init systems not allowing additional
165 parameters via the commandline (systemd).
166 """
167 service_result = service('reload', service_name, **kwargs)
39 if not service_result and restart_on_failure:168 if not service_result and restart_on_failure:
40 service_result = service('restart', service_name)169 service_result = service('restart', service_name, **kwargs)
41 return service_result170 return service_result
42171
43172
44def service(action, service_name):173def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d",
45 """Control a system service"""174 **kwargs):
46 cmd = ['service', service_name, action]175 """Pause a system service.
176
177 Stop it, and prevent it from starting again at boot.
178
179 :param service_name: the name of the service to pause
180 :param init_dir: path to the upstart init directory
181 :param initd_dir: path to the sysv init directory
182 :param **kwargs: additional parameters to pass to the init system when
183 managing services. These will be passed as key=value
184 parameters to the init system's commandline. kwargs
185 are ignored for init systems which do not support
186 key=value arguments via the commandline.
187 """
188 stopped = True
189 if service_running(service_name, **kwargs):
190 stopped = service_stop(service_name, **kwargs)
191 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
192 sysv_file = os.path.join(initd_dir, service_name)
193 if init_is_systemd():
194 service('disable', service_name)
195 service('mask', service_name)
196 elif os.path.exists(upstart_file):
197 override_path = os.path.join(
198 init_dir, '{}.override'.format(service_name))
199 with open(override_path, 'w') as fh:
200 fh.write("manual\n")
201 elif os.path.exists(sysv_file):
202 subprocess.check_call(["update-rc.d", service_name, "disable"])
203 else:
204 raise ValueError(
205 "Unable to detect {0} as SystemD, Upstart {1} or"
206 " SysV {2}".format(
207 service_name, upstart_file, sysv_file))
208 return stopped
209
210
211def service_resume(service_name, init_dir="/etc/init",
212 initd_dir="/etc/init.d", **kwargs):
213 """Resume a system service.
214
215 Reenable starting again at boot. Start the service.
216
217 :param service_name: the name of the service to resume
218 :param init_dir: the path to the init dir
219 :param initd dir: the path to the initd dir
220 :param **kwargs: additional parameters to pass to the init system when
221 managing services. These will be passed as key=value
222 parameters to the init system's commandline. kwargs
223 are ignored for systemd enabled systems.
224 """
225 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
226 sysv_file = os.path.join(initd_dir, service_name)
227 if init_is_systemd():
228 service('unmask', service_name)
229 service('enable', service_name)
230 elif os.path.exists(upstart_file):
231 override_path = os.path.join(
232 init_dir, '{}.override'.format(service_name))
233 if os.path.exists(override_path):
234 os.unlink(override_path)
235 elif os.path.exists(sysv_file):
236 subprocess.check_call(["update-rc.d", service_name, "enable"])
237 else:
238 raise ValueError(
239 "Unable to detect {0} as SystemD, Upstart {1} or"
240 " SysV {2}".format(
241 service_name, upstart_file, sysv_file))
242 started = service_running(service_name, **kwargs)
243
244 if not started:
245 started = service_start(service_name, **kwargs)
246 return started
247
248
249def service(action, service_name, **kwargs):
250 """Control a system service.
251
252 :param action: the action to take on the service
253 :param service_name: the name of the service to perform th action on
254 :param **kwargs: additional params to be passed to the service command in
255 the form of key=value.
256 """
257 if init_is_systemd():
258 cmd = ['systemctl', action, service_name]
259 else:
260 cmd = ['service', service_name, action]
261 for key, value in six.iteritems(kwargs):
262 parameter = '%s=%s' % (key, value)
263 cmd.append(parameter)
47 return subprocess.call(cmd) == 0264 return subprocess.call(cmd) == 0
48265
49266
50def service_running(service):267_UPSTART_CONF = "/etc/init/{}.conf"
51 """Determine whether a system service is running"""268_INIT_D_CONF = "/etc/init.d/{}"
52 try:269
53 output = subprocess.check_output(['service', service, 'status'])270
54 except subprocess.CalledProcessError:271def service_running(service_name, **kwargs):
55 return False272 """Determine whether a system service is running.
273
274 :param service_name: the name of the service
275 :param **kwargs: additional args to pass to the service command. This is
276 used to pass additional key=value arguments to the
277 service command line for managing specific instance
278 units (e.g. service ceph-osd status id=2). The kwargs
279 are ignored in systemd services.
280 """
281 if init_is_systemd():
282 return service('is-active', service_name)
56 else:283 else:
57 if ("start/running" in output or "is running" in output):284 if os.path.exists(_UPSTART_CONF.format(service_name)):
58 return True285 try:
59 else:286 cmd = ['status', service_name]
60 return False287 for key, value in six.iteritems(kwargs):
288 parameter = '%s=%s' % (key, value)
289 cmd.append(parameter)
290 output = subprocess.check_output(cmd,
291 stderr=subprocess.STDOUT).decode('UTF-8')
292 except subprocess.CalledProcessError:
293 return False
294 else:
295 # This works for upstart scripts where the 'service' command
296 # returns a consistent string to represent running
297 # 'start/running'
298 if ("start/running" in output or
299 "is running" in output or
300 "up and running" in output):
301 return True
302 elif os.path.exists(_INIT_D_CONF.format(service_name)):
303 # Check System V scripts init script return codes
304 return service('status', service_name)
305 return False
306
61307
308SYSTEMD_SYSTEM = '/run/systemd/system'
62309
63def adduser(username, password=None, shell='/bin/bash', system_user=False):310
64 """Add a user to the system"""311def init_is_systemd():
312 """Return True if the host system uses systemd, False otherwise."""
313 if lsb_release()['DISTRIB_CODENAME'] == 'trusty':
314 return False
315 return os.path.isdir(SYSTEMD_SYSTEM)
316
317
318def adduser(username, password=None, shell='/bin/bash',
319 system_user=False, primary_group=None,
320 secondary_groups=None, uid=None, home_dir=None):
321 """Add a user to the system.
322
323 Will log but otherwise succeed if the user already exists.
324
325 :param str username: Username to create
326 :param str password: Password for user; if ``None``, create a system user
327 :param str shell: The default shell for the user
328 :param bool system_user: Whether to create a login or system user
329 :param str primary_group: Primary group for user; defaults to username
330 :param list secondary_groups: Optional list of additional groups
331 :param int uid: UID for user being created
332 :param str home_dir: Home directory for user
333
334 :returns: The password database entry struct, as returned by `pwd.getpwnam`
335 """
65 try:336 try:
66 user_info = pwd.getpwnam(username)337 user_info = pwd.getpwnam(username)
67 log('user {0} already exists!'.format(username))338 log('user {0} already exists!'.format(username))
339 if uid:
340 user_info = pwd.getpwuid(int(uid))
341 log('user with uid {0} already exists!'.format(uid))
68 except KeyError:342 except KeyError:
69 log('creating user {0}'.format(username))343 log('creating user {0}'.format(username))
70 cmd = ['useradd']344 cmd = ['useradd']
345 if uid:
346 cmd.extend(['--uid', str(uid)])
347 if home_dir:
348 cmd.extend(['--home', str(home_dir)])
71 if system_user or password is None:349 if system_user or password is None:
72 cmd.append('--system')350 cmd.append('--system')
73 else:351 else:
@@ -76,32 +354,104 @@ def adduser(username, password=None, shell='/bin/bash', system_user=False):
76 '--shell', shell,354 '--shell', shell,
77 '--password', password,355 '--password', password,
78 ])356 ])
357 if not primary_group:
358 try:
359 grp.getgrnam(username)
360 primary_group = username # avoid "group exists" error
361 except KeyError:
362 pass
363 if primary_group:
364 cmd.extend(['-g', primary_group])
365 if secondary_groups:
366 cmd.extend(['-G', ','.join(secondary_groups)])
79 cmd.append(username)367 cmd.append(username)
80 subprocess.check_call(cmd)368 subprocess.check_call(cmd)
81 user_info = pwd.getpwnam(username)369 user_info = pwd.getpwnam(username)
82 return user_info370 return user_info
83371
84372
373def user_exists(username):
374 """Check if a user exists"""
375 try:
376 pwd.getpwnam(username)
377 user_exists = True
378 except KeyError:
379 user_exists = False
380 return user_exists
381
382
383def uid_exists(uid):
384 """Check if a uid exists"""
385 try:
386 pwd.getpwuid(uid)
387 uid_exists = True
388 except KeyError:
389 uid_exists = False
390 return uid_exists
391
392
393def group_exists(groupname):
394 """Check if a group exists"""
395 try:
396 grp.getgrnam(groupname)
397 group_exists = True
398 except KeyError:
399 group_exists = False
400 return group_exists
401
402
403def gid_exists(gid):
404 """Check if a gid exists"""
405 try:
406 grp.getgrgid(gid)
407 gid_exists = True
408 except KeyError:
409 gid_exists = False
410 return gid_exists
411
412
413def add_group(group_name, system_group=False, gid=None):
414 """Add a group to the system
415
416 Will log but otherwise succeed if the group already exists.
417
418 :param str group_name: group to create
419 :param bool system_group: Create system group
420 :param int gid: GID for user being created
421
422 :returns: The password database entry struct, as returned by `grp.getgrnam`
423 """
424 try:
425 group_info = grp.getgrnam(group_name)
426 log('group {0} already exists!'.format(group_name))
427 if gid:
428 group_info = grp.getgrgid(gid)
429 log('group with gid {0} already exists!'.format(gid))
430 except KeyError:
431 log('creating group {0}'.format(group_name))
432 add_new_group(group_name, system_group, gid)
433 group_info = grp.getgrnam(group_name)
434 return group_info
435
436
85def add_user_to_group(username, group):437def add_user_to_group(username, group):
86 """Add a user to a group"""438 """Add a user to a group"""
87 cmd = [439 cmd = ['gpasswd', '-a', username, group]
88 'gpasswd', '-a',
89 username,
90 group
91 ]
92 log("Adding user {} to group {}".format(username, group))440 log("Adding user {} to group {}".format(username, group))
93 subprocess.check_call(cmd)441 subprocess.check_call(cmd)
94442
95443
96def rsync(from_path, to_path, flags='-r', options=None):444def rsync(from_path, to_path, flags='-r', options=None, timeout=None):
97 """Replicate the contents of a path"""445 """Replicate the contents of a path"""
98 options = options or ['--delete', '--executability']446 options = options or ['--delete', '--executability']
99 cmd = ['/usr/bin/rsync', flags]447 cmd = ['/usr/bin/rsync', flags]
448 if timeout:
449 cmd = ['timeout', str(timeout)] + cmd
100 cmd.extend(options)450 cmd.extend(options)
101 cmd.append(from_path)451 cmd.append(from_path)
102 cmd.append(to_path)452 cmd.append(to_path)
103 log(" ".join(cmd))453 log(" ".join(cmd))
104 return subprocess.check_output(cmd).strip()454 return subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode('UTF-8').strip()
105455
106456
107def symlink(source, destination):457def symlink(source, destination):
@@ -116,34 +466,71 @@ def symlink(source, destination):
116 subprocess.check_call(cmd)466 subprocess.check_call(cmd)
117467
118468
119def mkdir(path, owner='root', group='root', perms=0555, force=False):469def mkdir(path, owner='root', group='root', perms=0o555, force=False):
120 """Create a directory"""470 """Create a directory"""
121 log("Making dir {} {}:{} {:o}".format(path, owner, group,471 log("Making dir {} {}:{} {:o}".format(path, owner, group,
122 perms))472 perms))
123 uid = pwd.getpwnam(owner).pw_uid473 uid = pwd.getpwnam(owner).pw_uid
124 gid = grp.getgrnam(group).gr_gid474 gid = grp.getgrnam(group).gr_gid
125 realpath = os.path.abspath(path)475 realpath = os.path.abspath(path)
126 if os.path.exists(realpath):476 path_exists = os.path.exists(realpath)
127 if force and not os.path.isdir(realpath):477 if path_exists and force:
478 if not os.path.isdir(realpath):
128 log("Removing non-directory file {} prior to mkdir()".format(path))479 log("Removing non-directory file {} prior to mkdir()".format(path))
129 os.unlink(realpath)480 os.unlink(realpath)
130 else:481 os.makedirs(realpath, perms)
482 elif not path_exists:
131 os.makedirs(realpath, perms)483 os.makedirs(realpath, perms)
132 os.chown(realpath, uid, gid)484 os.chown(realpath, uid, gid)
485 os.chmod(realpath, perms)
133486
134487
135def write_file(path, content, owner='root', group='root', perms=0444):488def write_file(path, content, owner='root', group='root', perms=0o444):
136 """Create or overwrite a file with the contents of a string"""489 """Create or overwrite a file with the contents of a byte string."""
137 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
138 uid = pwd.getpwnam(owner).pw_uid490 uid = pwd.getpwnam(owner).pw_uid
139 gid = grp.getgrnam(group).gr_gid491 gid = grp.getgrnam(group).gr_gid
140 with open(path, 'w') as target:492 # lets see if we can grab the file and compare the context, to avoid doing
141 os.fchown(target.fileno(), uid, gid)493 # a write.
142 os.fchmod(target.fileno(), perms)494 existing_content = None
143 target.write(content)495 existing_uid, existing_gid = None, None
144496 try:
145497 with open(path, 'rb') as target:
146def mount(device, mountpoint, options=None, persist=False):498 existing_content = target.read()
499 stat = os.stat(path)
500 existing_uid, existing_gid = stat.st_uid, stat.st_gid
501 except:
502 pass
503 if content != existing_content:
504 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms),
505 level=DEBUG)
506 with open(path, 'wb') as target:
507 os.fchown(target.fileno(), uid, gid)
508 os.fchmod(target.fileno(), perms)
509 target.write(content)
510 return
511 # the contents were the same, but we might still need to change the
512 # ownership.
513 if existing_uid != uid:
514 log("Changing uid on already existing content: {} -> {}"
515 .format(existing_uid, uid), level=DEBUG)
516 os.chown(path, uid, -1)
517 if existing_gid != gid:
518 log("Changing gid on already existing content: {} -> {}"
519 .format(existing_gid, gid), level=DEBUG)
520 os.chown(path, -1, gid)
521
522
523def fstab_remove(mp):
524 """Remove the given mountpoint entry from /etc/fstab"""
525 return Fstab.remove_by_mountpoint(mp)
526
527
528def fstab_add(dev, mp, fs, options=None):
529 """Adds the given device entry to the /etc/fstab file"""
530 return Fstab.add(dev, mp, fs, options=options)
531
532
533def mount(device, mountpoint, options=None, persist=False, filesystem="ext3"):
147 """Mount a filesystem at a particular mountpoint"""534 """Mount a filesystem at a particular mountpoint"""
148 cmd_args = ['mount']535 cmd_args = ['mount']
149 if options is not None:536 if options is not None:
@@ -151,12 +538,12 @@ def mount(device, mountpoint, options=None, persist=False):
151 cmd_args.extend([device, mountpoint])538 cmd_args.extend([device, mountpoint])
152 try:539 try:
153 subprocess.check_output(cmd_args)540 subprocess.check_output(cmd_args)
154 except subprocess.CalledProcessError, e:541 except subprocess.CalledProcessError as e:
155 log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))542 log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
156 return False543 return False
544
157 if persist:545 if persist:
158 # TODO: update fstab546 return fstab_add(device, mountpoint, filesystem, options=options)
159 pass
160 return True547 return True
161548
162549
@@ -165,12 +552,12 @@ def umount(mountpoint, persist=False):
165 cmd_args = ['umount', mountpoint]552 cmd_args = ['umount', mountpoint]
166 try:553 try:
167 subprocess.check_output(cmd_args)554 subprocess.check_output(cmd_args)
168 except subprocess.CalledProcessError, e:555 except subprocess.CalledProcessError as e:
169 log('Error unmounting {}\n{}'.format(mountpoint, e.output))556 log('Error unmounting {}\n{}'.format(mountpoint, e.output))
170 return False557 return False
558
171 if persist:559 if persist:
172 # TODO: update fstab560 return fstab_remove(mountpoint)
173 pass
174 return True561 return True
175562
176563
@@ -183,102 +570,240 @@ def mounts():
183 return system_mounts570 return system_mounts
184571
185572
186def file_hash(path):573def fstab_mount(mountpoint):
187 """Generate a md5 hash of the contents of 'path' or None if not found """574 """Mount filesystem using fstab"""
575 cmd_args = ['mount', mountpoint]
576 try:
577 subprocess.check_output(cmd_args)
578 except subprocess.CalledProcessError as e:
579 log('Error unmounting {}\n{}'.format(mountpoint, e.output))
580 return False
581 return True
582
583
584def file_hash(path, hash_type='md5'):
585 """Generate a hash checksum of the contents of 'path' or None if not found.
586
587 :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
588 such as md5, sha1, sha256, sha512, etc.
589 """
188 if os.path.exists(path):590 if os.path.exists(path):
189 h = hashlib.md5()591 h = getattr(hashlib, hash_type)()
190 with open(path, 'r') as source:592 with open(path, 'rb') as source:
191 h.update(source.read()) # IGNORE:E1101 - it does have update593 h.update(source.read())
192 return h.hexdigest()594 return h.hexdigest()
193 else:595 else:
194 return None596 return None
195597
196598
197def restart_on_change(restart_map, stopstart=False):599def path_hash(path):
600 """Generate a hash checksum of all files matching 'path'. Standard
601 wildcards like '*' and '?' are supported, see documentation for the 'glob'
602 module for more information.
603
604 :return: dict: A { filename: hash } dictionary for all matched files.
605 Empty if none found.
606 """
607 return {
608 filename: file_hash(filename)
609 for filename in glob.iglob(path)
610 }
611
612
613def check_hash(path, checksum, hash_type='md5'):
614 """Validate a file using a cryptographic checksum.
615
616 :param str checksum: Value of the checksum used to validate the file.
617 :param str hash_type: Hash algorithm used to generate `checksum`.
618 Can be any hash alrgorithm supported by :mod:`hashlib`,
619 such as md5, sha1, sha256, sha512, etc.
620 :raises ChecksumError: If the file fails the checksum
621
622 """
623 actual_checksum = file_hash(path, hash_type)
624 if checksum != actual_checksum:
625 raise ChecksumError("'%s' != '%s'" % (checksum, actual_checksum))
626
627
628class ChecksumError(ValueError):
629 """A class derived from Value error to indicate the checksum failed."""
630 pass
631
632
633def restart_on_change(restart_map, stopstart=False, restart_functions=None):
198 """Restart services based on configuration files changing634 """Restart services based on configuration files changing
199635
200 This function is used a decorator, for example636 This function is used a decorator, for example::
201637
202 @restart_on_change({638 @restart_on_change({
203 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]639 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
640 '/etc/apache/sites-enabled/*': [ 'apache2' ]
204 })641 })
205 def ceph_client_changed():642 def config_changed():
206 ...643 pass # your code here
207644
208 In this example, the cinder-api and cinder-volume services645 In this example, the cinder-api and cinder-volume services
209 would be restarted if /etc/ceph/ceph.conf is changed by the646 would be restarted if /etc/ceph/ceph.conf is changed by the
210 ceph_client_changed function.647 ceph_client_changed function. The apache2 service would be
648 restarted if any file matching the pattern got changed, created
649 or removed. Standard wildcards are supported, see documentation
650 for the 'glob' module for more information.
651
652 @param restart_map: {path_file_name: [service_name, ...]
653 @param stopstart: DEFAULT false; whether to stop, start OR restart
654 @param restart_functions: nonstandard functions to use to restart services
655 {svc: func, ...}
656 @returns result from decorated function
211 """657 """
212 def wrap(f):658 def wrap(f):
213 def wrapped_f(*args):659 @functools.wraps(f)
214 checksums = {}660 def wrapped_f(*args, **kwargs):
215 for path in restart_map:661 return restart_on_change_helper(
216 checksums[path] = file_hash(path)662 (lambda: f(*args, **kwargs)), restart_map, stopstart,
217 f(*args)663 restart_functions)
218 restarts = []
219 for path in restart_map:
220 if checksums[path] != file_hash(path):
221 restarts += restart_map[path]
222 services_list = list(OrderedDict.fromkeys(restarts))
223 if not stopstart:
224 for service_name in services_list:
225 service('restart', service_name)
226 else:
227 for action in ['stop', 'start']:
228 for service_name in services_list:
229 service(action, service_name)
230 return wrapped_f664 return wrapped_f
231 return wrap665 return wrap
232666
233667
234def lsb_release():668def restart_on_change_helper(lambda_f, restart_map, stopstart=False,
235 """Return /etc/lsb-release in a dict"""669 restart_functions=None):
236 d = {}670 """Helper function to perform the restart_on_change function.
237 with open('/etc/lsb-release', 'r') as lsb:671
238 for l in lsb:672 This is provided for decorators to restart services if files described
239 k, v = l.split('=')673 in the restart_map have changed after an invocation of lambda_f().
240 d[k.strip()] = v.strip()674
241 return d675 @param lambda_f: function to call.
676 @param restart_map: {file: [service, ...]}
677 @param stopstart: whether to stop, start or restart a service
678 @param restart_functions: nonstandard functions to use to restart services
679 {svc: func, ...}
680 @returns result of lambda_f()
681 """
682 if restart_functions is None:
683 restart_functions = {}
684 checksums = {path: path_hash(path) for path in restart_map}
685 r = lambda_f()
686 # create a list of lists of the services to restart
687 restarts = [restart_map[path]
688 for path in restart_map
689 if path_hash(path) != checksums[path]]
690 # create a flat list of ordered services without duplicates from lists
691 services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts)))
692 if services_list:
693 actions = ('stop', 'start') if stopstart else ('restart',)
694 for service_name in services_list:
695 if service_name in restart_functions:
696 restart_functions[service_name](service_name)
697 else:
698 for action in actions:
699 service(action, service_name)
700 return r
242701
243702
244def pwgen(length=None):703def pwgen(length=None):
245 """Generate a random pasword."""704 """Generate a random pasword."""
246 if length is None:705 if length is None:
706 # A random length is ok to use a weak PRNG
247 length = random.choice(range(35, 45))707 length = random.choice(range(35, 45))
248 alphanumeric_chars = [708 alphanumeric_chars = [
249 l for l in (string.letters + string.digits)709 l for l in (string.ascii_letters + string.digits)
250 if l not in 'l0QD1vAEIOUaeiou']710 if l not in 'l0QD1vAEIOUaeiou']
711 # Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
712 # actual password
713 random_generator = random.SystemRandom()
251 random_chars = [714 random_chars = [
252 random.choice(alphanumeric_chars) for _ in range(length)]715 random_generator.choice(alphanumeric_chars) for _ in range(length)]
253 return(''.join(random_chars))716 return(''.join(random_chars))
254717
255718
256def list_nics(nic_type):719def is_phy_iface(interface):
257 '''Return a list of nics of given type(s)'''720 """Returns True if interface is not virtual, otherwise False."""
258 if isinstance(nic_type, basestring):721 if interface:
722 sys_net = '/sys/class/net'
723 if os.path.isdir(sys_net):
724 for iface in glob.glob(os.path.join(sys_net, '*')):
725 if '/virtual/' in os.path.realpath(iface):
726 continue
727
728 if interface == os.path.basename(iface):
729 return True
730
731 return False
732
733
734def get_bond_master(interface):
735 """Returns bond master if interface is bond slave otherwise None.
736
737 NOTE: the provided interface is expected to be physical
738 """
739 if interface:
740 iface_path = '/sys/class/net/%s' % (interface)
741 if os.path.exists(iface_path):
742 if '/virtual/' in os.path.realpath(iface_path):
743 return None
744
745 master = os.path.join(iface_path, 'master')
746 if os.path.exists(master):
747 master = os.path.realpath(master)
748 # make sure it is a bond master
749 if os.path.exists(os.path.join(master, 'bonding')):
750 return os.path.basename(master)
751
752 return None
753
754
755def list_nics(nic_type=None):
756 """Return a list of nics of given type(s)"""
757 if isinstance(nic_type, six.string_types):
259 int_types = [nic_type]758 int_types = [nic_type]
260 else:759 else:
261 int_types = nic_type760 int_types = nic_type
761
262 interfaces = []762 interfaces = []
263 for int_type in int_types:763 if nic_type:
264 cmd = ['ip', 'addr', 'show', 'label', int_type + '*']764 for int_type in int_types:
265 ip_output = subprocess.check_output(cmd).split('\n')765 cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
266 ip_output = (line for line in ip_output if line)766 ip_output = subprocess.check_output(cmd).decode('UTF-8')
767 ip_output = ip_output.split('\n')
768 ip_output = (line for line in ip_output if line)
769 for line in ip_output:
770 if line.split()[1].startswith(int_type):
771 matched = re.search('.*: (' + int_type +
772 r'[0-9]+\.[0-9]+)@.*', line)
773 if matched:
774 iface = matched.groups()[0]
775 else:
776 iface = line.split()[1].replace(":", "")
777
778 if iface not in interfaces:
779 interfaces.append(iface)
780 else:
781 cmd = ['ip', 'a']
782 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
783 ip_output = (line.strip() for line in ip_output if line)
784
785 key = re.compile('^[0-9]+:\s+(.+):')
267 for line in ip_output:786 for line in ip_output:
268 if line.split()[1].startswith(int_type):787 matched = re.search(key, line)
269 interfaces.append(line.split()[1].replace(":", ""))788 if matched:
789 iface = matched.group(1)
790 iface = iface.partition("@")[0]
791 if iface not in interfaces:
792 interfaces.append(iface)
793
270 return interfaces794 return interfaces
271795
272796
273def set_nic_mtu(nic, mtu):797def set_nic_mtu(nic, mtu):
274 '''Set MTU on a network interface'''798 """Set the Maximum Transmission Unit (MTU) on a network interface."""
275 cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]799 cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
276 subprocess.check_call(cmd)800 subprocess.check_call(cmd)
277801
278802
279def get_nic_mtu(nic):803def get_nic_mtu(nic):
804 """Return the Maximum Transmission Unit (MTU) for a network interface."""
280 cmd = ['ip', 'addr', 'show', nic]805 cmd = ['ip', 'addr', 'show', nic]
281 ip_output = subprocess.check_output(cmd).split('\n')806 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
282 mtu = ""807 mtu = ""
283 for line in ip_output:808 for line in ip_output:
284 words = line.split()809 words = line.split()
@@ -288,10 +813,136 @@ def get_nic_mtu(nic):
288813
289814
290def get_nic_hwaddr(nic):815def get_nic_hwaddr(nic):
816 """Return the Media Access Control (MAC) for a network interface."""
291 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]817 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
292 ip_output = subprocess.check_output(cmd)818 ip_output = subprocess.check_output(cmd).decode('UTF-8')
293 hwaddr = ""819 hwaddr = ""
294 words = ip_output.split()820 words = ip_output.split()
295 if 'link/ether' in words:821 if 'link/ether' in words:
296 hwaddr = words[words.index('link/ether') + 1]822 hwaddr = words[words.index('link/ether') + 1]
297 return hwaddr823 return hwaddr
824
825
826@contextmanager
827def chdir(directory):
828 """Change the current working directory to a different directory for a code
829 block and return the previous directory after the block exits. Useful to
830 run commands from a specificed directory.
831
832 :param str directory: The directory path to change to for this context.
833 """
834 cur = os.getcwd()
835 try:
836 yield os.chdir(directory)
837 finally:
838 os.chdir(cur)
839
840
841def chownr(path, owner, group, follow_links=True, chowntopdir=False):
842 """Recursively change user and group ownership of files and directories
843 in given path. Doesn't chown path itself by default, only its children.
844
845 :param str path: The string path to start changing ownership.
846 :param str owner: The owner string to use when looking up the uid.
847 :param str group: The group string to use when looking up the gid.
848 :param bool follow_links: Also follow and chown links if True
849 :param bool chowntopdir: Also chown path itself if True
850 """
851 uid = pwd.getpwnam(owner).pw_uid
852 gid = grp.getgrnam(group).gr_gid
853 if follow_links:
854 chown = os.chown
855 else:
856 chown = os.lchown
857
858 if chowntopdir:
859 broken_symlink = os.path.lexists(path) and not os.path.exists(path)
860 if not broken_symlink:
861 chown(path, uid, gid)
862 for root, dirs, files in os.walk(path, followlinks=follow_links):
863 for name in dirs + files:
864 full = os.path.join(root, name)
865 broken_symlink = os.path.lexists(full) and not os.path.exists(full)
866 if not broken_symlink:
867 chown(full, uid, gid)
868
869
870def lchownr(path, owner, group):
871 """Recursively change user and group ownership of files and directories
872 in a given path, not following symbolic links. See the documentation for
873 'os.lchown' for more information.
874
875 :param str path: The string path to start changing ownership.
876 :param str owner: The owner string to use when looking up the uid.
877 :param str group: The group string to use when looking up the gid.
878 """
879 chownr(path, owner, group, follow_links=False)
880
881
882def owner(path):
883 """Returns a tuple containing the username & groupname owning the path.
884
885 :param str path: the string path to retrieve the ownership
886 :return tuple(str, str): A (username, groupname) tuple containing the
887 name of the user and group owning the path.
888 :raises OSError: if the specified path does not exist
889 """
890 stat = os.stat(path)
891 username = pwd.getpwuid(stat.st_uid)[0]
892 groupname = grp.getgrgid(stat.st_gid)[0]
893 return username, groupname
894
895
896def get_total_ram():
897 """The total amount of system RAM in bytes.
898
899 This is what is reported by the OS, and may be overcommitted when
900 there are multiple containers hosted on the same machine.
901 """
902 with open('/proc/meminfo', 'r') as f:
903 for line in f.readlines():
904 if line:
905 key, value, unit = line.split()
906 if key == 'MemTotal:':
907 assert unit == 'kB', 'Unknown unit'
908 return int(value) * 1024 # Classic, not KiB.
909 raise NotImplementedError()
910
911
912UPSTART_CONTAINER_TYPE = '/run/container_type'
913
914
915def is_container():
916 """Determine whether unit is running in a container
917
918 @return: boolean indicating if unit is in a container
919 """
920 if init_is_systemd():
921 # Detect using systemd-detect-virt
922 return subprocess.call(['systemd-detect-virt',
923 '--container']) == 0
924 else:
925 # Detect using upstart container file marker
926 return os.path.exists(UPSTART_CONTAINER_TYPE)
927
928
929def add_to_updatedb_prunepath(path, updatedb_path=UPDATEDB_PATH):
930 with open(updatedb_path, 'r+') as f_id:
931 updatedb_text = f_id.read()
932 output = updatedb(updatedb_text, path)
933 f_id.seek(0)
934 f_id.write(output)
935 f_id.truncate()
936
937
938def updatedb(updatedb_text, new_path):
939 lines = [line for line in updatedb_text.split("\n")]
940 for i, line in enumerate(lines):
941 if line.startswith("PRUNEPATHS="):
942 paths_line = line.split("=")[1].replace('"', '')
943 paths = paths_line.split(" ")
944 if new_path not in paths:
945 paths.append(new_path)
946 lines[i] = 'PRUNEPATHS="{}"'.format(' '.join(paths))
947 output = "\n".join(lines)
948 return output
diff --git a/hooks/charmhelpers/core/host_factory/__init__.py b/hooks/charmhelpers/core/host_factory/__init__.py
298new file mode 100644949new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/hooks/charmhelpers/core/host_factory/__init__.py
diff --git a/hooks/charmhelpers/core/host_factory/centos.py b/hooks/charmhelpers/core/host_factory/centos.py
299new file mode 100644950new file mode 100644
index 0000000..7781a39
--- /dev/null
+++ b/hooks/charmhelpers/core/host_factory/centos.py
@@ -0,0 +1,72 @@
1import subprocess
2import yum
3import os
4
5from charmhelpers.core.strutils import BasicStringComparator
6
7
8class CompareHostReleases(BasicStringComparator):
9 """Provide comparisons of Host releases.
10
11 Use in the form of
12
13 if CompareHostReleases(release) > 'trusty':
14 # do something with mitaka
15 """
16
17 def __init__(self, item):
18 raise NotImplementedError(
19 "CompareHostReleases() is not implemented for CentOS")
20
21
22def service_available(service_name):
23 # """Determine whether a system service is available."""
24 if os.path.isdir('/run/systemd/system'):
25 cmd = ['systemctl', 'is-enabled', service_name]
26 else:
27 cmd = ['service', service_name, 'is-enabled']
28 return subprocess.call(cmd) == 0
29
30
31def add_new_group(group_name, system_group=False, gid=None):
32 cmd = ['groupadd']
33 if gid:
34 cmd.extend(['--gid', str(gid)])
35 if system_group:
36 cmd.append('-r')
37 cmd.append(group_name)
38 subprocess.check_call(cmd)
39
40
41def lsb_release():
42 """Return /etc/os-release in a dict."""
43 d = {}
44 with open('/etc/os-release', 'r') as lsb:
45 for l in lsb:
46 s = l.split('=')
47 if len(s) != 2:
48 continue
49 d[s[0].strip()] = s[1].strip()
50 return d
51
52
53def cmp_pkgrevno(package, revno, pkgcache=None):
54 """Compare supplied revno with the revno of the installed package.
55
56 * 1 => Installed revno is greater than supplied arg
57 * 0 => Installed revno is the same as supplied arg
58 * -1 => Installed revno is less than supplied arg
59
60 This function imports YumBase function if the pkgcache argument
61 is None.
62 """
63 if not pkgcache:
64 y = yum.YumBase()
65 packages = y.doPackageLists()
66 pkgcache = {i.Name: i.version for i in packages['installed']}
67 pkg = pkgcache[package]
68 if pkg > revno:
69 return 1
70 if pkg < revno:
71 return -1
72 return 0
diff --git a/hooks/charmhelpers/core/host_factory/ubuntu.py b/hooks/charmhelpers/core/host_factory/ubuntu.py
0new file mode 10064473new file mode 100644
index 0000000..d8dc378
--- /dev/null
+++ b/hooks/charmhelpers/core/host_factory/ubuntu.py
@@ -0,0 +1,89 @@
1import subprocess
2
3from charmhelpers.core.strutils import BasicStringComparator
4
5
6UBUNTU_RELEASES = (
7 'lucid',
8 'maverick',
9 'natty',
10 'oneiric',
11 'precise',
12 'quantal',
13 'raring',
14 'saucy',
15 'trusty',
16 'utopic',
17 'vivid',
18 'wily',
19 'xenial',
20 'yakkety',
21 'zesty',
22 'artful',
23)
24
25
26class CompareHostReleases(BasicStringComparator):
27 """Provide comparisons of Ubuntu releases.
28
29 Use in the form of
30
31 if CompareHostReleases(release) > 'trusty':
32 # do something with mitaka
33 """
34 _list = UBUNTU_RELEASES
35
36
37def service_available(service_name):
38 """Determine whether a system service is available"""
39 try:
40 subprocess.check_output(
41 ['service', service_name, 'status'],
42 stderr=subprocess.STDOUT).decode('UTF-8')
43 except subprocess.CalledProcessError as e:
44 return b'unrecognized service' not in e.output
45 else:
46 return True
47
48
49def add_new_group(group_name, system_group=False, gid=None):
50 cmd = ['addgroup']
51 if gid:
52 cmd.extend(['--gid', str(gid)])
53 if system_group:
54 cmd.append('--system')
55 else:
56 cmd.extend([
57 '--group',
58 ])
59 cmd.append(group_name)
60 subprocess.check_call(cmd)
61
62
63def lsb_release():
64 """Return /etc/lsb-release in a dict"""
65 d = {}
66 with open('/etc/lsb-release', 'r') as lsb:
67 for l in lsb:
68 k, v = l.split('=')
69 d[k.strip()] = v.strip()
70 return d
71
72
73def cmp_pkgrevno(package, revno, pkgcache=None):
74 """Compare supplied revno with the revno of the installed package.
75
76 * 1 => Installed revno is greater than supplied arg
77 * 0 => Installed revno is the same as supplied arg
78 * -1 => Installed revno is less than supplied arg
79
80 This function imports apt_cache function from charmhelpers.fetch if
81 the pkgcache argument is None. Be sure to add charmhelpers.fetch if
82 you call this function, or pass an apt_pkg.Cache() instance.
83 """
84 import apt_pkg
85 if not pkgcache:
86 from charmhelpers.fetch import apt_cache
87 pkgcache = apt_cache()
88 pkg = pkgcache[package]
89 return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
diff --git a/hooks/charmhelpers/core/hugepage.py b/hooks/charmhelpers/core/hugepage.py
0new file mode 10064490new file mode 100644
index 0000000..54b5b5e
--- /dev/null
+++ b/hooks/charmhelpers/core/hugepage.py
@@ -0,0 +1,69 @@
1# -*- coding: utf-8 -*-
2
3# Copyright 2014-2015 Canonical Limited.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17import yaml
18from charmhelpers.core import fstab
19from charmhelpers.core import sysctl
20from charmhelpers.core.host import (
21 add_group,
22 add_user_to_group,
23 fstab_mount,
24 mkdir,
25)
26from charmhelpers.core.strutils import bytes_from_string
27from subprocess import check_output
28
29
30def hugepage_support(user, group='hugetlb', nr_hugepages=256,
31 max_map_count=65536, mnt_point='/run/hugepages/kvm',
32 pagesize='2MB', mount=True, set_shmmax=False):
33 """Enable hugepages on system.
34
35 Args:
36 user (str) -- Username to allow access to hugepages to
37 group (str) -- Group name to own hugepages
38 nr_hugepages (int) -- Number of pages to reserve
39 max_map_count (int) -- Number of Virtual Memory Areas a process can own
40 mnt_point (str) -- Directory to mount hugepages on
41 pagesize (str) -- Size of hugepages
42 mount (bool) -- Whether to Mount hugepages
43 """
44 group_info = add_group(group)
45 gid = group_info.gr_gid
46 add_user_to_group(user, group)
47 if max_map_count < 2 * nr_hugepages:
48 max_map_count = 2 * nr_hugepages
49 sysctl_settings = {
50 'vm.nr_hugepages': nr_hugepages,
51 'vm.max_map_count': max_map_count,
52 'vm.hugetlb_shm_group': gid,
53 }
54 if set_shmmax:
55 shmmax_current = int(check_output(['sysctl', '-n', 'kernel.shmmax']))
56 shmmax_minsize = bytes_from_string(pagesize) * nr_hugepages
57 if shmmax_minsize > shmmax_current:
58 sysctl_settings['kernel.shmmax'] = shmmax_minsize
59 sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf')
60 mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False)
61 lfstab = fstab.Fstab()
62 fstab_entry = lfstab.get_entry_by_attr('mountpoint', mnt_point)
63 if fstab_entry:
64 lfstab.remove_entry(fstab_entry)
65 entry = lfstab.Entry('nodev', mnt_point, 'hugetlbfs',
66 'mode=1770,gid={},pagesize={}'.format(gid, pagesize), 0, 0)
67 lfstab.add_entry(entry)
68 if mount:
69 fstab_mount(mnt_point)
diff --git a/hooks/charmhelpers/core/kernel.py b/hooks/charmhelpers/core/kernel.py
0new file mode 10064470new file mode 100644
index 0000000..2d40452
--- /dev/null
+++ b/hooks/charmhelpers/core/kernel.py
@@ -0,0 +1,72 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# Copyright 2014-2015 Canonical Limited.
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17
18import re
19import subprocess
20
21from charmhelpers.osplatform import get_platform
22from charmhelpers.core.hookenv import (
23 log,
24 INFO
25)
26
27__platform__ = get_platform()
28if __platform__ == "ubuntu":
29 from charmhelpers.core.kernel_factory.ubuntu import (
30 persistent_modprobe,
31 update_initramfs,
32 ) # flake8: noqa -- ignore F401 for this import
33elif __platform__ == "centos":
34 from charmhelpers.core.kernel_factory.centos import (
35 persistent_modprobe,
36 update_initramfs,
37 ) # flake8: noqa -- ignore F401 for this import
38
39__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
40
41
42def modprobe(module, persist=True):
43 """Load a kernel module and configure for auto-load on reboot."""
44 cmd = ['modprobe', module]
45
46 log('Loading kernel module %s' % module, level=INFO)
47
48 subprocess.check_call(cmd)
49 if persist:
50 persistent_modprobe(module)
51
52
53def rmmod(module, force=False):
54 """Remove a module from the linux kernel"""
55 cmd = ['rmmod']
56 if force:
57 cmd.append('-f')
58 cmd.append(module)
59 log('Removing kernel module %s' % module, level=INFO)
60 return subprocess.check_call(cmd)
61
62
63def lsmod():
64 """Shows what kernel modules are currently loaded"""
65 return subprocess.check_output(['lsmod'],
66 universal_newlines=True)
67
68
69def is_module_loaded(module):
70 """Checks if a kernel module is already loaded"""
71 matches = re.findall('^%s[ ]+' % module, lsmod(), re.M)
72 return len(matches) > 0
diff --git a/hooks/charmhelpers/core/kernel_factory/__init__.py b/hooks/charmhelpers/core/kernel_factory/__init__.py
0new file mode 10064473new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/hooks/charmhelpers/core/kernel_factory/__init__.py
diff --git a/hooks/charmhelpers/core/kernel_factory/centos.py b/hooks/charmhelpers/core/kernel_factory/centos.py
1new file mode 10064474new file mode 100644
index 0000000..1c402c1
--- /dev/null
+++ b/hooks/charmhelpers/core/kernel_factory/centos.py
@@ -0,0 +1,17 @@
1import subprocess
2import os
3
4
5def persistent_modprobe(module):
6 """Load a kernel module and configure for auto-load on reboot."""
7 if not os.path.exists('/etc/rc.modules'):
8 open('/etc/rc.modules', 'a')
9 os.chmod('/etc/rc.modules', 111)
10 with open('/etc/rc.modules', 'r+') as modules:
11 if module not in modules.read():
12 modules.write('modprobe %s\n' % module)
13
14
15def update_initramfs(version='all'):
16 """Updates an initramfs image."""
17 return subprocess.check_call(["dracut", "-f", version])
diff --git a/hooks/charmhelpers/core/kernel_factory/ubuntu.py b/hooks/charmhelpers/core/kernel_factory/ubuntu.py
0new file mode 10064418new file mode 100644
index 0000000..3de372f
--- /dev/null
+++ b/hooks/charmhelpers/core/kernel_factory/ubuntu.py
@@ -0,0 +1,13 @@
1import subprocess
2
3
4def persistent_modprobe(module):
5 """Load a kernel module and configure for auto-load on reboot."""
6 with open('/etc/modules', 'r+') as modules:
7 if module not in modules.read():
8 modules.write(module + "\n")
9
10
11def update_initramfs(version='all'):
12 """Updates an initramfs image."""
13 return subprocess.check_call(["update-initramfs", "-k", version, "-u"])
diff --git a/hooks/charmhelpers/core/services/__init__.py b/hooks/charmhelpers/core/services/__init__.py
0new file mode 10064414new file mode 100644
index 0000000..61fd074
--- /dev/null
+++ b/hooks/charmhelpers/core/services/__init__.py
@@ -0,0 +1,16 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15from .base import * # NOQA
16from .helpers import * # NOQA
diff --git a/hooks/charmhelpers/core/services/base.py b/hooks/charmhelpers/core/services/base.py
0new file mode 10064417new file mode 100644
index 0000000..ca9dc99
--- /dev/null
+++ b/hooks/charmhelpers/core/services/base.py
@@ -0,0 +1,351 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import os
16import json
17from inspect import getargspec
18from collections import Iterable, OrderedDict
19
20from charmhelpers.core import host
21from charmhelpers.core import hookenv
22
23
24__all__ = ['ServiceManager', 'ManagerCallback',
25 'PortManagerCallback', 'open_ports', 'close_ports', 'manage_ports',
26 'service_restart', 'service_stop']
27
28
29class ServiceManager(object):
30 def __init__(self, services=None):
31 """
32 Register a list of services, given their definitions.
33
34 Service definitions are dicts in the following formats (all keys except
35 'service' are optional)::
36
37 {
38 "service": <service name>,
39 "required_data": <list of required data contexts>,
40 "provided_data": <list of provided data contexts>,
41 "data_ready": <one or more callbacks>,
42 "data_lost": <one or more callbacks>,
43 "start": <one or more callbacks>,
44 "stop": <one or more callbacks>,
45 "ports": <list of ports to manage>,
46 }
47
48 The 'required_data' list should contain dicts of required data (or
49 dependency managers that act like dicts and know how to collect the data).
50 Only when all items in the 'required_data' list are populated are the list
51 of 'data_ready' and 'start' callbacks executed. See `is_ready()` for more
52 information.
53
54 The 'provided_data' list should contain relation data providers, most likely
55 a subclass of :class:`charmhelpers.core.services.helpers.RelationContext`,
56 that will indicate a set of data to set on a given relation.
57
58 The 'data_ready' value should be either a single callback, or a list of
59 callbacks, to be called when all items in 'required_data' pass `is_ready()`.
60 Each callback will be called with the service name as the only parameter.
61 After all of the 'data_ready' callbacks are called, the 'start' callbacks
62 are fired.
63
64 The 'data_lost' value should be either a single callback, or a list of
65 callbacks, to be called when a 'required_data' item no longer passes
66 `is_ready()`. Each callback will be called with the service name as the
67 only parameter. After all of the 'data_lost' callbacks are called,
68 the 'stop' callbacks are fired.
69
70 The 'start' value should be either a single callback, or a list of
71 callbacks, to be called when starting the service, after the 'data_ready'
72 callbacks are complete. Each callback will be called with the service
73 name as the only parameter. This defaults to
74 `[host.service_start, services.open_ports]`.
75
76 The 'stop' value should be either a single callback, or a list of
77 callbacks, to be called when stopping the service. If the service is
78 being stopped because it no longer has all of its 'required_data', this
79 will be called after all of the 'data_lost' callbacks are complete.
80 Each callback will be called with the service name as the only parameter.
81 This defaults to `[services.close_ports, host.service_stop]`.
82
83 The 'ports' value should be a list of ports to manage. The default
84 'start' handler will open the ports after the service is started,
85 and the default 'stop' handler will close the ports prior to stopping
86 the service.
87
88
89 Examples:
90
91 The following registers an Upstart service called bingod that depends on
92 a mongodb relation and which runs a custom `db_migrate` function prior to
93 restarting the service, and a Runit service called spadesd::
94
95 manager = services.ServiceManager([
96 {
97 'service': 'bingod',
98 'ports': [80, 443],
99 'required_data': [MongoRelation(), config(), {'my': 'data'}],
100 'data_ready': [
101 services.template(source='bingod.conf'),
102 services.template(source='bingod.ini',
103 target='/etc/bingod.ini',
104 owner='bingo', perms=0400),
105 ],
106 },
107 {
108 'service': 'spadesd',
109 'data_ready': services.template(source='spadesd_run.j2',
110 target='/etc/sv/spadesd/run',
111 perms=0555),
112 'start': runit_start,
113 'stop': runit_stop,
114 },
115 ])
116 manager.manage()
117 """
118 self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
119 self._ready = None
120 self.services = OrderedDict()
121 for service in services or []:
122 service_name = service['service']
123 self.services[service_name] = service
124
125 def manage(self):
126 """
127 Handle the current hook by doing The Right Thing with the registered services.
128 """
129 hookenv._run_atstart()
130 try:
131 hook_name = hookenv.hook_name()
132 if hook_name == 'stop':
133 self.stop_services()
134 else:
135 self.reconfigure_services()
136 self.provide_data()
137 except SystemExit as x:
138 if x.code is None or x.code == 0:
139 hookenv._run_atexit()
140 hookenv._run_atexit()
141
142 def provide_data(self):
143 """
144 Set the relation data for each provider in the ``provided_data`` list.
145
146 A provider must have a `name` attribute, which indicates which relation
147 to set data on, and a `provide_data()` method, which returns a dict of
148 data to set.
149
150 The `provide_data()` method can optionally accept two parameters:
151
152 * ``remote_service`` The name of the remote service that the data will
153 be provided to. The `provide_data()` method will be called once
154 for each connected service (not unit). This allows the method to
155 tailor its data to the given service.
156 * ``service_ready`` Whether or not the service definition had all of
157 its requirements met, and thus the ``data_ready`` callbacks run.
158
159 Note that the ``provided_data`` methods are now called **after** the
160 ``data_ready`` callbacks are run. This gives the ``data_ready`` callbacks
161 a chance to generate any data necessary for the providing to the remote
162 services.
163 """
164 for service_name, service in self.services.items():
165 service_ready = self.is_ready(service_name)
166 for provider in service.get('provided_data', []):
167 for relid in hookenv.relation_ids(provider.name):
168 units = hookenv.related_units(relid)
169 if not units:
170 continue
171 remote_service = units[0].split('/')[0]
172 argspec = getargspec(provider.provide_data)
173 if len(argspec.args) > 1:
174 data = provider.provide_data(remote_service, service_ready)
175 else:
176 data = provider.provide_data()
177 if data:
178 hookenv.relation_set(relid, data)
179
180 def reconfigure_services(self, *service_names):
181 """
182 Update all files for one or more registered services, and,
183 if ready, optionally restart them.
184
185 If no service names are given, reconfigures all registered services.
186 """
187 for service_name in service_names or self.services.keys():
188 if self.is_ready(service_name):
189 self.fire_event('data_ready', service_name)
190 self.fire_event('start', service_name, default=[
191 service_restart,
192 manage_ports])
193 self.save_ready(service_name)
194 else:
195 if self.was_ready(service_name):
196 self.fire_event('data_lost', service_name)
197 self.fire_event('stop', service_name, default=[
198 manage_ports,
199 service_stop])
200 self.save_lost(service_name)
201
202 def stop_services(self, *service_names):
203 """
204 Stop one or more registered services, by name.
205
206 If no service names are given, stops all registered services.
207 """
208 for service_name in service_names or self.services.keys():
209 self.fire_event('stop', service_name, default=[
210 manage_ports,
211 service_stop])
212
213 def get_service(self, service_name):
214 """
215 Given the name of a registered service, return its service definition.
216 """
217 service = self.services.get(service_name)
218 if not service:
219 raise KeyError('Service not registered: %s' % service_name)
220 return service
221
222 def fire_event(self, event_name, service_name, default=None):
223 """
224 Fire a data_ready, data_lost, start, or stop event on a given service.
225 """
226 service = self.get_service(service_name)
227 callbacks = service.get(event_name, default)
228 if not callbacks:
229 return
230 if not isinstance(callbacks, Iterable):
231 callbacks = [callbacks]
232 for callback in callbacks:
233 if isinstance(callback, ManagerCallback):
234 callback(self, service_name, event_name)
235 else:
236 callback(service_name)
237
238 def is_ready(self, service_name):
239 """
240 Determine if a registered service is ready, by checking its 'required_data'.
241
242 A 'required_data' item can be any mapping type, and is considered ready
243 if `bool(item)` evaluates as True.
244 """
245 service = self.get_service(service_name)
246 reqs = service.get('required_data', [])
247 return all(bool(req) for req in reqs)
248
249 def _load_ready_file(self):
250 if self._ready is not None:
251 return
252 if os.path.exists(self._ready_file):
253 with open(self._ready_file) as fp:
254 self._ready = set(json.load(fp))
255 else:
256 self._ready = set()
257
258 def _save_ready_file(self):
259 if self._ready is None:
260 return
261 with open(self._ready_file, 'w') as fp:
262 json.dump(list(self._ready), fp)
263
264 def save_ready(self, service_name):
265 """
266 Save an indicator that the given service is now data_ready.
267 """
268 self._load_ready_file()
269 self._ready.add(service_name)
270 self._save_ready_file()
271
272 def save_lost(self, service_name):
273 """
274 Save an indicator that the given service is no longer data_ready.
275 """
276 self._load_ready_file()
277 self._ready.discard(service_name)
278 self._save_ready_file()
279
280 def was_ready(self, service_name):
281 """
282 Determine if the given service was previously data_ready.
283 """
284 self._load_ready_file()
285 return service_name in self._ready
286
287
288class ManagerCallback(object):
289 """
290 Special case of a callback that takes the `ServiceManager` instance
291 in addition to the service name.
292
293 Subclasses should implement `__call__` which should accept three parameters:
294
295 * `manager` The `ServiceManager` instance
296 * `service_name` The name of the service it's being triggered for
297 * `event_name` The name of the event that this callback is handling
298 """
299 def __call__(self, manager, service_name, event_name):
300 raise NotImplementedError()
301
302
303class PortManagerCallback(ManagerCallback):
304 """
305 Callback class that will open or close ports, for use as either
306 a start or stop action.
307 """
308 def __call__(self, manager, service_name, event_name):
309 service = manager.get_service(service_name)
310 new_ports = service.get('ports', [])
311 port_file = os.path.join(hookenv.charm_dir(), '.{}.ports'.format(service_name))
312 if os.path.exists(port_file):
313 with open(port_file) as fp:
314 old_ports = fp.read().split(',')
315 for old_port in old_ports:
316 if bool(old_port):
317 old_port = int(old_port)
318 if old_port not in new_ports:
319 hookenv.close_port(old_port)
320 with open(port_file, 'w') as fp:
321 fp.write(','.join(str(port) for port in new_ports))
322 for port in new_ports:
323 if event_name == 'start':
324 hookenv.open_port(port)
325 elif event_name == 'stop':
326 hookenv.close_port(port)
327
328
329def service_stop(service_name):
330 """
331 Wrapper around host.service_stop to prevent spurious "unknown service"
332 messages in the logs.
333 """
334 if host.service_running(service_name):
335 host.service_stop(service_name)
336
337
338def service_restart(service_name):
339 """
340 Wrapper around host.service_restart to prevent spurious "unknown service"
341 messages in the logs.
342 """
343 if host.service_available(service_name):
344 if host.service_running(service_name):
345 host.service_restart(service_name)
346 else:
347 host.service_start(service_name)
348
349
350# Convenience aliases
351open_ports = close_ports = manage_ports = PortManagerCallback()
diff --git a/hooks/charmhelpers/core/services/helpers.py b/hooks/charmhelpers/core/services/helpers.py
0new file mode 100644352new file mode 100644
index 0000000..3e6e30d
--- /dev/null
+++ b/hooks/charmhelpers/core/services/helpers.py
@@ -0,0 +1,290 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import os
16import yaml
17
18from charmhelpers.core import hookenv
19from charmhelpers.core import host
20from charmhelpers.core import templating
21
22from charmhelpers.core.services.base import ManagerCallback
23
24
25__all__ = ['RelationContext', 'TemplateCallback',
26 'render_template', 'template']
27
28
29class RelationContext(dict):
30 """
31 Base class for a context generator that gets relation data from juju.
32
33 Subclasses must provide the attributes `name`, which is the name of the
34 interface of interest, `interface`, which is the type of the interface of
35 interest, and `required_keys`, which is the set of keys required for the
36 relation to be considered complete. The data for all interfaces matching
37 the `name` attribute that are complete will used to populate the dictionary
38 values (see `get_data`, below).
39
40 The generated context will be namespaced under the relation :attr:`name`,
41 to prevent potential naming conflicts.
42
43 :param str name: Override the relation :attr:`name`, since it can vary from charm to charm
44 :param list additional_required_keys: Extend the list of :attr:`required_keys`
45 """
46 name = None
47 interface = None
48
49 def __init__(self, name=None, additional_required_keys=None):
50 if not hasattr(self, 'required_keys'):
51 self.required_keys = []
52
53 if name is not None:
54 self.name = name
55 if additional_required_keys:
56 self.required_keys.extend(additional_required_keys)
57 self.get_data()
58
59 def __bool__(self):
60 """
61 Returns True if all of the required_keys are available.
62 """
63 return self.is_ready()
64
65 __nonzero__ = __bool__
66
67 def __repr__(self):
68 return super(RelationContext, self).__repr__()
69
70 def is_ready(self):
71 """
72 Returns True if all of the `required_keys` are available from any units.
73 """
74 ready = len(self.get(self.name, [])) > 0
75 if not ready:
76 hookenv.log('Incomplete relation: {}'.format(self.__class__.__name__), hookenv.DEBUG)
77 return ready
78
79 def _is_ready(self, unit_data):
80 """
81 Helper method that tests a set of relation data and returns True if
82 all of the `required_keys` are present.
83 """
84 return set(unit_data.keys()).issuperset(set(self.required_keys))
85
86 def get_data(self):
87 """
88 Retrieve the relation data for each unit involved in a relation and,
89 if complete, store it in a list under `self[self.name]`. This
90 is automatically called when the RelationContext is instantiated.
91
92 The units are sorted lexographically first by the service ID, then by
93 the unit ID. Thus, if an interface has two other services, 'db:1'
94 and 'db:2', with 'db:1' having two units, 'wordpress/0' and 'wordpress/1',
95 and 'db:2' having one unit, 'mediawiki/0', all of which have a complete
96 set of data, the relation data for the units will be stored in the
97 order: 'wordpress/0', 'wordpress/1', 'mediawiki/0'.
98
99 If you only care about a single unit on the relation, you can just
100 access it as `{{ interface[0]['key'] }}`. However, if you can at all
101 support multiple units on a relation, you should iterate over the list,
102 like::
103
104 {% for unit in interface -%}
105 {{ unit['key'] }}{% if not loop.last %},{% endif %}
106 {%- endfor %}
107
108 Note that since all sets of relation data from all related services and
109 units are in a single list, if you need to know which service or unit a
110 set of data came from, you'll need to extend this class to preserve
111 that information.
112 """
113 if not hookenv.relation_ids(self.name):
114 return
115
116 ns = self.setdefault(self.name, [])
117 for rid in sorted(hookenv.relation_ids(self.name)):
118 for unit in sorted(hookenv.related_units(rid)):
119 reldata = hookenv.relation_get(rid=rid, unit=unit)
120 if self._is_ready(reldata):
121 ns.append(reldata)
122
123 def provide_data(self):
124 """
125 Return data to be relation_set for this interface.
126 """
127 return {}
128
129
130class MysqlRelation(RelationContext):
131 """
132 Relation context for the `mysql` interface.
133
134 :param str name: Override the relation :attr:`name`, since it can vary from charm to charm
135 :param list additional_required_keys: Extend the list of :attr:`required_keys`
136 """
137 name = 'db'
138 interface = 'mysql'
139
140 def __init__(self, *args, **kwargs):
141 self.required_keys = ['host', 'user', 'password', 'database']
142 RelationContext.__init__(self, *args, **kwargs)
143
144
145class HttpRelation(RelationContext):
146 """
147 Relation context for the `http` interface.
148
149 :param str name: Override the relation :attr:`name`, since it can vary from charm to charm
150 :param list additional_required_keys: Extend the list of :attr:`required_keys`
151 """
152 name = 'website'
153 interface = 'http'
154
155 def __init__(self, *args, **kwargs):
156 self.required_keys = ['host', 'port']
157 RelationContext.__init__(self, *args, **kwargs)
158
159 def provide_data(self):
160 return {
161 'host': hookenv.unit_get('private-address'),
162 'port': 80,
163 }
164
165
166class RequiredConfig(dict):
167 """
168 Data context that loads config options with one or more mandatory options.
169
170 Once the required options have been changed from their default values, all
171 config options will be available, namespaced under `config` to prevent
172 potential naming conflicts (for example, between a config option and a
173 relation property).
174
175 :param list *args: List of options that must be changed from their default values.
176 """
177
178 def __init__(self, *args):
179 self.required_options = args
180 self['config'] = hookenv.config()
181 with open(os.path.join(hookenv.charm_dir(), 'config.yaml')) as fp:
182 self.config = yaml.load(fp).get('options', {})
183
184 def __bool__(self):
185 for option in self.required_options:
186 if option not in self['config']:
187 return False
188 current_value = self['config'][option]
189 default_value = self.config[option].get('default')
190 if current_value == default_value:
191 return False
192 if current_value in (None, '') and default_value in (None, ''):
193 return False
194 return True
195
196 def __nonzero__(self):
197 return self.__bool__()
198
199
200class StoredContext(dict):
201 """
202 A data context that always returns the data that it was first created with.
203
204 This is useful to do a one-time generation of things like passwords, that
205 will thereafter use the same value that was originally generated, instead
206 of generating a new value each time it is run.
207 """
208 def __init__(self, file_name, config_data):
209 """
210 If the file exists, populate `self` with the data from the file.
211 Otherwise, populate with the given data and persist it to the file.
212 """
213 if os.path.exists(file_name):
214 self.update(self.read_context(file_name))
215 else:
216 self.store_context(file_name, config_data)
217 self.update(config_data)
218
219 def store_context(self, file_name, config_data):
220 if not os.path.isabs(file_name):
221 file_name = os.path.join(hookenv.charm_dir(), file_name)
222 with open(file_name, 'w') as file_stream:
223 os.fchmod(file_stream.fileno(), 0o600)
224 yaml.dump(config_data, file_stream)
225
226 def read_context(self, file_name):
227 if not os.path.isabs(file_name):
228 file_name = os.path.join(hookenv.charm_dir(), file_name)
229 with open(file_name, 'r') as file_stream:
230 data = yaml.load(file_stream)
231 if not data:
232 raise OSError("%s is empty" % file_name)
233 return data
234
235
236class TemplateCallback(ManagerCallback):
237 """
238 Callback class that will render a Jinja2 template, for use as a ready
239 action.
240
241 :param str source: The template source file, relative to
242 `$CHARM_DIR/templates`
243
244 :param str target: The target to write the rendered template to (or None)
245 :param str owner: The owner of the rendered file
246 :param str group: The group of the rendered file
247 :param int perms: The permissions of the rendered file
248 :param partial on_change_action: functools partial to be executed when
249 rendered file changes
250 :param jinja2 loader template_loader: A jinja2 template loader
251
252 :return str: The rendered template
253 """
254 def __init__(self, source, target,
255 owner='root', group='root', perms=0o444,
256 on_change_action=None, template_loader=None):
257 self.source = source
258 self.target = target
259 self.owner = owner
260 self.group = group
261 self.perms = perms
262 self.on_change_action = on_change_action
263 self.template_loader = template_loader
264
265 def __call__(self, manager, service_name, event_name):
266 pre_checksum = ''
267 if self.on_change_action and os.path.isfile(self.target):
268 pre_checksum = host.file_hash(self.target)
269 service = manager.get_service(service_name)
270 context = {'ctx': {}}
271 for ctx in service.get('required_data', []):
272 context.update(ctx)
273 context['ctx'].update(ctx)
274
275 result = templating.render(self.source, self.target, context,
276 self.owner, self.group, self.perms,
277 template_loader=self.template_loader)
278 if self.on_change_action:
279 if pre_checksum == host.file_hash(self.target):
280 hookenv.log(
281 'No change detected: {}'.format(self.target),
282 hookenv.DEBUG)
283 else:
284 self.on_change_action()
285
286 return result
287
288
289# Convenience aliases for templates
290render_template = template = TemplateCallback
diff --git a/hooks/charmhelpers/core/strutils.py b/hooks/charmhelpers/core/strutils.py
0new file mode 100644291new file mode 100644
index 0000000..685dabd
--- /dev/null
+++ b/hooks/charmhelpers/core/strutils.py
@@ -0,0 +1,123 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# Copyright 2014-2015 Canonical Limited.
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17
18import six
19import re
20
21
22def bool_from_string(value):
23 """Interpret string value as boolean.
24
25 Returns True if value translates to True otherwise False.
26 """
27 if isinstance(value, six.string_types):
28 value = six.text_type(value)
29 else:
30 msg = "Unable to interpret non-string value '%s' as boolean" % (value)
31 raise ValueError(msg)
32
33 value = value.strip().lower()
34
35 if value in ['y', 'yes', 'true', 't', 'on']:
36 return True
37 elif value in ['n', 'no', 'false', 'f', 'off']:
38 return False
39
40 msg = "Unable to interpret string value '%s' as boolean" % (value)
41 raise ValueError(msg)
42
43
44def bytes_from_string(value):
45 """Interpret human readable string value as bytes.
46
47 Returns int
48 """
49 BYTE_POWER = {
50 'K': 1,
51 'KB': 1,
52 'M': 2,
53 'MB': 2,
54 'G': 3,
55 'GB': 3,
56 'T': 4,
57 'TB': 4,
58 'P': 5,
59 'PB': 5,
60 }
61 if isinstance(value, six.string_types):
62 value = six.text_type(value)
63 else:
64 msg = "Unable to interpret non-string value '%s' as boolean" % (value)
65 raise ValueError(msg)
66 matches = re.match("([0-9]+)([a-zA-Z]+)", value)
67 if not matches:
68 msg = "Unable to interpret string value '%s' as bytes" % (value)
69 raise ValueError(msg)
70 return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)])
71
72
73class BasicStringComparator(object):
74 """Provides a class that will compare strings from an iterator type object.
75 Used to provide > and < comparisons on strings that may not necessarily be
76 alphanumerically ordered. e.g. OpenStack or Ubuntu releases AFTER the
77 z-wrap.
78 """
79
80 _list = None
81
82 def __init__(self, item):
83 if self._list is None:
84 raise Exception("Must define the _list in the class definition!")
85 try:
86 self.index = self._list.index(item)
87 except Exception:
88 raise KeyError("Item '{}' is not in list '{}'"
89 .format(item, self._list))
90
91 def __eq__(self, other):
92 assert isinstance(other, str) or isinstance(other, self.__class__)
93 return self.index == self._list.index(other)
94
95 def __ne__(self, other):
96 return not self.__eq__(other)
97
98 def __lt__(self, other):
99 assert isinstance(other, str) or isinstance(other, self.__class__)
100 return self.index < self._list.index(other)
101
102 def __ge__(self, other):
103 return not self.__lt__(other)
104
105 def __gt__(self, other):
106 assert isinstance(other, str) or isinstance(other, self.__class__)
107 return self.index > self._list.index(other)
108
109 def __le__(self, other):
110 return not self.__gt__(other)
111
112 def __str__(self):
113 """Always give back the item at the index so it can be used in
114 comparisons like:
115
116 s_mitaka = CompareOpenStack('mitaka')
117 s_newton = CompareOpenstack('newton')
118
119 assert s_newton > s_mitaka
120
121 @returns: <string>
122 """
123 return self._list[self.index]
diff --git a/hooks/charmhelpers/core/sysctl.py b/hooks/charmhelpers/core/sysctl.py
0new file mode 100644124new file mode 100644
index 0000000..6e413e3
--- /dev/null
+++ b/hooks/charmhelpers/core/sysctl.py
@@ -0,0 +1,54 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# Copyright 2014-2015 Canonical Limited.
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17
18import yaml
19
20from subprocess import check_call
21
22from charmhelpers.core.hookenv import (
23 log,
24 DEBUG,
25 ERROR,
26)
27
28__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
29
30
31def create(sysctl_dict, sysctl_file):
32 """Creates a sysctl.conf file from a YAML associative array
33
34 :param sysctl_dict: a YAML-formatted string of sysctl options eg "{ 'kernel.max_pid': 1337 }"
35 :type sysctl_dict: str
36 :param sysctl_file: path to the sysctl file to be saved
37 :type sysctl_file: str or unicode
38 :returns: None
39 """
40 try:
41 sysctl_dict_parsed = yaml.safe_load(sysctl_dict)
42 except yaml.YAMLError:
43 log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict),
44 level=ERROR)
45 return
46
47 with open(sysctl_file, "w") as fd:
48 for key, value in sysctl_dict_parsed.items():
49 fd.write("{}={}\n".format(key, value))
50
51 log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict_parsed),
52 level=DEBUG)
53
54 check_call(["sysctl", "-p", sysctl_file])
diff --git a/hooks/charmhelpers/core/templating.py b/hooks/charmhelpers/core/templating.py
0new file mode 10064455new file mode 100644
index 0000000..7b801a3
--- /dev/null
+++ b/hooks/charmhelpers/core/templating.py
@@ -0,0 +1,84 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import os
16import sys
17
18from charmhelpers.core import host
19from charmhelpers.core import hookenv
20
21
22def render(source, target, context, owner='root', group='root',
23 perms=0o444, templates_dir=None, encoding='UTF-8', template_loader=None):
24 """
25 Render a template.
26
27 The `source` path, if not absolute, is relative to the `templates_dir`.
28
29 The `target` path should be absolute. It can also be `None`, in which
30 case no file will be written.
31
32 The context should be a dict containing the values to be replaced in the
33 template.
34
35 The `owner`, `group`, and `perms` options will be passed to `write_file`.
36
37 If omitted, `templates_dir` defaults to the `templates` folder in the charm.
38
39 The rendered template will be written to the file as well as being returned
40 as a string.
41
42 Note: Using this requires python-jinja2 or python3-jinja2; if it is not
43 installed, calling this will attempt to use charmhelpers.fetch.apt_install
44 to install it.
45 """
46 try:
47 from jinja2 import FileSystemLoader, Environment, exceptions
48 except ImportError:
49 try:
50 from charmhelpers.fetch import apt_install
51 except ImportError:
52 hookenv.log('Could not import jinja2, and could not import '
53 'charmhelpers.fetch to install it',
54 level=hookenv.ERROR)
55 raise
56 if sys.version_info.major == 2:
57 apt_install('python-jinja2', fatal=True)
58 else:
59 apt_install('python3-jinja2', fatal=True)
60 from jinja2 import FileSystemLoader, Environment, exceptions
61
62 if template_loader:
63 template_env = Environment(loader=template_loader)
64 else:
65 if templates_dir is None:
66 templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
67 template_env = Environment(loader=FileSystemLoader(templates_dir))
68 try:
69 source = source
70 template = template_env.get_template(source)
71 except exceptions.TemplateNotFound as e:
72 hookenv.log('Could not load template %s from %s.' %
73 (source, templates_dir),
74 level=hookenv.ERROR)
75 raise e
76 content = template.render(context)
77 if target is not None:
78 target_dir = os.path.dirname(target)
79 if not os.path.exists(target_dir):
80 # This is a terrible default directory permission, as the file
81 # or its siblings will often contain secrets.
82 host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
83 host.write_file(target, content.encode(encoding), owner, group, perms)
84 return content
diff --git a/hooks/charmhelpers/core/unitdata.py b/hooks/charmhelpers/core/unitdata.py
0new file mode 10064485new file mode 100644
index 0000000..54ec969
--- /dev/null
+++ b/hooks/charmhelpers/core/unitdata.py
@@ -0,0 +1,518 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4# Copyright 2014-2015 Canonical Limited.
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17#
18# Authors:
19# Kapil Thangavelu <kapil.foss@gmail.com>
20#
21"""
22Intro
23-----
24
25A simple way to store state in units. This provides a key value
26storage with support for versioned, transactional operation,
27and can calculate deltas from previous values to simplify unit logic
28when processing changes.
29
30
31Hook Integration
32----------------
33
34There are several extant frameworks for hook execution, including
35
36 - charmhelpers.core.hookenv.Hooks
37 - charmhelpers.core.services.ServiceManager
38
39The storage classes are framework agnostic, one simple integration is
40via the HookData contextmanager. It will record the current hook
41execution environment (including relation data, config data, etc.),
42setup a transaction and allow easy access to the changes from
43previously seen values. One consequence of the integration is the
44reservation of particular keys ('rels', 'unit', 'env', 'config',
45'charm_revisions') for their respective values.
46
47Here's a fully worked integration example using hookenv.Hooks::
48
49 from charmhelper.core import hookenv, unitdata
50
51 hook_data = unitdata.HookData()
52 db = unitdata.kv()
53 hooks = hookenv.Hooks()
54
55 @hooks.hook
56 def config_changed():
57 # Print all changes to configuration from previously seen
58 # values.
59 for changed, (prev, cur) in hook_data.conf.items():
60 print('config changed', changed,
61 'previous value', prev,
62 'current value', cur)
63
64 # Get some unit specific bookeeping
65 if not db.get('pkg_key'):
66 key = urllib.urlopen('https://example.com/pkg_key').read()
67 db.set('pkg_key', key)
68
69 # Directly access all charm config as a mapping.
70 conf = db.getrange('config', True)
71
72 # Directly access all relation data as a mapping
73 rels = db.getrange('rels', True)
74
75 if __name__ == '__main__':
76 with hook_data():
77 hook.execute()
78
79
80A more basic integration is via the hook_scope context manager which simply
81manages transaction scope (and records hook name, and timestamp)::
82
83 >>> from unitdata import kv
84 >>> db = kv()
85 >>> with db.hook_scope('install'):
86 ... # do work, in transactional scope.
87 ... db.set('x', 1)
88 >>> db.get('x')
89 1
90
91
92Usage
93-----
94
95Values are automatically json de/serialized to preserve basic typing
96and complex data struct capabilities (dicts, lists, ints, booleans, etc).
97
98Individual values can be manipulated via get/set::
99
100 >>> kv.set('y', True)
101 >>> kv.get('y')
102 True
103
104 # We can set complex values (dicts, lists) as a single key.
105 >>> kv.set('config', {'a': 1, 'b': True'})
106
107 # Also supports returning dictionaries as a record which
108 # provides attribute access.
109 >>> config = kv.get('config', record=True)
110 >>> config.b
111 True
112
113
114Groups of keys can be manipulated with update/getrange::
115
116 >>> kv.update({'z': 1, 'y': 2}, prefix="gui.")
117 >>> kv.getrange('gui.', strip=True)
118 {'z': 1, 'y': 2}
119
120When updating values, its very helpful to understand which values
121have actually changed and how have they changed. The storage
122provides a delta method to provide for this::
123
124 >>> data = {'debug': True, 'option': 2}
125 >>> delta = kv.delta(data, 'config.')
126 >>> delta.debug.previous
127 None
128 >>> delta.debug.current
129 True
130 >>> delta
131 {'debug': (None, True), 'option': (None, 2)}
132
133Note the delta method does not persist the actual change, it needs to
134be explicitly saved via 'update' method::
135
136 >>> kv.update(data, 'config.')
137
138Values modified in the context of a hook scope retain historical values
139associated to the hookname.
140
141 >>> with db.hook_scope('config-changed'):
142 ... db.set('x', 42)
143 >>> db.gethistory('x')
144 [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'),
145 (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')]
146
147"""
148
149import collections
150import contextlib
151import datetime
152import itertools
153import json
154import os
155import pprint
156import sqlite3
157import sys
158
159__author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>'
160
161
162class Storage(object):
163 """Simple key value database for local unit state within charms.
164
165 Modifications are not persisted unless :meth:`flush` is called.
166
167 To support dicts, lists, integer, floats, and booleans values
168 are automatically json encoded/decoded.
169 """
170 def __init__(self, path=None):
171 self.db_path = path
172 if path is None:
173 if 'UNIT_STATE_DB' in os.environ:
174 self.db_path = os.environ['UNIT_STATE_DB']
175 else:
176 self.db_path = os.path.join(
177 os.environ.get('CHARM_DIR', ''), '.unit-state.db')
178 self.conn = sqlite3.connect('%s' % self.db_path)
179 self.cursor = self.conn.cursor()
180 self.revision = None
181 self._closed = False
182 self._init()
183
184 def close(self):
185 if self._closed:
186 return
187 self.flush(False)
188 self.cursor.close()
189 self.conn.close()
190 self._closed = True
191
192 def get(self, key, default=None, record=False):
193 self.cursor.execute('select data from kv where key=?', [key])
194 result = self.cursor.fetchone()
195 if not result:
196 return default
197 if record:
198 return Record(json.loads(result[0]))
199 return json.loads(result[0])
200
201 def getrange(self, key_prefix, strip=False):
202 """
203 Get a range of keys starting with a common prefix as a mapping of
204 keys to values.
205
206 :param str key_prefix: Common prefix among all keys
207 :param bool strip: Optionally strip the common prefix from the key
208 names in the returned dict
209 :return dict: A (possibly empty) dict of key-value mappings
210 """
211 self.cursor.execute("select key, data from kv where key like ?",
212 ['%s%%' % key_prefix])
213 result = self.cursor.fetchall()
214
215 if not result:
216 return {}
217 if not strip:
218 key_prefix = ''
219 return dict([
220 (k[len(key_prefix):], json.loads(v)) for k, v in result])
221
222 def update(self, mapping, prefix=""):
223 """
224 Set the values of multiple keys at once.
225
226 :param dict mapping: Mapping of keys to values
227 :param str prefix: Optional prefix to apply to all keys in `mapping`
228 before setting
229 """
230 for k, v in mapping.items():
231 self.set("%s%s" % (prefix, k), v)
232
233 def unset(self, key):
234 """
235 Remove a key from the database entirely.
236 """
237 self.cursor.execute('delete from kv where key=?', [key])
238 if self.revision and self.cursor.rowcount:
239 self.cursor.execute(
240 'insert into kv_revisions values (?, ?, ?)',
241 [key, self.revision, json.dumps('DELETED')])
242
243 def unsetrange(self, keys=None, prefix=""):
244 """
245 Remove a range of keys starting with a common prefix, from the database
246 entirely.
247
248 :param list keys: List of keys to remove.
249 :param str prefix: Optional prefix to apply to all keys in ``keys``
250 before removing.
251 """
252 if keys is not None:
253 keys = ['%s%s' % (prefix, key) for key in keys]
254 self.cursor.execute('delete from kv where key in (%s)' % ','.join(['?'] * len(keys)), keys)
255 if self.revision and self.cursor.rowcount:
256 self.cursor.execute(
257 'insert into kv_revisions values %s' % ','.join(['(?, ?, ?)'] * len(keys)),
258 list(itertools.chain.from_iterable((key, self.revision, json.dumps('DELETED')) for key in keys)))
259 else:
260 self.cursor.execute('delete from kv where key like ?',
261 ['%s%%' % prefix])
262 if self.revision and self.cursor.rowcount:
263 self.cursor.execute(
264 'insert into kv_revisions values (?, ?, ?)',
265 ['%s%%' % prefix, self.revision, json.dumps('DELETED')])
266
267 def set(self, key, value):
268 """
269 Set a value in the database.
270
271 :param str key: Key to set the value for
272 :param value: Any JSON-serializable value to be set
273 """
274 serialized = json.dumps(value)
275
276 self.cursor.execute('select data from kv where key=?', [key])
277 exists = self.cursor.fetchone()
278
279 # Skip mutations to the same value
280 if exists:
281 if exists[0] == serialized:
282 return value
283
284 if not exists:
285 self.cursor.execute(
286 'insert into kv (key, data) values (?, ?)',
287 (key, serialized))
288 else:
289 self.cursor.execute('''
290 update kv
291 set data = ?
292 where key = ?''', [serialized, key])
293
294 # Save
295 if not self.revision:
296 return value
297
298 self.cursor.execute(
299 'select 1 from kv_revisions where key=? and revision=?',
300 [key, self.revision])
301 exists = self.cursor.fetchone()
302
303 if not exists:
304 self.cursor.execute(
305 '''insert into kv_revisions (
306 revision, key, data) values (?, ?, ?)''',
307 (self.revision, key, serialized))
308 else:
309 self.cursor.execute(
310 '''
311 update kv_revisions
312 set data = ?
313 where key = ?
314 and revision = ?''',
315 [serialized, key, self.revision])
316
317 return value
318
319 def delta(self, mapping, prefix):
320 """
321 return a delta containing values that have changed.
322 """
323 previous = self.getrange(prefix, strip=True)
324 if not previous:
325 pk = set()
326 else:
327 pk = set(previous.keys())
328 ck = set(mapping.keys())
329 delta = DeltaSet()
330
331 # added
332 for k in ck.difference(pk):
333 delta[k] = Delta(None, mapping[k])
334
335 # removed
336 for k in pk.difference(ck):
337 delta[k] = Delta(previous[k], None)
338
339 # changed
340 for k in pk.intersection(ck):
341 c = mapping[k]
342 p = previous[k]
343 if c != p:
344 delta[k] = Delta(p, c)
345
346 return delta
347
348 @contextlib.contextmanager
349 def hook_scope(self, name=""):
350 """Scope all future interactions to the current hook execution
351 revision."""
352 assert not self.revision
353 self.cursor.execute(
354 'insert into hooks (hook, date) values (?, ?)',
355 (name or sys.argv[0],
356 datetime.datetime.utcnow().isoformat()))
357 self.revision = self.cursor.lastrowid
358 try:
359 yield self.revision
360 self.revision = None
361 except:
362 self.flush(False)
363 self.revision = None
364 raise
365 else:
366 self.flush()
367
368 def flush(self, save=True):
369 if save:
370 self.conn.commit()
371 elif self._closed:
372 return
373 else:
374 self.conn.rollback()
375
376 def _init(self):
377 self.cursor.execute('''
378 create table if not exists kv (
379 key text,
380 data text,
381 primary key (key)
382 )''')
383 self.cursor.execute('''
384 create table if not exists kv_revisions (
385 key text,
386 revision integer,
387 data text,
388 primary key (key, revision)
389 )''')
390 self.cursor.execute('''
391 create table if not exists hooks (
392 version integer primary key autoincrement,
393 hook text,
394 date text
395 )''')
396 self.conn.commit()
397
398 def gethistory(self, key, deserialize=False):
399 self.cursor.execute(
400 '''
401 select kv.revision, kv.key, kv.data, h.hook, h.date
402 from kv_revisions kv,
403 hooks h
404 where kv.key=?
405 and kv.revision = h.version
406 ''', [key])
407 if deserialize is False:
408 return self.cursor.fetchall()
409 return map(_parse_history, self.cursor.fetchall())
410
411 def debug(self, fh=sys.stderr):
412 self.cursor.execute('select * from kv')
413 pprint.pprint(self.cursor.fetchall(), stream=fh)
414 self.cursor.execute('select * from kv_revisions')
415 pprint.pprint(self.cursor.fetchall(), stream=fh)
416
417
418def _parse_history(d):
419 return (d[0], d[1], json.loads(d[2]), d[3],
420 datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f"))
421
422
423class HookData(object):
424 """Simple integration for existing hook exec frameworks.
425
426 Records all unit information, and stores deltas for processing
427 by the hook.
428
429 Sample::
430
431 from charmhelper.core import hookenv, unitdata
432
433 changes = unitdata.HookData()
434 db = unitdata.kv()
435 hooks = hookenv.Hooks()
436
437 @hooks.hook
438 def config_changed():
439 # View all changes to configuration
440 for changed, (prev, cur) in changes.conf.items():
441 print('config changed', changed,
442 'previous value', prev,
443 'current value', cur)
444
445 # Get some unit specific bookeeping
446 if not db.get('pkg_key'):
447 key = urllib.urlopen('https://example.com/pkg_key').read()
448 db.set('pkg_key', key)
449
450 if __name__ == '__main__':
451 with changes():
452 hook.execute()
453
454 """
455 def __init__(self):
456 self.kv = kv()
457 self.conf = None
458 self.rels = None
459
460 @contextlib.contextmanager
461 def __call__(self):
462 from charmhelpers.core import hookenv
463 hook_name = hookenv.hook_name()
464
465 with self.kv.hook_scope(hook_name):
466 self._record_charm_version(hookenv.charm_dir())
467 delta_config, delta_relation = self._record_hook(hookenv)
468 yield self.kv, delta_config, delta_relation
469
470 def _record_charm_version(self, charm_dir):
471 # Record revisions.. charm revisions are meaningless
472 # to charm authors as they don't control the revision.
473 # so logic dependnent on revision is not particularly
474 # useful, however it is useful for debugging analysis.
475 charm_rev = open(
476 os.path.join(charm_dir, 'revision')).read().strip()
477 charm_rev = charm_rev or '0'
478 revs = self.kv.get('charm_revisions', [])
479 if charm_rev not in revs:
480 revs.append(charm_rev.strip() or '0')
481 self.kv.set('charm_revisions', revs)
482
483 def _record_hook(self, hookenv):
484 data = hookenv.execution_environment()
485 self.conf = conf_delta = self.kv.delta(data['conf'], 'config')
486 self.rels = rels_delta = self.kv.delta(data['rels'], 'rels')
487 self.kv.set('env', dict(data['env']))
488 self.kv.set('unit', data['unit'])
489 self.kv.set('relid', data.get('relid'))
490 return conf_delta, rels_delta
491
492
493class Record(dict):
494
495 __slots__ = ()
496
497 def __getattr__(self, k):
498 if k in self:
499 return self[k]
500 raise AttributeError(k)
501
502
503class DeltaSet(Record):
504
505 __slots__ = ()
506
507
508Delta = collections.namedtuple('Delta', ['previous', 'current'])
509
510
511_KV = None
512
513
514def kv():
515 global _KV
516 if _KV is None:
517 _KV = Storage()
518 return _KV
diff --git a/hooks/charmhelpers/fetch/__init__.py b/hooks/charmhelpers/fetch/__init__.py
index 07bb707..480a627 100644
--- a/hooks/charmhelpers/fetch/__init__.py
+++ b/hooks/charmhelpers/fetch/__init__.py
@@ -1,267 +1,191 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
1import importlib15import importlib
16from charmhelpers.osplatform import get_platform
2from yaml import safe_load17from yaml import safe_load
3from charmhelpers.core.host import (
4 lsb_release
5)
6from urlparse import (
7 urlparse,
8 urlunparse,
9)
10import subprocess
11from charmhelpers.core.hookenv import (18from charmhelpers.core.hookenv import (
12 config,19 config,
13 log,20 log,
14)21)
15import apt_pkg
16import os
17
18CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
19deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
20"""
21PROPOSED_POCKET = """# Proposed
22deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
23"""
24CLOUD_ARCHIVE_POCKETS = {
25 # Folsom
26 'folsom': 'precise-updates/folsom',
27 'precise-folsom': 'precise-updates/folsom',
28 'precise-folsom/updates': 'precise-updates/folsom',
29 'precise-updates/folsom': 'precise-updates/folsom',
30 'folsom/proposed': 'precise-proposed/folsom',
31 'precise-folsom/proposed': 'precise-proposed/folsom',
32 'precise-proposed/folsom': 'precise-proposed/folsom',
33 # Grizzly
34 'grizzly': 'precise-updates/grizzly',
35 'precise-grizzly': 'precise-updates/grizzly',
36 'precise-grizzly/updates': 'precise-updates/grizzly',
37 'precise-updates/grizzly': 'precise-updates/grizzly',
38 'grizzly/proposed': 'precise-proposed/grizzly',
39 'precise-grizzly/proposed': 'precise-proposed/grizzly',
40 'precise-proposed/grizzly': 'precise-proposed/grizzly',
41 # Havana
42 'havana': 'precise-updates/havana',
43 'precise-havana': 'precise-updates/havana',
44 'precise-havana/updates': 'precise-updates/havana',
45 'precise-updates/havana': 'precise-updates/havana',
46 'havana/proposed': 'precise-proposed/havana',
47 'precise-havana/proposed': 'precise-proposed/havana',
48 'precise-proposed/havana': 'precise-proposed/havana',
49 # Icehouse
50 'icehouse': 'precise-updates/icehouse',
51 'precise-icehouse': 'precise-updates/icehouse',
52 'precise-icehouse/updates': 'precise-updates/icehouse',
53 'precise-updates/icehouse': 'precise-updates/icehouse',
54 'icehouse/proposed': 'precise-proposed/icehouse',
55 'precise-icehouse/proposed': 'precise-proposed/icehouse',
56 'precise-proposed/icehouse': 'precise-proposed/icehouse',
57}
58
59
60def filter_installed_packages(packages):
61 """Returns a list of packages that require installation"""
62 apt_pkg.init()
63 cache = apt_pkg.Cache()
64 _pkgs = []
65 for package in packages:
66 try:
67 p = cache[package]
68 p.current_ver or _pkgs.append(package)
69 except KeyError:
70 log('Package {} has no installation candidate.'.format(package),
71 level='WARNING')
72 _pkgs.append(package)
73 return _pkgs
7422
23import six
24if six.PY3:
25 from urllib.parse import urlparse, urlunparse
26else:
27 from urlparse import urlparse, urlunparse
7528
76def apt_install(packages, options=None, fatal=False):
77 """Install one or more packages"""
78 if options is None:
79 options = ['--option=Dpkg::Options::=--force-confold']
8029
81 cmd = ['apt-get', '--assume-yes']30# The order of this list is very important. Handlers should be listed in from
82 cmd.extend(options)31# least- to most-specific URL matching.
83 cmd.append('install')32FETCH_HANDLERS = (
84 if isinstance(packages, basestring):33 'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
85 cmd.append(packages)34 'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',
86 else:35 'charmhelpers.fetch.giturl.GitUrlFetchHandler',
87 cmd.extend(packages)36)
88 log("Installing {} with options: {}".format(packages,
89 options))
90 env = os.environ.copy()
91 if 'DEBIAN_FRONTEND' not in env:
92 env['DEBIAN_FRONTEND'] = 'noninteractive'
93
94 if fatal:
95 subprocess.check_call(cmd, env=env)
96 else:
97 subprocess.call(cmd, env=env)
9837
9938
100def apt_update(fatal=False):39class SourceConfigError(Exception):
101 """Update local apt cache"""40 pass
102 cmd = ['apt-get', 'update']
103 if fatal:
104 subprocess.check_call(cmd)
105 else:
106 subprocess.call(cmd)
10741
10842
109def apt_purge(packages, fatal=False):43class UnhandledSource(Exception):
110 """Purge one or more packages"""44 pass
111 cmd = ['apt-get', '--assume-yes', 'purge']
112 if isinstance(packages, basestring):
113 cmd.append(packages)
114 else:
115 cmd.extend(packages)
116 log("Purging {}".format(packages))
117 if fatal:
118 subprocess.check_call(cmd)
119 else:
120 subprocess.call(cmd)
12145
12246
123def apt_hold(packages, fatal=False):47class AptLockError(Exception):
124 """Hold one or more packages"""48 pass
125 cmd = ['apt-mark', 'hold']
126 if isinstance(packages, basestring):
127 cmd.append(packages)
128 else:
129 cmd.extend(packages)
130 log("Holding {}".format(packages))
131 if fatal:
132 subprocess.check_call(cmd)
133 else:
134 subprocess.call(cmd)
135
136
137def add_source(source, key=None):
138 if (source.startswith('ppa:') or
139 source.startswith('http') or
140 source.startswith('deb ') or
141 source.startswith('cloud-archive:')):
142 subprocess.check_call(['add-apt-repository', '--yes', source])
143 elif source.startswith('cloud:'):
144 apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
145 fatal=True)
146 pocket = source.split(':')[-1]
147 if pocket not in CLOUD_ARCHIVE_POCKETS:
148 raise SourceConfigError(
149 'Unsupported cloud: source option %s' %
150 pocket)
151 actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
152 with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
153 apt.write(CLOUD_ARCHIVE.format(actual_pocket))
154 elif source == 'proposed':
155 release = lsb_release()['DISTRIB_CODENAME']
156 with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
157 apt.write(PROPOSED_POCKET.format(release))
158 if key:
159 subprocess.check_call(['apt-key', 'adv', '--keyserver',
160 'keyserver.ubuntu.com', '--recv',
161 key])
16249
16350
164class SourceConfigError(Exception):51class GPGKeyError(Exception):
52 """Exception occurs when a GPG key cannot be fetched or used. The message
53 indicates what the problem is.
54 """
165 pass55 pass
16656
16757
58class BaseFetchHandler(object):
59
60 """Base class for FetchHandler implementations in fetch plugins"""
61
62 def can_handle(self, source):
63 """Returns True if the source can be handled. Otherwise returns
64 a string explaining why it cannot"""
65 return "Wrong source type"
66
67 def install(self, source):
68 """Try to download and unpack the source. Return the path to the
69 unpacked files or raise UnhandledSource."""
70 raise UnhandledSource("Wrong source type {}".format(source))
71
72 def parse_url(self, url):
73 return urlparse(url)
74
75 def base_url(self, url):
76 """Return url without querystring or fragment"""
77 parts = list(self.parse_url(url))
78 parts[4:] = ['' for i in parts[4:]]
79 return urlunparse(parts)
80
81
82__platform__ = get_platform()
83module = "charmhelpers.fetch.%s" % __platform__
84fetch = importlib.import_module(module)
85
86filter_installed_packages = fetch.filter_installed_packages
87install = fetch.apt_install
88upgrade = fetch.apt_upgrade
89update = _fetch_update = fetch.apt_update
90purge = fetch.apt_purge
91add_source = fetch.add_source
92
93if __platform__ == "ubuntu":
94 apt_cache = fetch.apt_cache
95 apt_install = fetch.apt_install
96 apt_update = fetch.apt_update
97 apt_upgrade = fetch.apt_upgrade
98 apt_purge = fetch.apt_purge
99 apt_mark = fetch.apt_mark
100 apt_hold = fetch.apt_hold
101 apt_unhold = fetch.apt_unhold
102 import_key = fetch.import_key
103 get_upstream_version = fetch.get_upstream_version
104elif __platform__ == "centos":
105 yum_search = fetch.yum_search
106
107
168def configure_sources(update=False,108def configure_sources(update=False,
169 sources_var='install_sources',109 sources_var='install_sources',
170 keys_var='install_keys'):110 keys_var='install_keys'):
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches