Merge lp:~xavpaice/charms/trusty/thruk-master/trunk into lp:~canonical-bootstack/charms/trusty/thruk-master/trunk

Proposed by Xav Paice
Status: Merged
Merged at revision: 23
Proposed branch: lp:~xavpaice/charms/trusty/thruk-master/trunk
Merge into: lp:~canonical-bootstack/charms/trusty/thruk-master/trunk
Diff against target: 4794 lines (+3049/-823)
33 files modified
charm-helpers.yaml (+1/-0)
config.yaml (+21/-3)
hooks/actions.py (+10/-12)
hooks/charmhelpers/__init__.py (+72/-13)
hooks/charmhelpers/core/__init__.py (+11/-13)
hooks/charmhelpers/core/decorators.py (+11/-13)
hooks/charmhelpers/core/files.py (+43/-0)
hooks/charmhelpers/core/fstab.py (+11/-13)
hooks/charmhelpers/core/hookenv.py (+530/-56)
hooks/charmhelpers/core/host.py (+629/-155)
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/centos.py (+17/-0)
hooks/charmhelpers/core/kernel_factory/ubuntu.py (+13/-0)
hooks/charmhelpers/core/services/__init__.py (+11/-13)
hooks/charmhelpers/core/services/base.py (+54/-32)
hooks/charmhelpers/core/services/helpers.py (+42/-19)
hooks/charmhelpers/core/strutils.py (+96/-15)
hooks/charmhelpers/core/sysctl.py (+11/-13)
hooks/charmhelpers/core/templating.py (+40/-24)
hooks/charmhelpers/core/unitdata.py (+72/-31)
hooks/charmhelpers/fetch/__init__.py (+54/-288)
hooks/charmhelpers/fetch/archiveurl.py (+19/-15)
hooks/charmhelpers/fetch/bzrurl.py (+48/-50)
hooks/charmhelpers/fetch/centos.py (+171/-0)
hooks/charmhelpers/fetch/giturl.py (+37/-39)
hooks/charmhelpers/fetch/snap.py (+122/-0)
hooks/charmhelpers/fetch/ubuntu.py (+568/-0)
hooks/charmhelpers/osplatform.py (+25/-0)
hooks/install.real (+4/-6)
templates/thruk_local.conf (+4/-0)
To merge this branch: bzr merge lp:~xavpaice/charms/trusty/thruk-master/trunk
Reviewer Review Type Date Requested Status
James Hebden (community) Approve
Review via email: mp+328013@code.launchpad.net

Description of the change

Update Thruk to PPA version 2.14 and use LMD

To post a comment you must log in.
Revision history for this message
James Troup (elmo) wrote :

Have you not made a PPA mandatory with this change? That's fine if it's deliberate, but config.yaml should be updated to not claim 'source' is optional.

Revision history for this message
James Hebden (ec0) :
review: Approve
23. By James Hebden

[jhebden, r=elmo] Fixed working around PPAs, merged changes for 328013

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

Merging, but fixing the wording as pointed out by elmo.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'charm-helpers.yaml'
--- charm-helpers.yaml 2015-04-09 06:46:35 +0000
+++ charm-helpers.yaml 2017-07-25 09:17:37 +0000
@@ -3,3 +3,4 @@
3include:3include:
4 - core4 - core
5 - fetch5 - fetch
6 - osplatform
67
=== modified file 'config.yaml'
--- config.yaml 2015-08-31 04:46:54 +0000
+++ config.yaml 2017-07-25 09:17:37 +0000
@@ -1,8 +1,22 @@
1options:1options:
2 source:2 source:
3 type: string3 type: string
4 default: "ppa:brad-marshall/thruk"4 default: ppa:canonical-bootstack/thruk
5 description: "PPA to install thruk"5 description: |
6 Optional configuration to support use of additional sources such as:
7
8 - ppa:myteam/ppa
9 - cloud:trusty-proposed/kilo
10 - http://my.archive.com/ubuntu main
11
12 The last option should be used in conjunction with the key configuration
13 option.
14 key:
15 type: string
16 default: 7A207542
17 description: |
18 Key ID to import to the apt keyring to support use with arbitary source
19 configuration from outside of Launchpad archives or PPA's.
6 url:20 url:
7 type: string21 type: string
8 default: "nagios.example.com"22 default: "nagios.example.com"
@@ -55,4 +69,8 @@
55 description: >69 description: >
56 Enables debug logging to stderr for thruk - in the default case, this ends70 Enables debug logging to stderr for thruk - in the default case, this ends
57 up in the apache error logs.71 up in the apache error logs.
5872 enable-lmd:
73 default: true
74 type: boolean
75 description: >
76 If enabled, make use of the LMD - Livestatus Multitool Daemon
5977
=== modified file 'hooks/actions.py'
--- hooks/actions.py 2017-03-21 04:14:41 +0000
+++ hooks/actions.py 2017-07-25 09:17:37 +0000
@@ -1,6 +1,6 @@
1from charmhelpers.core import hookenv1from charmhelpers.core import hookenv
2from charmhelpers.fetch import (2from charmhelpers.fetch import (
3 apt_install, apt_update, add_source3 apt_install, apt_update, add_source, apt_purge
4)4)
5import subprocess5import subprocess
6import os6import os
@@ -17,17 +17,15 @@
1717
18def update_ppa(service_name):18def update_ppa(service_name):
19 config = hookenv.config()19 config = hookenv.config()
2020 new_source = config.get('source')
21 if config.changed('source'):21 prev_source = config.previous('source')
22 prev_ppa = config.previous('source')22 if prev_source is not None and prev_source != new_source:
23 if prev_ppa is not None:23 subprocess.check_call(['add-apt-repository',
24 subprocess.check_call(['add-apt-repository',24 '--yes', '--remove', prev_source])
25 '--yes', '--remove', prev_ppa])25 add_source(config.get('source'), config.get('key', None))
26 ppa = config.get('source')26 apt_update(fatal=True)
27 if ppa is not None:27 package_list = ["thruk", "pwgen", "apache2-utils"]
28 add_source(ppa)28 apt_install(packages=package_list, fatal=True)
29 apt_update()
30 apt_install(["thruk"])
3129
3230
33def thruk_set_password(service_name):31def thruk_set_password(service_name):
3432
=== modified file 'hooks/charmhelpers/__init__.py'
--- hooks/charmhelpers/__init__.py 2015-04-09 06:46:35 +0000
+++ hooks/charmhelpers/__init__.py 2017-07-25 09:17:37 +0000
@@ -1,21 +1,24 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17# Bootstrap charm-helpers, installing its dependencies if necessary using15# Bootstrap charm-helpers, installing its dependencies if necessary using
18# only standard libraries.16# only standard libraries.
17from __future__ import print_function
18from __future__ import absolute_import
19
20import functools
21import inspect
19import subprocess22import subprocess
20import sys23import sys
2124
@@ -36,3 +39,59 @@
36 else:39 else:
37 subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])40 subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
38 import yaml # flake8: noqa41 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
3998
=== modified file 'hooks/charmhelpers/core/__init__.py'
--- hooks/charmhelpers/core/__init__.py 2015-04-09 06:46:35 +0000
+++ hooks/charmhelpers/core/__init__.py 2017-07-25 09:17:37 +0000
@@ -1,15 +1,13 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
=== modified file 'hooks/charmhelpers/core/decorators.py'
--- hooks/charmhelpers/core/decorators.py 2015-04-09 06:46:35 +0000
+++ hooks/charmhelpers/core/decorators.py 2017-07-25 09:17:37 +0000
@@ -1,18 +1,16 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17#15#
18# Copyright 2014 Canonical Ltd.16# Copyright 2014 Canonical Ltd.
1917
=== added file 'hooks/charmhelpers/core/files.py'
--- hooks/charmhelpers/core/files.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/files.py 2017-07-25 09:17:37 +0000
@@ -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)])
044
=== modified file 'hooks/charmhelpers/core/fstab.py'
--- hooks/charmhelpers/core/fstab.py 2015-04-09 06:46:35 +0000
+++ hooks/charmhelpers/core/fstab.py 2017-07-25 09:17:37 +0000
@@ -3,19 +3,17 @@
33
4# Copyright 2014-2015 Canonical Limited.4# Copyright 2014-2015 Canonical Limited.
5#5#
6# This file is part of charm-helpers.6# Licensed under the Apache License, Version 2.0 (the "License");
7#7# you may not use this file except in compliance with the License.
8# charm-helpers is free software: you can redistribute it and/or modify8# You may obtain a copy of the License at
9# it under the terms of the GNU Lesser General Public License version 3 as9#
10# published by the Free Software Foundation.10# http://www.apache.org/licenses/LICENSE-2.0
11#11#
12# charm-helpers is distributed in the hope that it will be useful,12# Unless required by applicable law or agreed to in writing, software
13# but WITHOUT ANY WARRANTY; without even the implied warranty of13# distributed under the License is distributed on an "AS IS" BASIS,
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# GNU Lesser General Public License for more details.15# See the License for the specific language governing permissions and
16#16# limitations under the License.
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1917
20import io18import io
21import os19import os
2220
=== modified file 'hooks/charmhelpers/core/hookenv.py'
--- hooks/charmhelpers/core/hookenv.py 2015-04-09 06:46:35 +0000
+++ hooks/charmhelpers/core/hookenv.py 2017-07-25 09:17:37 +0000
@@ -1,18 +1,16 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17"Interactions with the Juju environment"15"Interactions with the Juju environment"
18# Copyright 2013 Canonical Ltd.16# Copyright 2013 Canonical Ltd.
@@ -20,11 +18,18 @@
20# Authors:18# Authors:
21# Charm Helpers Developers <juju@lists.ubuntu.com>19# Charm Helpers Developers <juju@lists.ubuntu.com>
2220
21from __future__ import print_function
22import copy
23from distutils.version import LooseVersion
24from functools import wraps
25import glob
23import os26import os
24import json27import json
25import yaml28import yaml
26import subprocess29import subprocess
27import sys30import sys
31import errno
32import tempfile
28from subprocess import CalledProcessError33from subprocess import CalledProcessError
2934
30import six35import six
@@ -56,15 +61,18 @@
5661
57 will cache the result of unit_get + 'test' for future calls.62 will cache the result of unit_get + 'test' for future calls.
58 """63 """
64 @wraps(func)
59 def wrapper(*args, **kwargs):65 def wrapper(*args, **kwargs):
60 global cache66 global cache
61 key = str((func, args, kwargs))67 key = str((func, args, kwargs))
62 try:68 try:
63 return cache[key]69 return cache[key]
64 except KeyError:70 except KeyError:
65 res = func(*args, **kwargs)71 pass # Drop out of the exception handler scope.
66 cache[key] = res72 res = func(*args, **kwargs)
67 return res73 cache[key] = res
74 return res
75 wrapper._wrapped = func
68 return wrapper76 return wrapper
6977
7078
@@ -87,7 +95,18 @@
87 if not isinstance(message, six.string_types):95 if not isinstance(message, six.string_types):
88 message = repr(message)96 message = repr(message)
89 command += [message]97 command += [message]
90 subprocess.call(command)98 # Missing juju-log should not cause failures in unit tests
99 # Send log output to stderr
100 try:
101 subprocess.call(command)
102 except OSError as e:
103 if e.errno == errno.ENOENT:
104 if level:
105 message = "{}: {}".format(level, message)
106 message = "juju-log: {}".format(message)
107 print(message, file=sys.stderr)
108 else:
109 raise
91110
92111
93class Serializable(UserDict):112class Serializable(UserDict):
@@ -153,9 +172,19 @@
153 return os.environ.get('JUJU_RELATION', None)172 return os.environ.get('JUJU_RELATION', None)
154173
155174
156def relation_id():175@cached
157 """The relation ID for the current relation hook"""176def relation_id(relation_name=None, service_or_unit=None):
158 return os.environ.get('JUJU_RELATION_ID', None)177 """The relation ID for the current or a specified relation"""
178 if not relation_name and not service_or_unit:
179 return os.environ.get('JUJU_RELATION_ID', None)
180 elif relation_name and service_or_unit:
181 service_name = service_or_unit.split('/')[0]
182 for relid in relation_ids(relation_name):
183 remote_service = remote_service_name(relid)
184 if remote_service == service_name:
185 return relid
186 else:
187 raise ValueError('Must specify neither or both of relation_name and service_or_unit')
159188
160189
161def local_unit():190def local_unit():
@@ -165,7 +194,7 @@
165194
166def remote_unit():195def remote_unit():
167 """The remote unit for the current relation hook"""196 """The remote unit for the current relation hook"""
168 return os.environ['JUJU_REMOTE_UNIT']197 return os.environ.get('JUJU_REMOTE_UNIT', None)
169198
170199
171def service_name():200def service_name():
@@ -173,9 +202,20 @@
173 return local_unit().split('/')[0]202 return local_unit().split('/')[0]
174203
175204
205@cached
206def remote_service_name(relid=None):
207 """The remote service name for a given relation-id (or the current relation)"""
208 if relid is None:
209 unit = remote_unit()
210 else:
211 units = related_units(relid)
212 unit = units[0] if units else None
213 return unit.split('/')[0] if unit else None
214
215
176def hook_name():216def hook_name():
177 """The name of the currently executing hook"""217 """The name of the currently executing hook"""
178 return os.path.basename(sys.argv[0])218 return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0]))
179219
180220
181class Config(dict):221class Config(dict):
@@ -225,23 +265,7 @@
225 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)265 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
226 if os.path.exists(self.path):266 if os.path.exists(self.path):
227 self.load_previous()267 self.load_previous()
228268 atexit(self._implicit_save)
229 def __getitem__(self, key):
230 """For regular dict lookups, check the current juju config first,
231 then the previous (saved) copy. This ensures that user-saved values
232 will be returned by a dict lookup.
233
234 """
235 try:
236 return dict.__getitem__(self, key)
237 except KeyError:
238 return (self._prev_dict or {})[key]
239
240 def keys(self):
241 prev_keys = []
242 if self._prev_dict is not None:
243 prev_keys = self._prev_dict.keys()
244 return list(set(prev_keys + list(dict.keys(self))))
245269
246 def load_previous(self, path=None):270 def load_previous(self, path=None):
247 """Load previous copy of config from disk.271 """Load previous copy of config from disk.
@@ -260,6 +284,9 @@
260 self.path = path or self.path284 self.path = path or self.path
261 with open(self.path) as f:285 with open(self.path) as f:
262 self._prev_dict = json.load(f)286 self._prev_dict = json.load(f)
287 for k, v in copy.deepcopy(self._prev_dict).items():
288 if k not in self:
289 self[k] = v
263290
264 def changed(self, key):291 def changed(self, key):
265 """Return True if the current value for this key is different from292 """Return True if the current value for this key is different from
@@ -291,13 +318,13 @@
291 instance.318 instance.
292319
293 """320 """
294 if self._prev_dict:
295 for k, v in six.iteritems(self._prev_dict):
296 if k not in self:
297 self[k] = v
298 with open(self.path, 'w') as f:321 with open(self.path, 'w') as f:
299 json.dump(self, f)322 json.dump(self, f)
300323
324 def _implicit_save(self):
325 if self.implicit_save:
326 self.save()
327
301328
302@cached329@cached
303def config(scope=None):330def config(scope=None):
@@ -305,6 +332,8 @@
305 config_cmd_line = ['config-get']332 config_cmd_line = ['config-get']
306 if scope is not None:333 if scope is not None:
307 config_cmd_line.append(scope)334 config_cmd_line.append(scope)
335 else:
336 config_cmd_line.append('--all')
308 config_cmd_line.append('--format=json')337 config_cmd_line.append('--format=json')
309 try:338 try:
310 config_data = json.loads(339 config_data = json.loads(
@@ -340,18 +369,49 @@
340 """Set relation information for the current unit"""369 """Set relation information for the current unit"""
341 relation_settings = relation_settings if relation_settings else {}370 relation_settings = relation_settings if relation_settings else {}
342 relation_cmd_line = ['relation-set']371 relation_cmd_line = ['relation-set']
372 accepts_file = "--file" in subprocess.check_output(
373 relation_cmd_line + ["--help"], universal_newlines=True)
343 if relation_id is not None:374 if relation_id is not None:
344 relation_cmd_line.extend(('-r', relation_id))375 relation_cmd_line.extend(('-r', relation_id))
345 for k, v in (list(relation_settings.items()) + list(kwargs.items())):376 settings = relation_settings.copy()
346 if v is None:377 settings.update(kwargs)
347 relation_cmd_line.append('{}='.format(k))378 for key, value in settings.items():
348 else:379 # Force value to be a string: it always should, but some call
349 relation_cmd_line.append('{}={}'.format(k, v))380 # sites pass in things like dicts or numbers.
350 subprocess.check_call(relation_cmd_line)381 if value is not None:
382 settings[key] = "{}".format(value)
383 if accepts_file:
384 # --file was introduced in Juju 1.23.2. Use it by default if
385 # available, since otherwise we'll break if the relation data is
386 # too big. Ideally we should tell relation-set to read the data from
387 # stdin, but that feature is broken in 1.23.2: Bug #1454678.
388 with tempfile.NamedTemporaryFile(delete=False) as settings_file:
389 settings_file.write(yaml.safe_dump(settings).encode("utf-8"))
390 subprocess.check_call(
391 relation_cmd_line + ["--file", settings_file.name])
392 os.remove(settings_file.name)
393 else:
394 for key, value in settings.items():
395 if value is None:
396 relation_cmd_line.append('{}='.format(key))
397 else:
398 relation_cmd_line.append('{}={}'.format(key, value))
399 subprocess.check_call(relation_cmd_line)
351 # Flush cache of any relation-gets for local unit400 # Flush cache of any relation-gets for local unit
352 flush(local_unit())401 flush(local_unit())
353402
354403
404def relation_clear(r_id=None):
405 ''' Clears any relation data already set on relation r_id '''
406 settings = relation_get(rid=r_id,
407 unit=local_unit())
408 for setting in settings:
409 if setting not in ['public-address', 'private-address']:
410 settings[setting] = None
411 relation_set(relation_id=r_id,
412 **settings)
413
414
355@cached415@cached
356def relation_ids(reltype=None):416def relation_ids(reltype=None):
357 """A list of relation_ids"""417 """A list of relation_ids"""
@@ -431,6 +491,76 @@
431491
432492
433@cached493@cached
494def peer_relation_id():
495 '''Get the peers relation id if a peers relation has been joined, else None.'''
496 md = metadata()
497 section = md.get('peers')
498 if section:
499 for key in section:
500 relids = relation_ids(key)
501 if relids:
502 return relids[0]
503 return None
504
505
506@cached
507def relation_to_interface(relation_name):
508 """
509 Given the name of a relation, return the interface that relation uses.
510
511 :returns: The interface name, or ``None``.
512 """
513 return relation_to_role_and_interface(relation_name)[1]
514
515
516@cached
517def relation_to_role_and_interface(relation_name):
518 """
519 Given the name of a relation, return the role and the name of the interface
520 that relation uses (where role is one of ``provides``, ``requires``, or ``peers``).
521
522 :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
523 """
524 _metadata = metadata()
525 for role in ('provides', 'requires', 'peers'):
526 interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
527 if interface:
528 return role, interface
529 return None, None
530
531
532@cached
533def role_and_interface_to_relations(role, interface_name):
534 """
535 Given a role and interface name, return a list of relation names for the
536 current charm that use that interface under that role (where role is one
537 of ``provides``, ``requires``, or ``peers``).
538
539 :returns: A list of relation names.
540 """
541 _metadata = metadata()
542 results = []
543 for relation_name, relation in _metadata.get(role, {}).items():
544 if relation['interface'] == interface_name:
545 results.append(relation_name)
546 return results
547
548
549@cached
550def interface_to_relations(interface_name):
551 """
552 Given an interface, return a list of relation names for the current
553 charm that use that interface.
554
555 :returns: A list of relation names.
556 """
557 results = []
558 for role in ('provides', 'requires', 'peers'):
559 results.extend(role_and_interface_to_relations(role, interface_name))
560 return results
561
562
563@cached
434def charm_name():564def charm_name():
435 """Get the name of the current charm as is specified on metadata.yaml"""565 """Get the name of the current charm as is specified on metadata.yaml"""
436 return metadata().get('name')566 return metadata().get('name')
@@ -486,6 +616,20 @@
486 subprocess.check_call(_args)616 subprocess.check_call(_args)
487617
488618
619def open_ports(start, end, protocol="TCP"):
620 """Opens a range of service network ports"""
621 _args = ['open-port']
622 _args.append('{}-{}/{}'.format(start, end, protocol))
623 subprocess.check_call(_args)
624
625
626def close_ports(start, end, protocol="TCP"):
627 """Close a range of service network ports"""
628 _args = ['close-port']
629 _args.append('{}-{}/{}'.format(start, end, protocol))
630 subprocess.check_call(_args)
631
632
489@cached633@cached
490def unit_get(attribute):634def unit_get(attribute):
491 """Get the unit ID for the remote unit"""635 """Get the unit ID for the remote unit"""
@@ -496,11 +640,48 @@
496 return None640 return None
497641
498642
643def unit_public_ip():
644 """Get this unit's public IP address"""
645 return unit_get('public-address')
646
647
499def unit_private_ip():648def unit_private_ip():
500 """Get this unit's private IP address"""649 """Get this unit's private IP address"""
501 return unit_get('private-address')650 return unit_get('private-address')
502651
503652
653@cached
654def storage_get(attribute=None, storage_id=None):
655 """Get storage attributes"""
656 _args = ['storage-get', '--format=json']
657 if storage_id:
658 _args.extend(('-s', storage_id))
659 if attribute:
660 _args.append(attribute)
661 try:
662 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
663 except ValueError:
664 return None
665
666
667@cached
668def storage_list(storage_name=None):
669 """List the storage IDs for the unit"""
670 _args = ['storage-list', '--format=json']
671 if storage_name:
672 _args.append(storage_name)
673 try:
674 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
675 except ValueError:
676 return None
677 except OSError as e:
678 import errno
679 if e.errno == errno.ENOENT:
680 # storage-list does not exist
681 return []
682 raise
683
684
504class UnregisteredHookError(Exception):685class UnregisteredHookError(Exception):
505 """Raised when an undefined hook is called"""686 """Raised when an undefined hook is called"""
506 pass687 pass
@@ -528,10 +709,14 @@
528 hooks.execute(sys.argv)709 hooks.execute(sys.argv)
529 """710 """
530711
531 def __init__(self, config_save=True):712 def __init__(self, config_save=None):
532 super(Hooks, self).__init__()713 super(Hooks, self).__init__()
533 self._hooks = {}714 self._hooks = {}
534 self._config_save = config_save715
716 # For unknown reasons, we allow the Hooks constructor to override
717 # config().implicit_save.
718 if config_save is not None:
719 config().implicit_save = config_save
535720
536 def register(self, name, function):721 def register(self, name, function):
537 """Register a hook"""722 """Register a hook"""
@@ -539,13 +724,16 @@
539724
540 def execute(self, args):725 def execute(self, args):
541 """Execute a registered hook based on args[0]"""726 """Execute a registered hook based on args[0]"""
727 _run_atstart()
542 hook_name = os.path.basename(args[0])728 hook_name = os.path.basename(args[0])
543 if hook_name in self._hooks:729 if hook_name in self._hooks:
544 self._hooks[hook_name]()730 try:
545 if self._config_save:731 self._hooks[hook_name]()
546 cfg = config()732 except SystemExit as x:
547 if cfg.implicit_save:733 if x.code is None or x.code == 0:
548 cfg.save()734 _run_atexit()
735 raise
736 _run_atexit()
549 else:737 else:
550 raise UnregisteredHookError(hook_name)738 raise UnregisteredHookError(hook_name)
551739
@@ -592,3 +780,289 @@
592780
593 The results set by action_set are preserved."""781 The results set by action_set are preserved."""
594 subprocess.check_call(['action-fail', message])782 subprocess.check_call(['action-fail', message])
783
784
785def action_name():
786 """Get the name of the currently executing action."""
787 return os.environ.get('JUJU_ACTION_NAME')
788
789
790def action_uuid():
791 """Get the UUID of the currently executing action."""
792 return os.environ.get('JUJU_ACTION_UUID')
793
794
795def action_tag():
796 """Get the tag for the currently executing action."""
797 return os.environ.get('JUJU_ACTION_TAG')
798
799
800def status_set(workload_state, message):
801 """Set the workload state with a message
802
803 Use status-set to set the workload state with a message which is visible
804 to the user via juju status. If the status-set command is not found then
805 assume this is juju < 1.23 and juju-log the message unstead.
806
807 workload_state -- valid juju workload state.
808 message -- status update message
809 """
810 valid_states = ['maintenance', 'blocked', 'waiting', 'active']
811 if workload_state not in valid_states:
812 raise ValueError(
813 '{!r} is not a valid workload state'.format(workload_state)
814 )
815 cmd = ['status-set', workload_state, message]
816 try:
817 ret = subprocess.call(cmd)
818 if ret == 0:
819 return
820 except OSError as e:
821 if e.errno != errno.ENOENT:
822 raise
823 log_message = 'status-set failed: {} {}'.format(workload_state,
824 message)
825 log(log_message, level='INFO')
826
827
828def status_get():
829 """Retrieve the previously set juju workload state and message
830
831 If the status-get command is not found then assume this is juju < 1.23 and
832 return 'unknown', ""
833
834 """
835 cmd = ['status-get', "--format=json", "--include-data"]
836 try:
837 raw_status = subprocess.check_output(cmd)
838 except OSError as e:
839 if e.errno == errno.ENOENT:
840 return ('unknown', "")
841 else:
842 raise
843 else:
844 status = json.loads(raw_status.decode("UTF-8"))
845 return (status["status"], status["message"])
846
847
848def translate_exc(from_exc, to_exc):
849 def inner_translate_exc1(f):
850 @wraps(f)
851 def inner_translate_exc2(*args, **kwargs):
852 try:
853 return f(*args, **kwargs)
854 except from_exc:
855 raise to_exc
856
857 return inner_translate_exc2
858
859 return inner_translate_exc1
860
861
862def application_version_set(version):
863 """Charm authors may trigger this command from any hook to output what
864 version of the application is running. This could be a package version,
865 for instance postgres version 9.5. It could also be a build number or
866 version control revision identifier, for instance git sha 6fb7ba68. """
867
868 cmd = ['application-version-set']
869 cmd.append(version)
870 try:
871 subprocess.check_call(cmd)
872 except OSError:
873 log("Application Version: {}".format(version))
874
875
876@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
877def is_leader():
878 """Does the current unit hold the juju leadership
879
880 Uses juju to determine whether the current unit is the leader of its peers
881 """
882 cmd = ['is-leader', '--format=json']
883 return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
884
885
886@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
887def leader_get(attribute=None):
888 """Juju leader get value(s)"""
889 cmd = ['leader-get', '--format=json'] + [attribute or '-']
890 return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
891
892
893@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
894def leader_set(settings=None, **kwargs):
895 """Juju leader set value(s)"""
896 # Don't log secrets.
897 # log("Juju leader-set '%s'" % (settings), level=DEBUG)
898 cmd = ['leader-set']
899 settings = settings or {}
900 settings.update(kwargs)
901 for k, v in settings.items():
902 if v is None:
903 cmd.append('{}='.format(k))
904 else:
905 cmd.append('{}={}'.format(k, v))
906 subprocess.check_call(cmd)
907
908
909@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
910def payload_register(ptype, klass, pid):
911 """ is used while a hook is running to let Juju know that a
912 payload has been started."""
913 cmd = ['payload-register']
914 for x in [ptype, klass, pid]:
915 cmd.append(x)
916 subprocess.check_call(cmd)
917
918
919@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
920def payload_unregister(klass, pid):
921 """ is used while a hook is running to let Juju know
922 that a payload has been manually stopped. The <class> and <id> provided
923 must match a payload that has been previously registered with juju using
924 payload-register."""
925 cmd = ['payload-unregister']
926 for x in [klass, pid]:
927 cmd.append(x)
928 subprocess.check_call(cmd)
929
930
931@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
932def payload_status_set(klass, pid, status):
933 """is used to update the current status of a registered payload.
934 The <class> and <id> provided must match a payload that has been previously
935 registered with juju using payload-register. The <status> must be one of the
936 follow: starting, started, stopping, stopped"""
937 cmd = ['payload-status-set']
938 for x in [klass, pid, status]:
939 cmd.append(x)
940 subprocess.check_call(cmd)
941
942
943@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
944def resource_get(name):
945 """used to fetch the resource path of the given name.
946
947 <name> must match a name of defined resource in metadata.yaml
948
949 returns either a path or False if resource not available
950 """
951 if not name:
952 return False
953
954 cmd = ['resource-get', name]
955 try:
956 return subprocess.check_output(cmd).decode('UTF-8')
957 except subprocess.CalledProcessError:
958 return False
959
960
961@cached
962def juju_version():
963 """Full version string (eg. '1.23.3.1-trusty-amd64')"""
964 # Per https://bugs.launchpad.net/juju-core/+bug/1455368/comments/1
965 jujud = glob.glob('/var/lib/juju/tools/machine-*/jujud')[0]
966 return subprocess.check_output([jujud, 'version'],
967 universal_newlines=True).strip()
968
969
970@cached
971def has_juju_version(minimum_version):
972 """Return True if the Juju version is at least the provided version"""
973 return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
974
975
976_atexit = []
977_atstart = []
978
979
980def atstart(callback, *args, **kwargs):
981 '''Schedule a callback to run before the main hook.
982
983 Callbacks are run in the order they were added.
984
985 This is useful for modules and classes to perform initialization
986 and inject behavior. In particular:
987
988 - Run common code before all of your hooks, such as logging
989 the hook name or interesting relation data.
990 - Defer object or module initialization that requires a hook
991 context until we know there actually is a hook context,
992 making testing easier.
993 - Rather than requiring charm authors to include boilerplate to
994 invoke your helper's behavior, have it run automatically if
995 your object is instantiated or module imported.
996
997 This is not at all useful after your hook framework as been launched.
998 '''
999 global _atstart
1000 _atstart.append((callback, args, kwargs))
1001
1002
1003def atexit(callback, *args, **kwargs):
1004 '''Schedule a callback to run on successful hook completion.
1005
1006 Callbacks are run in the reverse order that they were added.'''
1007 _atexit.append((callback, args, kwargs))
1008
1009
1010def _run_atstart():
1011 '''Hook frameworks must invoke this before running the main hook body.'''
1012 global _atstart
1013 for callback, args, kwargs in _atstart:
1014 callback(*args, **kwargs)
1015 del _atstart[:]
1016
1017
1018def _run_atexit():
1019 '''Hook frameworks must invoke this after the main hook body has
1020 successfully completed. Do not invoke it if the hook fails.'''
1021 global _atexit
1022 for callback, args, kwargs in reversed(_atexit):
1023 callback(*args, **kwargs)
1024 del _atexit[:]
1025
1026
1027@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1028def network_get_primary_address(binding):
1029 '''
1030 Retrieve the primary network address for a named binding
1031
1032 :param binding: string. The name of a relation of extra-binding
1033 :return: string. The primary IP address for the named binding
1034 :raise: NotImplementedError if run on Juju < 2.0
1035 '''
1036 cmd = ['network-get', '--primary-address', binding]
1037 return subprocess.check_output(cmd).decode('UTF-8').strip()
1038
1039
1040def add_metric(*args, **kwargs):
1041 """Add metric values. Values may be expressed with keyword arguments. For
1042 metric names containing dashes, these may be expressed as one or more
1043 'key=value' positional arguments. May only be called from the collect-metrics
1044 hook."""
1045 _args = ['add-metric']
1046 _kvpairs = []
1047 _kvpairs.extend(args)
1048 _kvpairs.extend(['{}={}'.format(k, v) for k, v in kwargs.items()])
1049 _args.extend(sorted(_kvpairs))
1050 try:
1051 subprocess.check_call(_args)
1052 return
1053 except EnvironmentError as e:
1054 if e.errno != errno.ENOENT:
1055 raise
1056 log_message = 'add-metric failed: {}'.format(' '.join(_kvpairs))
1057 log(log_message, level='INFO')
1058
1059
1060def meter_status():
1061 """Get the meter status, if running in the meter-status-changed hook."""
1062 return os.environ.get('JUJU_METER_STATUS')
1063
1064
1065def meter_info():
1066 """Get the meter status information, if running in the meter-status-changed
1067 hook."""
1068 return os.environ.get('JUJU_METER_INFO')
5951069
=== modified file 'hooks/charmhelpers/core/host.py'
--- hooks/charmhelpers/core/host.py 2015-04-09 06:46:35 +0000
+++ hooks/charmhelpers/core/host.py 2017-07-25 09:17:37 +0000
@@ -1,18 +1,16 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17"""Tools for working with the host system"""15"""Tools for working with the host system"""
18# Copyright 2012 Canonical Ltd.16# Copyright 2012 Canonical Ltd.
@@ -24,85 +22,330 @@
24import os22import os
25import re23import re
26import pwd24import pwd
25import glob
27import grp26import grp
28import random27import random
29import string28import string
30import subprocess29import subprocess
31import hashlib30import hashlib
31import functools
32import itertools
33import six
34
32from contextlib import contextmanager35from contextlib import contextmanager
33from collections import OrderedDict36from collections import OrderedDict
34
35import six
36
37from .hookenv import log37from .hookenv import log
38from .fstab import Fstab38from .fstab import Fstab
3939from charmhelpers.osplatform import get_platform
4040
41def service_start(service_name):41__platform__ = get_platform()
42 """Start a system service"""42if __platform__ == "ubuntu":
43 return service('start', service_name)43 from charmhelpers.core.host_factory.ubuntu import (
4444 service_available,
4545 add_new_group,
46def service_stop(service_name):46 lsb_release,
47 """Stop a system service"""47 cmp_pkgrevno,
48 return service('stop', service_name)48 CompareHostReleases,
4949 ) # flake8: noqa -- ignore F401 for this import
5050elif __platform__ == "centos":
51def service_restart(service_name):51 from charmhelpers.core.host_factory.centos import (
52 """Restart a system service"""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)
85
86
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)
111
112
113def service_restart(service_name, **kwargs):
114 """Restart a system service.
115
116 The specified service name is managed via the system level init system.
117 Some init systems (e.g. upstart) require that additional arguments be
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).
121
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:
127
128 service_restart('ceph-osd', id=4)
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 """
53 return service('restart', service_name)137 return service('restart', service_name)
54138
55139
56def service_reload(service_name, restart_on_failure=False):140def service_reload(service_name, restart_on_failure=False, **kwargs):
57 """Reload a system service, optionally falling back to restart if141 """Reload a system service, optionally falling back to restart if
58 reload fails"""142 reload fails.
59 service_result = service('reload', service_name)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)
60 if not service_result and restart_on_failure:168 if not service_result and restart_on_failure:
61 service_result = service('restart', service_name)169 service_result = service('restart', service_name, **kwargs)
62 return service_result170 return service_result
63171
64172
65def service(action, service_name):173def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d",
66 """Control a system service"""174 **kwargs):
67 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)
68 return subprocess.call(cmd) == 0264 return subprocess.call(cmd) == 0
69265
70266
71def service_running(service):267_UPSTART_CONF = "/etc/init/{}.conf"
72 """Determine whether a system service is running"""268_INIT_D_CONF = "/etc/init.d/{}"
73 try:269
74 output = subprocess.check_output(270
75 ['service', service, 'status'],271def service_running(service_name, **kwargs):
76 stderr=subprocess.STDOUT).decode('UTF-8')272 """Determine whether a system service is running.
77 except subprocess.CalledProcessError:273
78 return False274 :param service_name: the name of the service
79 else:275 :param **kwargs: additional args to pass to the service command. This is
80 if ("start/running" in output or "is running" in output):276 used to pass additional key=value arguments to the
81 return True277 service command line for managing specific instance
82 else:278 units (e.g. service ceph-osd status id=2). The kwargs
83 return False279 are ignored in systemd services.
84280 """
85281 if init_is_systemd():
86def service_available(service_name):282 return service('is-active', service_name)
87 """Determine whether a system service is available"""283 else:
88 try:284 if os.path.exists(_UPSTART_CONF.format(service_name)):
89 subprocess.check_output(285 try:
90 ['service', service_name, 'status'],286 cmd = ['status', service_name]
91 stderr=subprocess.STDOUT).decode('UTF-8')287 for key, value in six.iteritems(kwargs):
92 except subprocess.CalledProcessError as e:288 parameter = '%s=%s' % (key, value)
93 return 'unrecognized service' not in e.output289 cmd.append(parameter)
94 else:290 output = subprocess.check_output(cmd,
95 return True291 stderr=subprocess.STDOUT).decode('UTF-8')
96292 except subprocess.CalledProcessError:
97293 return False
98def adduser(username, password=None, shell='/bin/bash', system_user=False):294 else:
99 """Add a user to the system"""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
307
308SYSTEMD_SYSTEM = '/run/systemd/system'
309
310
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 """
100 try:336 try:
101 user_info = pwd.getpwnam(username)337 user_info = pwd.getpwnam(username)
102 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))
103 except KeyError:342 except KeyError:
104 log('creating user {0}'.format(username))343 log('creating user {0}'.format(username))
105 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)])
106 if system_user or password is None:349 if system_user or password is None:
107 cmd.append('--system')350 cmd.append('--system')
108 else:351 else:
@@ -111,52 +354,104 @@
111 '--shell', shell,354 '--shell', shell,
112 '--password', password,355 '--password', password,
113 ])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)])
114 cmd.append(username)367 cmd.append(username)
115 subprocess.check_call(cmd)368 subprocess.check_call(cmd)
116 user_info = pwd.getpwnam(username)369 user_info = pwd.getpwnam(username)
117 return user_info370 return user_info
118371
119372
120def add_group(group_name, system_group=False):373def user_exists(username):
121 """Add a group to the system"""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 """
122 try:424 try:
123 group_info = grp.getgrnam(group_name)425 group_info = grp.getgrnam(group_name)
124 log('group {0} already exists!'.format(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))
125 except KeyError:430 except KeyError:
126 log('creating group {0}'.format(group_name))431 log('creating group {0}'.format(group_name))
127 cmd = ['addgroup']432 add_new_group(group_name, system_group, gid)
128 if system_group:
129 cmd.append('--system')
130 else:
131 cmd.extend([
132 '--group',
133 ])
134 cmd.append(group_name)
135 subprocess.check_call(cmd)
136 group_info = grp.getgrnam(group_name)433 group_info = grp.getgrnam(group_name)
137 return group_info434 return group_info
138435
139436
140def add_user_to_group(username, group):437def add_user_to_group(username, group):
141 """Add a user to a group"""438 """Add a user to a group"""
142 cmd = [439 cmd = ['gpasswd', '-a', username, group]
143 'gpasswd', '-a',
144 username,
145 group
146 ]
147 log("Adding user {} to group {}".format(username, group))440 log("Adding user {} to group {}".format(username, group))
148 subprocess.check_call(cmd)441 subprocess.check_call(cmd)
149442
150443
151def rsync(from_path, to_path, flags='-r', options=None):444def rsync(from_path, to_path, flags='-r', options=None, timeout=None):
152 """Replicate the contents of a path"""445 """Replicate the contents of a path"""
153 options = options or ['--delete', '--executability']446 options = options or ['--delete', '--executability']
154 cmd = ['/usr/bin/rsync', flags]447 cmd = ['/usr/bin/rsync', flags]
448 if timeout:
449 cmd = ['timeout', str(timeout)] + cmd
155 cmd.extend(options)450 cmd.extend(options)
156 cmd.append(from_path)451 cmd.append(from_path)
157 cmd.append(to_path)452 cmd.append(to_path)
158 log(" ".join(cmd))453 log(" ".join(cmd))
159 return subprocess.check_output(cmd).decode('UTF-8').strip()454 return subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode('UTF-8').strip()
160455
161456
162def symlink(source, destination):457def symlink(source, destination):
@@ -202,14 +497,12 @@
202497
203498
204def fstab_remove(mp):499def fstab_remove(mp):
205 """Remove the given mountpoint entry from /etc/fstab500 """Remove the given mountpoint entry from /etc/fstab"""
206 """
207 return Fstab.remove_by_mountpoint(mp)501 return Fstab.remove_by_mountpoint(mp)
208502
209503
210def fstab_add(dev, mp, fs, options=None):504def fstab_add(dev, mp, fs, options=None):
211 """Adds the given device entry to the /etc/fstab file505 """Adds the given device entry to the /etc/fstab file"""
212 """
213 return Fstab.add(dev, mp, fs, options=options)506 return Fstab.add(dev, mp, fs, options=options)
214507
215508
@@ -253,9 +546,19 @@
253 return system_mounts546 return system_mounts
254547
255548
549def fstab_mount(mountpoint):
550 """Mount filesystem using fstab"""
551 cmd_args = ['mount', mountpoint]
552 try:
553 subprocess.check_output(cmd_args)
554 except subprocess.CalledProcessError as e:
555 log('Error unmounting {}\n{}'.format(mountpoint, e.output))
556 return False
557 return True
558
559
256def file_hash(path, hash_type='md5'):560def file_hash(path, hash_type='md5'):
257 """561 """Generate a hash checksum of the contents of 'path' or None if not found.
258 Generate a hash checksum of the contents of 'path' or None if not found.
259562
260 :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,563 :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
261 such as md5, sha1, sha256, sha512, etc.564 such as md5, sha1, sha256, sha512, etc.
@@ -269,9 +572,22 @@
269 return None572 return None
270573
271574
575def path_hash(path):
576 """Generate a hash checksum of all files matching 'path'. Standard
577 wildcards like '*' and '?' are supported, see documentation for the 'glob'
578 module for more information.
579
580 :return: dict: A { filename: hash } dictionary for all matched files.
581 Empty if none found.
582 """
583 return {
584 filename: file_hash(filename)
585 for filename in glob.iglob(path)
586 }
587
588
272def check_hash(path, checksum, hash_type='md5'):589def check_hash(path, checksum, hash_type='md5'):
273 """590 """Validate a file using a cryptographic checksum.
274 Validate a file using a cryptographic checksum.
275591
276 :param str checksum: Value of the checksum used to validate the file.592 :param str checksum: Value of the checksum used to validate the file.
277 :param str hash_type: Hash algorithm used to generate `checksum`.593 :param str hash_type: Hash algorithm used to generate `checksum`.
@@ -286,54 +602,78 @@
286602
287603
288class ChecksumError(ValueError):604class ChecksumError(ValueError):
605 """A class derived from Value error to indicate the checksum failed."""
289 pass606 pass
290607
291608
292def restart_on_change(restart_map, stopstart=False):609def restart_on_change(restart_map, stopstart=False, restart_functions=None):
293 """Restart services based on configuration files changing610 """Restart services based on configuration files changing
294611
295 This function is used a decorator, for example::612 This function is used a decorator, for example::
296613
297 @restart_on_change({614 @restart_on_change({
298 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]615 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
616 '/etc/apache/sites-enabled/*': [ 'apache2' ]
299 })617 })
300 def ceph_client_changed():618 def config_changed():
301 pass # your code here619 pass # your code here
302620
303 In this example, the cinder-api and cinder-volume services621 In this example, the cinder-api and cinder-volume services
304 would be restarted if /etc/ceph/ceph.conf is changed by the622 would be restarted if /etc/ceph/ceph.conf is changed by the
305 ceph_client_changed function.623 ceph_client_changed function. The apache2 service would be
624 restarted if any file matching the pattern got changed, created
625 or removed. Standard wildcards are supported, see documentation
626 for the 'glob' module for more information.
627
628 @param restart_map: {path_file_name: [service_name, ...]
629 @param stopstart: DEFAULT false; whether to stop, start OR restart
630 @param restart_functions: nonstandard functions to use to restart services
631 {svc: func, ...}
632 @returns result from decorated function
306 """633 """
307 def wrap(f):634 def wrap(f):
635 @functools.wraps(f)
308 def wrapped_f(*args, **kwargs):636 def wrapped_f(*args, **kwargs):
309 checksums = {}637 return restart_on_change_helper(
310 for path in restart_map:638 (lambda: f(*args, **kwargs)), restart_map, stopstart,
311 checksums[path] = file_hash(path)639 restart_functions)
312 f(*args, **kwargs)
313 restarts = []
314 for path in restart_map:
315 if checksums[path] != file_hash(path):
316 restarts += restart_map[path]
317 services_list = list(OrderedDict.fromkeys(restarts))
318 if not stopstart:
319 for service_name in services_list:
320 service('restart', service_name)
321 else:
322 for action in ['stop', 'start']:
323 for service_name in services_list:
324 service(action, service_name)
325 return wrapped_f640 return wrapped_f
326 return wrap641 return wrap
327642
328643
329def lsb_release():644def restart_on_change_helper(lambda_f, restart_map, stopstart=False,
330 """Return /etc/lsb-release in a dict"""645 restart_functions=None):
331 d = {}646 """Helper function to perform the restart_on_change function.
332 with open('/etc/lsb-release', 'r') as lsb:647
333 for l in lsb:648 This is provided for decorators to restart services if files described
334 k, v = l.split('=')649 in the restart_map have changed after an invocation of lambda_f().
335 d[k.strip()] = v.strip()650
336 return d651 @param lambda_f: function to call.
652 @param restart_map: {file: [service, ...]}
653 @param stopstart: whether to stop, start or restart a service
654 @param restart_functions: nonstandard functions to use to restart services
655 {svc: func, ...}
656 @returns result of lambda_f()
657 """
658 if restart_functions is None:
659 restart_functions = {}
660 checksums = {path: path_hash(path) for path in restart_map}
661 r = lambda_f()
662 # create a list of lists of the services to restart
663 restarts = [restart_map[path]
664 for path in restart_map
665 if path_hash(path) != checksums[path]]
666 # create a flat list of ordered services without duplicates from lists
667 services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts)))
668 if services_list:
669 actions = ('stop', 'start') if stopstart else ('restart',)
670 for service_name in services_list:
671 if service_name in restart_functions:
672 restart_functions[service_name](service_name)
673 else:
674 for action in actions:
675 service(action, service_name)
676 return r
337677
338678
339def pwgen(length=None):679def pwgen(length=None):
@@ -352,36 +692,92 @@
352 return(''.join(random_chars))692 return(''.join(random_chars))
353693
354694
355def list_nics(nic_type):695def is_phy_iface(interface):
356 '''Return a list of nics of given type(s)'''696 """Returns True if interface is not virtual, otherwise False."""
697 if interface:
698 sys_net = '/sys/class/net'
699 if os.path.isdir(sys_net):
700 for iface in glob.glob(os.path.join(sys_net, '*')):
701 if '/virtual/' in os.path.realpath(iface):
702 continue
703
704 if interface == os.path.basename(iface):
705 return True
706
707 return False
708
709
710def get_bond_master(interface):
711 """Returns bond master if interface is bond slave otherwise None.
712
713 NOTE: the provided interface is expected to be physical
714 """
715 if interface:
716 iface_path = '/sys/class/net/%s' % (interface)
717 if os.path.exists(iface_path):
718 if '/virtual/' in os.path.realpath(iface_path):
719 return None
720
721 master = os.path.join(iface_path, 'master')
722 if os.path.exists(master):
723 master = os.path.realpath(master)
724 # make sure it is a bond master
725 if os.path.exists(os.path.join(master, 'bonding')):
726 return os.path.basename(master)
727
728 return None
729
730
731def list_nics(nic_type=None):
732 """Return a list of nics of given type(s)"""
357 if isinstance(nic_type, six.string_types):733 if isinstance(nic_type, six.string_types):
358 int_types = [nic_type]734 int_types = [nic_type]
359 else:735 else:
360 int_types = nic_type736 int_types = nic_type
737
361 interfaces = []738 interfaces = []
362 for int_type in int_types:739 if nic_type:
363 cmd = ['ip', 'addr', 'show', 'label', int_type + '*']740 for int_type in int_types:
741 cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
742 ip_output = subprocess.check_output(cmd).decode('UTF-8')
743 ip_output = ip_output.split('\n')
744 ip_output = (line for line in ip_output if line)
745 for line in ip_output:
746 if line.split()[1].startswith(int_type):
747 matched = re.search('.*: (' + int_type +
748 r'[0-9]+\.[0-9]+)@.*', line)
749 if matched:
750 iface = matched.groups()[0]
751 else:
752 iface = line.split()[1].replace(":", "")
753
754 if iface not in interfaces:
755 interfaces.append(iface)
756 else:
757 cmd = ['ip', 'a']
364 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')758 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
365 ip_output = (line for line in ip_output if line)759 ip_output = (line.strip() for line in ip_output if line)
760
761 key = re.compile('^[0-9]+:\s+(.+):')
366 for line in ip_output:762 for line in ip_output:
367 if line.split()[1].startswith(int_type):763 matched = re.search(key, line)
368 matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line)764 if matched:
369 if matched:765 iface = matched.group(1)
370 interface = matched.groups()[0]766 iface = iface.partition("@")[0]
371 else:767 if iface not in interfaces:
372 interface = line.split()[1].replace(":", "")768 interfaces.append(iface)
373 interfaces.append(interface)
374769
375 return interfaces770 return interfaces
376771
377772
378def set_nic_mtu(nic, mtu):773def set_nic_mtu(nic, mtu):
379 '''Set MTU on a network interface'''774 """Set the Maximum Transmission Unit (MTU) on a network interface."""
380 cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]775 cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
381 subprocess.check_call(cmd)776 subprocess.check_call(cmd)
382777
383778
384def get_nic_mtu(nic):779def get_nic_mtu(nic):
780 """Return the Maximum Transmission Unit (MTU) for a network interface."""
385 cmd = ['ip', 'addr', 'show', nic]781 cmd = ['ip', 'addr', 'show', nic]
386 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')782 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
387 mtu = ""783 mtu = ""
@@ -393,6 +789,7 @@
393789
394790
395def get_nic_hwaddr(nic):791def get_nic_hwaddr(nic):
792 """Return the Media Access Control (MAC) for a network interface."""
396 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]793 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
397 ip_output = subprocess.check_output(cmd).decode('UTF-8')794 ip_output = subprocess.check_output(cmd).decode('UTF-8')
398 hwaddr = ""795 hwaddr = ""
@@ -402,35 +799,31 @@
402 return hwaddr799 return hwaddr
403800
404801
405def cmp_pkgrevno(package, revno, pkgcache=None):
406 '''Compare supplied revno with the revno of the installed package
407
408 * 1 => Installed revno is greater than supplied arg
409 * 0 => Installed revno is the same as supplied arg
410 * -1 => Installed revno is less than supplied arg
411
412 This function imports apt_cache function from charmhelpers.fetch if
413 the pkgcache argument is None. Be sure to add charmhelpers.fetch if
414 you call this function, or pass an apt_pkg.Cache() instance.
415 '''
416 import apt_pkg
417 if not pkgcache:
418 from charmhelpers.fetch import apt_cache
419 pkgcache = apt_cache()
420 pkg = pkgcache[package]
421 return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
422
423
424@contextmanager802@contextmanager
425def chdir(d):803def chdir(directory):
804 """Change the current working directory to a different directory for a code
805 block and return the previous directory after the block exits. Useful to
806 run commands from a specificed directory.
807
808 :param str directory: The directory path to change to for this context.
809 """
426 cur = os.getcwd()810 cur = os.getcwd()
427 try:811 try:
428 yield os.chdir(d)812 yield os.chdir(directory)
429 finally:813 finally:
430 os.chdir(cur)814 os.chdir(cur)
431815
432816
433def chownr(path, owner, group, follow_links=True):817def chownr(path, owner, group, follow_links=True, chowntopdir=False):
818 """Recursively change user and group ownership of files and directories
819 in given path. Doesn't chown path itself by default, only its children.
820
821 :param str path: The string path to start changing ownership.
822 :param str owner: The owner string to use when looking up the uid.
823 :param str group: The group string to use when looking up the gid.
824 :param bool follow_links: Also follow and chown links if True
825 :param bool chowntopdir: Also chown path itself if True
826 """
434 uid = pwd.getpwnam(owner).pw_uid827 uid = pwd.getpwnam(owner).pw_uid
435 gid = grp.getgrnam(group).gr_gid828 gid = grp.getgrnam(group).gr_gid
436 if follow_links:829 if follow_links:
@@ -438,7 +831,11 @@
438 else:831 else:
439 chown = os.lchown832 chown = os.lchown
440833
441 for root, dirs, files in os.walk(path):834 if chowntopdir:
835 broken_symlink = os.path.lexists(path) and not os.path.exists(path)
836 if not broken_symlink:
837 chown(path, uid, gid)
838 for root, dirs, files in os.walk(path, followlinks=follow_links):
442 for name in dirs + files:839 for name in dirs + files:
443 full = os.path.join(root, name)840 full = os.path.join(root, name)
444 broken_symlink = os.path.lexists(full) and not os.path.exists(full)841 broken_symlink = os.path.lexists(full) and not os.path.exists(full)
@@ -447,4 +844,81 @@
447844
448845
449def lchownr(path, owner, group):846def lchownr(path, owner, group):
847 """Recursively change user and group ownership of files and directories
848 in a given path, not following symbolic links. See the documentation for
849 'os.lchown' for more information.
850
851 :param str path: The string path to start changing ownership.
852 :param str owner: The owner string to use when looking up the uid.
853 :param str group: The group string to use when looking up the gid.
854 """
450 chownr(path, owner, group, follow_links=False)855 chownr(path, owner, group, follow_links=False)
856
857
858def owner(path):
859 """Returns a tuple containing the username & groupname owning the path.
860
861 :param str path: the string path to retrieve the ownership
862 :return tuple(str, str): A (username, groupname) tuple containing the
863 name of the user and group owning the path.
864 :raises OSError: if the specified path does not exist
865 """
866 stat = os.stat(path)
867 username = pwd.getpwuid(stat.st_uid)[0]
868 groupname = grp.getgrgid(stat.st_gid)[0]
869 return username, groupname
870
871
872def get_total_ram():
873 """The total amount of system RAM in bytes.
874
875 This is what is reported by the OS, and may be overcommitted when
876 there are multiple containers hosted on the same machine.
877 """
878 with open('/proc/meminfo', 'r') as f:
879 for line in f.readlines():
880 if line:
881 key, value, unit = line.split()
882 if key == 'MemTotal:':
883 assert unit == 'kB', 'Unknown unit'
884 return int(value) * 1024 # Classic, not KiB.
885 raise NotImplementedError()
886
887
888UPSTART_CONTAINER_TYPE = '/run/container_type'
889
890
891def is_container():
892 """Determine whether unit is running in a container
893
894 @return: boolean indicating if unit is in a container
895 """
896 if init_is_systemd():
897 # Detect using systemd-detect-virt
898 return subprocess.call(['systemd-detect-virt',
899 '--container']) == 0
900 else:
901 # Detect using upstart container file marker
902 return os.path.exists(UPSTART_CONTAINER_TYPE)
903
904
905def add_to_updatedb_prunepath(path, updatedb_path=UPDATEDB_PATH):
906 with open(updatedb_path, 'r+') as f_id:
907 updatedb_text = f_id.read()
908 output = updatedb(updatedb_text, path)
909 f_id.seek(0)
910 f_id.write(output)
911 f_id.truncate()
912
913
914def updatedb(updatedb_text, new_path):
915 lines = [line for line in updatedb_text.split("\n")]
916 for i, line in enumerate(lines):
917 if line.startswith("PRUNEPATHS="):
918 paths_line = line.split("=")[1].replace('"', '')
919 paths = paths_line.split(" ")
920 if new_path not in paths:
921 paths.append(new_path)
922 lines[i] = 'PRUNEPATHS="{}"'.format(' '.join(paths))
923 output = "\n".join(lines)
924 return output
451925
=== added directory 'hooks/charmhelpers/core/host_factory'
=== added file 'hooks/charmhelpers/core/host_factory/__init__.py'
=== added file 'hooks/charmhelpers/core/host_factory/centos.py'
--- hooks/charmhelpers/core/host_factory/centos.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/host_factory/centos.py 2017-07-25 09:17:37 +0000
@@ -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
073
=== added file 'hooks/charmhelpers/core/host_factory/ubuntu.py'
--- hooks/charmhelpers/core/host_factory/ubuntu.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/host_factory/ubuntu.py 2017-07-25 09:17:37 +0000
@@ -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)
090
=== added file 'hooks/charmhelpers/core/hugepage.py'
--- hooks/charmhelpers/core/hugepage.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/hugepage.py 2017-07-25 09:17:37 +0000
@@ -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)
070
=== added file 'hooks/charmhelpers/core/kernel.py'
--- hooks/charmhelpers/core/kernel.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/kernel.py 2017-07-25 09:17:37 +0000
@@ -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
073
=== added directory 'hooks/charmhelpers/core/kernel_factory'
=== added file 'hooks/charmhelpers/core/kernel_factory/__init__.py'
=== added file 'hooks/charmhelpers/core/kernel_factory/centos.py'
--- hooks/charmhelpers/core/kernel_factory/centos.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/kernel_factory/centos.py 2017-07-25 09:17:37 +0000
@@ -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])
018
=== added file 'hooks/charmhelpers/core/kernel_factory/ubuntu.py'
--- hooks/charmhelpers/core/kernel_factory/ubuntu.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/kernel_factory/ubuntu.py 2017-07-25 09:17:37 +0000
@@ -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"])
014
=== modified file 'hooks/charmhelpers/core/services/__init__.py'
--- hooks/charmhelpers/core/services/__init__.py 2015-04-09 06:46:35 +0000
+++ hooks/charmhelpers/core/services/__init__.py 2017-07-25 09:17:37 +0000
@@ -1,18 +1,16 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17from .base import * # NOQA15from .base import * # NOQA
18from .helpers import * # NOQA16from .helpers import * # NOQA
1917
=== modified file 'hooks/charmhelpers/core/services/base.py'
--- hooks/charmhelpers/core/services/base.py 2015-04-09 06:46:35 +0000
+++ hooks/charmhelpers/core/services/base.py 2017-07-25 09:17:37 +0000
@@ -1,23 +1,21 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17import os15import os
18import re
19import json16import json
20from collections import Iterable17from inspect import getargspec
18from collections import Iterable, OrderedDict
2119
22from charmhelpers.core import host20from charmhelpers.core import host
23from charmhelpers.core import hookenv21from charmhelpers.core import hookenv
@@ -119,7 +117,7 @@
119 """117 """
120 self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')118 self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
121 self._ready = None119 self._ready = None
122 self.services = {}120 self.services = OrderedDict()
123 for service in services or []:121 for service in services or []:
124 service_name = service['service']122 service_name = service['service']
125 self.services[service_name] = service123 self.services[service_name] = service
@@ -128,15 +126,18 @@
128 """126 """
129 Handle the current hook by doing The Right Thing with the registered services.127 Handle the current hook by doing The Right Thing with the registered services.
130 """128 """
131 hook_name = hookenv.hook_name()129 hookenv._run_atstart()
132 if hook_name == 'stop':130 try:
133 self.stop_services()131 hook_name = hookenv.hook_name()
134 else:132 if hook_name == 'stop':
135 self.provide_data()133 self.stop_services()
136 self.reconfigure_services()134 else:
137 cfg = hookenv.config()135 self.reconfigure_services()
138 if cfg.implicit_save:136 self.provide_data()
139 cfg.save()137 except SystemExit as x:
138 if x.code is None or x.code == 0:
139 hookenv._run_atexit()
140 hookenv._run_atexit()
140141
141 def provide_data(self):142 def provide_data(self):
142 """143 """
@@ -145,15 +146,36 @@
145 A provider must have a `name` attribute, which indicates which relation146 A provider must have a `name` attribute, which indicates which relation
146 to set data on, and a `provide_data()` method, which returns a dict of147 to set data on, and a `provide_data()` method, which returns a dict of
147 data to set.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.
148 """163 """
149 hook_name = hookenv.hook_name()164 for service_name, service in self.services.items():
150 for service in self.services.values():165 service_ready = self.is_ready(service_name)
151 for provider in service.get('provided_data', []):166 for provider in service.get('provided_data', []):
152 if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):167 for relid in hookenv.relation_ids(provider.name):
153 data = provider.provide_data()168 units = hookenv.related_units(relid)
154 _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data169 if not units:
155 if _ready:170 continue
156 hookenv.relation_set(None, data)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)
157179
158 def reconfigure_services(self, *service_names):180 def reconfigure_services(self, *service_names):
159 """181 """
160182
=== modified file 'hooks/charmhelpers/core/services/helpers.py'
--- hooks/charmhelpers/core/services/helpers.py 2015-04-09 06:46:35 +0000
+++ hooks/charmhelpers/core/services/helpers.py 2017-07-25 09:17:37 +0000
@@ -1,22 +1,22 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17import os15import os
18import yaml16import yaml
17
19from charmhelpers.core import hookenv18from charmhelpers.core import hookenv
19from charmhelpers.core import host
20from charmhelpers.core import templating20from charmhelpers.core import templating
2121
22from charmhelpers.core.services.base import ManagerCallback22from charmhelpers.core.services.base import ManagerCallback
@@ -239,28 +239,51 @@
239 action.239 action.
240240
241 :param str source: The template source file, relative to241 :param str source: The template source file, relative to
242 `$CHARM_DIR/templates`242 `$CHARM_DIR/templates`
243243
244 :param str target: The target to write the rendered template to244 :param str target: The target to write the rendered template to (or None)
245 :param str owner: The owner of the rendered file245 :param str owner: The owner of the rendered file
246 :param str group: The group of the rendered file246 :param str group: The group of the rendered file
247 :param int perms: The permissions of the rendered file247 :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
248 """253 """
249 def __init__(self, source, target,254 def __init__(self, source, target,
250 owner='root', group='root', perms=0o444):255 owner='root', group='root', perms=0o444,
256 on_change_action=None, template_loader=None):
251 self.source = source257 self.source = source
252 self.target = target258 self.target = target
253 self.owner = owner259 self.owner = owner
254 self.group = group260 self.group = group
255 self.perms = perms261 self.perms = perms
262 self.on_change_action = on_change_action
263 self.template_loader = template_loader
256264
257 def __call__(self, manager, service_name, event_name):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)
258 service = manager.get_service(service_name)269 service = manager.get_service(service_name)
259 context = {}270 context = {'ctx': {}}
260 for ctx in service.get('required_data', []):271 for ctx in service.get('required_data', []):
261 context.update(ctx)272 context.update(ctx)
262 templating.render(self.source, self.target, context,273 context['ctx'].update(ctx)
263 self.owner, self.group, self.perms)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
264287
265288
266# Convenience aliases for templates289# Convenience aliases for templates
267290
=== modified file 'hooks/charmhelpers/core/strutils.py'
--- hooks/charmhelpers/core/strutils.py 2015-04-09 06:46:35 +0000
+++ hooks/charmhelpers/core/strutils.py 2017-07-25 09:17:37 +0000
@@ -3,21 +3,20 @@
33
4# Copyright 2014-2015 Canonical Limited.4# Copyright 2014-2015 Canonical Limited.
5#5#
6# This file is part of charm-helpers.6# Licensed under the Apache License, Version 2.0 (the "License");
7#7# you may not use this file except in compliance with the License.
8# charm-helpers is free software: you can redistribute it and/or modify8# You may obtain a copy of the License at
9# it under the terms of the GNU Lesser General Public License version 3 as9#
10# published by the Free Software Foundation.10# http://www.apache.org/licenses/LICENSE-2.0
11#11#
12# charm-helpers is distributed in the hope that it will be useful,12# Unless required by applicable law or agreed to in writing, software
13# but WITHOUT ANY WARRANTY; without even the implied warranty of13# distributed under the License is distributed on an "AS IS" BASIS,
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# GNU Lesser General Public License for more details.15# See the License for the specific language governing permissions and
16#16# limitations under the License.
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1917
20import six18import six
19import re
2120
2221
23def bool_from_string(value):22def bool_from_string(value):
@@ -33,10 +32,92 @@
3332
34 value = value.strip().lower()33 value = value.strip().lower()
3534
36 if value in ['y', 'yes', 'true', 't']:35 if value in ['y', 'yes', 'true', 't', 'on']:
37 return True36 return True
38 elif value in ['n', 'no', 'false', 'f']:37 elif value in ['n', 'no', 'false', 'f', 'off']:
39 return False38 return False
4039
41 msg = "Unable to interpret string value '%s' as boolean" % (value)40 msg = "Unable to interpret string value '%s' as boolean" % (value)
42 raise ValueError(msg)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]
43124
=== modified file 'hooks/charmhelpers/core/sysctl.py'
--- hooks/charmhelpers/core/sysctl.py 2015-04-09 06:46:35 +0000
+++ hooks/charmhelpers/core/sysctl.py 2017-07-25 09:17:37 +0000
@@ -3,19 +3,17 @@
33
4# Copyright 2014-2015 Canonical Limited.4# Copyright 2014-2015 Canonical Limited.
5#5#
6# This file is part of charm-helpers.6# Licensed under the Apache License, Version 2.0 (the "License");
7#7# you may not use this file except in compliance with the License.
8# charm-helpers is free software: you can redistribute it and/or modify8# You may obtain a copy of the License at
9# it under the terms of the GNU Lesser General Public License version 3 as9#
10# published by the Free Software Foundation.10# http://www.apache.org/licenses/LICENSE-2.0
11#11#
12# charm-helpers is distributed in the hope that it will be useful,12# Unless required by applicable law or agreed to in writing, software
13# but WITHOUT ANY WARRANTY; without even the implied warranty of13# distributed under the License is distributed on an "AS IS" BASIS,
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# GNU Lesser General Public License for more details.15# See the License for the specific language governing permissions and
16#16# limitations under the License.
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1917
20import yaml18import yaml
2119
2220
=== modified file 'hooks/charmhelpers/core/templating.py'
--- hooks/charmhelpers/core/templating.py 2015-04-09 06:46:35 +0000
+++ hooks/charmhelpers/core/templating.py 2017-07-25 09:17:37 +0000
@@ -1,33 +1,33 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17import os15import os
16import sys
1817
19from charmhelpers.core import host18from charmhelpers.core import host
20from charmhelpers.core import hookenv19from charmhelpers.core import hookenv
2120
2221
23def render(source, target, context, owner='root', group='root',22def render(source, target, context, owner='root', group='root',
24 perms=0o444, templates_dir=None, encoding='UTF-8'):23 perms=0o444, templates_dir=None, encoding='UTF-8', template_loader=None):
25 """24 """
26 Render a template.25 Render a template.
2726
28 The `source` path, if not absolute, is relative to the `templates_dir`.27 The `source` path, if not absolute, is relative to the `templates_dir`.
2928
30 The `target` path should be absolute.29 The `target` path should be absolute. It can also be `None`, in which
30 case no file will be written.
3131
32 The context should be a dict containing the values to be replaced in the32 The context should be a dict containing the values to be replaced in the
33 template.33 template.
@@ -36,8 +36,12 @@
3636
37 If omitted, `templates_dir` defaults to the `templates` folder in the charm.37 If omitted, `templates_dir` defaults to the `templates` folder in the charm.
3838
39 Note: Using this requires python-jinja2; if it is not installed, calling39 The rendered template will be written to the file as well as being returned
40 this will attempt to use charmhelpers.fetch.apt_install to install it.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.
41 """45 """
42 try:46 try:
43 from jinja2 import FileSystemLoader, Environment, exceptions47 from jinja2 import FileSystemLoader, Environment, exceptions
@@ -49,20 +53,32 @@
49 'charmhelpers.fetch to install it',53 'charmhelpers.fetch to install it',
50 level=hookenv.ERROR)54 level=hookenv.ERROR)
51 raise55 raise
52 apt_install('python-jinja2', fatal=True)56 if sys.version_info.major == 2:
57 apt_install('python-jinja2', fatal=True)
58 else:
59 apt_install('python3-jinja2', fatal=True)
53 from jinja2 import FileSystemLoader, Environment, exceptions60 from jinja2 import FileSystemLoader, Environment, exceptions
5461
55 if templates_dir is None:62 if template_loader:
56 templates_dir = os.path.join(hookenv.charm_dir(), 'templates')63 template_env = Environment(loader=template_loader)
57 loader = Environment(loader=FileSystemLoader(templates_dir))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))
58 try:68 try:
59 source = source69 source = source
60 template = loader.get_template(source)70 template = template_env.get_template(source)
61 except exceptions.TemplateNotFound as e:71 except exceptions.TemplateNotFound as e:
62 hookenv.log('Could not load template %s from %s.' %72 hookenv.log('Could not load template %s from %s.' %
63 (source, templates_dir),73 (source, templates_dir),
64 level=hookenv.ERROR)74 level=hookenv.ERROR)
65 raise e75 raise e
66 content = template.render(context)76 content = template.render(context)
67 host.mkdir(os.path.dirname(target), owner, group, perms=0o755)77 if target is not None:
68 host.write_file(target, content.encode(encoding), owner, group, perms)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
6985
=== modified file 'hooks/charmhelpers/core/unitdata.py'
--- hooks/charmhelpers/core/unitdata.py 2015-04-09 06:46:35 +0000
+++ hooks/charmhelpers/core/unitdata.py 2017-07-25 09:17:37 +0000
@@ -3,20 +3,17 @@
3#3#
4# Copyright 2014-2015 Canonical Limited.4# Copyright 2014-2015 Canonical Limited.
5#5#
6# This file is part of charm-helpers.6# Licensed under the Apache License, Version 2.0 (the "License");
7#7# you may not use this file except in compliance with the License.
8# charm-helpers is free software: you can redistribute it and/or modify8# You may obtain a copy of the License at
9# it under the terms of the GNU Lesser General Public License version 3 as9#
10# published by the Free Software Foundation.10# http://www.apache.org/licenses/LICENSE-2.0
11#11#
12# charm-helpers is distributed in the hope that it will be useful,12# Unless required by applicable law or agreed to in writing, software
13# but WITHOUT ANY WARRANTY; without even the implied warranty of13# distributed under the License is distributed on an "AS IS" BASIS,
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# GNU Lesser General Public License for more details.15# See the License for the specific language governing permissions and
16#16# limitations under the License.
17# You should have received a copy of the GNU Lesser General Public License
18# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
19#
20#17#
21# Authors:18# Authors:
22# Kapil Thangavelu <kapil.foss@gmail.com>19# Kapil Thangavelu <kapil.foss@gmail.com>
@@ -152,6 +149,7 @@
152import collections149import collections
153import contextlib150import contextlib
154import datetime151import datetime
152import itertools
155import json153import json
156import os154import os
157import pprint155import pprint
@@ -164,8 +162,7 @@
164class Storage(object):162class Storage(object):
165 """Simple key value database for local unit state within charms.163 """Simple key value database for local unit state within charms.
166164
167 Modifications are automatically committed at hook exit. That's165 Modifications are not persisted unless :meth:`flush` is called.
168 currently regardless of exit code.
169166
170 To support dicts, lists, integer, floats, and booleans values167 To support dicts, lists, integer, floats, and booleans values
171 are automatically json encoded/decoded.168 are automatically json encoded/decoded.
@@ -173,8 +170,11 @@
173 def __init__(self, path=None):170 def __init__(self, path=None):
174 self.db_path = path171 self.db_path = path
175 if path is None:172 if path is None:
176 self.db_path = os.path.join(173 if 'UNIT_STATE_DB' in os.environ:
177 os.environ.get('CHARM_DIR', ''), '.unit-state.db')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)178 self.conn = sqlite3.connect('%s' % self.db_path)
179 self.cursor = self.conn.cursor()179 self.cursor = self.conn.cursor()
180 self.revision = None180 self.revision = None
@@ -189,15 +189,8 @@
189 self.conn.close()189 self.conn.close()
190 self._closed = True190 self._closed = True
191191
192 def _scoped_query(self, stmt, params=None):
193 if params is None:
194 params = []
195 return stmt, params
196
197 def get(self, key, default=None, record=False):192 def get(self, key, default=None, record=False):
198 self.cursor.execute(193 self.cursor.execute('select data from kv where key=?', [key])
199 *self._scoped_query(
200 'select data from kv where key=?', [key]))
201 result = self.cursor.fetchone()194 result = self.cursor.fetchone()
202 if not result:195 if not result:
203 return default196 return default
@@ -206,33 +199,81 @@
206 return json.loads(result[0])199 return json.loads(result[0])
207200
208 def getrange(self, key_prefix, strip=False):201 def getrange(self, key_prefix, strip=False):
209 stmt = "select key, data from kv where key like '%s%%'" % key_prefix202 """
210 self.cursor.execute(*self._scoped_query(stmt))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])
211 result = self.cursor.fetchall()213 result = self.cursor.fetchall()
212214
213 if not result:215 if not result:
214 return None216 return {}
215 if not strip:217 if not strip:
216 key_prefix = ''218 key_prefix = ''
217 return dict([219 return dict([
218 (k[len(key_prefix):], json.loads(v)) for k, v in result])220 (k[len(key_prefix):], json.loads(v)) for k, v in result])
219221
220 def update(self, mapping, prefix=""):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 """
221 for k, v in mapping.items():230 for k, v in mapping.items():
222 self.set("%s%s" % (prefix, k), v)231 self.set("%s%s" % (prefix, k), v)
223232
224 def unset(self, key):233 def unset(self, key):
234 """
235 Remove a key from the database entirely.
236 """
225 self.cursor.execute('delete from kv where key=?', [key])237 self.cursor.execute('delete from kv where key=?', [key])
226 if self.revision and self.cursor.rowcount:238 if self.revision and self.cursor.rowcount:
227 self.cursor.execute(239 self.cursor.execute(
228 'insert into kv_revisions values (?, ?, ?)',240 'insert into kv_revisions values (?, ?, ?)',
229 [key, self.revision, json.dumps('DELETED')])241 [key, self.revision, json.dumps('DELETED')])
230242
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
231 def set(self, key, value):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 """
232 serialized = json.dumps(value)274 serialized = json.dumps(value)
233275
234 self.cursor.execute(276 self.cursor.execute('select data from kv where key=?', [key])
235 'select data from kv where key=?', [key])
236 exists = self.cursor.fetchone()277 exists = self.cursor.fetchone()
237278
238 # Skip mutations to the same value279 # Skip mutations to the same value
239280
=== modified file 'hooks/charmhelpers/fetch/__init__.py'
--- hooks/charmhelpers/fetch/__init__.py 2015-04-09 06:46:35 +0000
+++ hooks/charmhelpers/fetch/__init__.py 2017-07-25 09:17:37 +0000
@@ -1,32 +1,24 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17import importlib15import importlib
18from tempfile import NamedTemporaryFile16from charmhelpers.osplatform import get_platform
19import time
20from yaml import safe_load17from yaml import safe_load
21from charmhelpers.core.host import (
22 lsb_release
23)
24import subprocess
25from charmhelpers.core.hookenv import (18from charmhelpers.core.hookenv import (
26 config,19 config,
27 log,20 log,
28)21)
29import os
3022
31import six23import six
32if six.PY3:24if six.PY3:
@@ -35,63 +27,6 @@
35 from urlparse import urlparse, urlunparse27 from urlparse import urlparse, urlunparse
3628
3729
38CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
39deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
40"""
41PROPOSED_POCKET = """# Proposed
42deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
43"""
44CLOUD_ARCHIVE_POCKETS = {
45 # Folsom
46 'folsom': 'precise-updates/folsom',
47 'precise-folsom': 'precise-updates/folsom',
48 'precise-folsom/updates': 'precise-updates/folsom',
49 'precise-updates/folsom': 'precise-updates/folsom',
50 'folsom/proposed': 'precise-proposed/folsom',
51 'precise-folsom/proposed': 'precise-proposed/folsom',
52 'precise-proposed/folsom': 'precise-proposed/folsom',
53 # Grizzly
54 'grizzly': 'precise-updates/grizzly',
55 'precise-grizzly': 'precise-updates/grizzly',
56 'precise-grizzly/updates': 'precise-updates/grizzly',
57 'precise-updates/grizzly': 'precise-updates/grizzly',
58 'grizzly/proposed': 'precise-proposed/grizzly',
59 'precise-grizzly/proposed': 'precise-proposed/grizzly',
60 'precise-proposed/grizzly': 'precise-proposed/grizzly',
61 # Havana
62 'havana': 'precise-updates/havana',
63 'precise-havana': 'precise-updates/havana',
64 'precise-havana/updates': 'precise-updates/havana',
65 'precise-updates/havana': 'precise-updates/havana',
66 'havana/proposed': 'precise-proposed/havana',
67 'precise-havana/proposed': 'precise-proposed/havana',
68 'precise-proposed/havana': 'precise-proposed/havana',
69 # Icehouse
70 'icehouse': 'precise-updates/icehouse',
71 'precise-icehouse': 'precise-updates/icehouse',
72 'precise-icehouse/updates': 'precise-updates/icehouse',
73 'precise-updates/icehouse': 'precise-updates/icehouse',
74 'icehouse/proposed': 'precise-proposed/icehouse',
75 'precise-icehouse/proposed': 'precise-proposed/icehouse',
76 'precise-proposed/icehouse': 'precise-proposed/icehouse',
77 # Juno
78 'juno': 'trusty-updates/juno',
79 'trusty-juno': 'trusty-updates/juno',
80 'trusty-juno/updates': 'trusty-updates/juno',
81 'trusty-updates/juno': 'trusty-updates/juno',
82 'juno/proposed': 'trusty-proposed/juno',
83 'trusty-juno/proposed': 'trusty-proposed/juno',
84 'trusty-proposed/juno': 'trusty-proposed/juno',
85 # Kilo
86 'kilo': 'trusty-updates/kilo',
87 'trusty-kilo': 'trusty-updates/kilo',
88 'trusty-kilo/updates': 'trusty-updates/kilo',
89 'trusty-updates/kilo': 'trusty-updates/kilo',
90 'kilo/proposed': 'trusty-proposed/kilo',
91 'trusty-kilo/proposed': 'trusty-proposed/kilo',
92 'trusty-proposed/kilo': 'trusty-proposed/kilo',
93}
94
95# The order of this list is very important. Handlers should be listed in from30# The order of this list is very important. Handlers should be listed in from
96# least- to most-specific URL matching.31# least- to most-specific URL matching.
97FETCH_HANDLERS = (32FETCH_HANDLERS = (
@@ -100,10 +35,6 @@
100 'charmhelpers.fetch.giturl.GitUrlFetchHandler',35 'charmhelpers.fetch.giturl.GitUrlFetchHandler',
101)36)
10237
103APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
104APT_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks.
105APT_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
106
10738
108class SourceConfigError(Exception):39class SourceConfigError(Exception):
109 pass40 pass
@@ -117,6 +48,13 @@
117 pass48 pass
11849
11950
51class GPGKeyError(Exception):
52 """Exception occurs when a GPG key cannot be fetched or used. The message
53 indicates what the problem is.
54 """
55 pass
56
57
120class BaseFetchHandler(object):58class BaseFetchHandler(object):
12159
122 """Base class for FetchHandler implementations in fetch plugins"""60 """Base class for FetchHandler implementations in fetch plugins"""
@@ -141,172 +79,39 @@
141 return urlunparse(parts)79 return urlunparse(parts)
14280
14381
144def filter_installed_packages(packages):82__platform__ = get_platform()
145 """Returns a list of packages that require installation"""83module = "charmhelpers.fetch.%s" % __platform__
146 cache = apt_cache()84fetch = importlib.import_module(module)
147 _pkgs = []85
148 for package in packages:86filter_installed_packages = fetch.filter_installed_packages
149 try:87install = fetch.apt_install
150 p = cache[package]88upgrade = fetch.apt_upgrade
151 p.current_ver or _pkgs.append(package)89update = _fetch_update = fetch.apt_update
152 except KeyError:90purge = fetch.apt_purge
153 log('Package {} has no installation candidate.'.format(package),91add_source = fetch.add_source
154 level='WARNING')92
155 _pkgs.append(package)93if __platform__ == "ubuntu":
156 return _pkgs94 apt_cache = fetch.apt_cache
15795 apt_install = fetch.apt_install
15896 apt_update = fetch.apt_update
159def apt_cache(in_memory=True):97 apt_upgrade = fetch.apt_upgrade
160 """Build and return an apt cache"""98 apt_purge = fetch.apt_purge
161 import apt_pkg99 apt_mark = fetch.apt_mark
162 apt_pkg.init()100 apt_hold = fetch.apt_hold
163 if in_memory:101 apt_unhold = fetch.apt_unhold
164 apt_pkg.config.set("Dir::Cache::pkgcache", "")102 import_key = fetch.import_key
165 apt_pkg.config.set("Dir::Cache::srcpkgcache", "")103 get_upstream_version = fetch.get_upstream_version
166 return apt_pkg.Cache()104elif __platform__ == "centos":
167105 yum_search = fetch.yum_search
168
169def apt_install(packages, options=None, fatal=False):
170 """Install one or more packages"""
171 if options is None:
172 options = ['--option=Dpkg::Options::=--force-confold']
173
174 cmd = ['apt-get', '--assume-yes']
175 cmd.extend(options)
176 cmd.append('install')
177 if isinstance(packages, six.string_types):
178 cmd.append(packages)
179 else:
180 cmd.extend(packages)
181 log("Installing {} with options: {}".format(packages,
182 options))
183 _run_apt_command(cmd, fatal)
184
185
186def apt_upgrade(options=None, fatal=False, dist=False):
187 """Upgrade all packages"""
188 if options is None:
189 options = ['--option=Dpkg::Options::=--force-confold']
190
191 cmd = ['apt-get', '--assume-yes']
192 cmd.extend(options)
193 if dist:
194 cmd.append('dist-upgrade')
195 else:
196 cmd.append('upgrade')
197 log("Upgrading with options: {}".format(options))
198 _run_apt_command(cmd, fatal)
199
200
201def apt_update(fatal=False):
202 """Update local apt cache"""
203 cmd = ['apt-get', 'update']
204 _run_apt_command(cmd, fatal)
205
206
207def apt_purge(packages, fatal=False):
208 """Purge one or more packages"""
209 cmd = ['apt-get', '--assume-yes', 'purge']
210 if isinstance(packages, six.string_types):
211 cmd.append(packages)
212 else:
213 cmd.extend(packages)
214 log("Purging {}".format(packages))
215 _run_apt_command(cmd, fatal)
216
217
218def apt_hold(packages, fatal=False):
219 """Hold one or more packages"""
220 cmd = ['apt-mark', 'hold']
221 if isinstance(packages, six.string_types):
222 cmd.append(packages)
223 else:
224 cmd.extend(packages)
225 log("Holding {}".format(packages))
226
227 if fatal:
228 subprocess.check_call(cmd)
229 else:
230 subprocess.call(cmd)
231
232
233def add_source(source, key=None):
234 """Add a package source to this system.
235
236 @param source: a URL or sources.list entry, as supported by
237 add-apt-repository(1). Examples::
238
239 ppa:charmers/example
240 deb https://stub:key@private.example.com/ubuntu trusty main
241
242 In addition:
243 'proposed:' may be used to enable the standard 'proposed'
244 pocket for the release.
245 'cloud:' may be used to activate official cloud archive pockets,
246 such as 'cloud:icehouse'
247 'distro' may be used as a noop
248
249 @param key: A key to be added to the system's APT keyring and used
250 to verify the signatures on packages. Ideally, this should be an
251 ASCII format GPG public key including the block headers. A GPG key
252 id may also be used, but be aware that only insecure protocols are
253 available to retrieve the actual public key from a public keyserver
254 placing your Juju environment at risk. ppa and cloud archive keys
255 are securely added automtically, so sould not be provided.
256 """
257 if source is None:
258 log('Source is not present. Skipping')
259 return
260
261 if (source.startswith('ppa:') or
262 source.startswith('http') or
263 source.startswith('deb ') or
264 source.startswith('cloud-archive:')):
265 subprocess.check_call(['add-apt-repository', '--yes', source])
266 elif source.startswith('cloud:'):
267 apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
268 fatal=True)
269 pocket = source.split(':')[-1]
270 if pocket not in CLOUD_ARCHIVE_POCKETS:
271 raise SourceConfigError(
272 'Unsupported cloud: source option %s' %
273 pocket)
274 actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
275 with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
276 apt.write(CLOUD_ARCHIVE.format(actual_pocket))
277 elif source == 'proposed':
278 release = lsb_release()['DISTRIB_CODENAME']
279 with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
280 apt.write(PROPOSED_POCKET.format(release))
281 elif source == 'distro':
282 pass
283 else:
284 log("Unknown source: {!r}".format(source))
285
286 if key:
287 if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
288 with NamedTemporaryFile('w+') as key_file:
289 key_file.write(key)
290 key_file.flush()
291 key_file.seek(0)
292 subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file)
293 else:
294 # Note that hkp: is in no way a secure protocol. Using a
295 # GPG key id is pointless from a security POV unless you
296 # absolutely trust your network and DNS.
297 subprocess.check_call(['apt-key', 'adv', '--keyserver',
298 'hkp://keyserver.ubuntu.com:80', '--recv',
299 key])
300106
301107
302def configure_sources(update=False,108def configure_sources(update=False,
303 sources_var='install_sources',109 sources_var='install_sources',
304 keys_var='install_keys'):110 keys_var='install_keys'):
305 """111 """Configure multiple sources from charm configuration.
306 Configure multiple sources from charm configuration.
307112
308 The lists are encoded as yaml fragments in the configuration.113 The lists are encoded as yaml fragments in the configuration.
309 The frament needs to be included as a string. Sources and their114 The fragment needs to be included as a string. Sources and their
310 corresponding keys are of the types supported by add_source().115 corresponding keys are of the types supported by add_source().
311116
312 Example config:117 Example config:
@@ -338,12 +143,11 @@
338 for source, key in zip(sources, keys):143 for source, key in zip(sources, keys):
339 add_source(source, key)144 add_source(source, key)
340 if update:145 if update:
341 apt_update(fatal=True)146 _fetch_update(fatal=True)
342147
343148
344def install_remote(source, *args, **kwargs):149def install_remote(source, *args, **kwargs):
345 """150 """Install a file tree from a remote source.
346 Install a file tree from a remote source
347151
348 The specified source should be a url of the form:152 The specified source should be a url of the form:
349 scheme://[host]/path[#[option=value][&...]]153 scheme://[host]/path[#[option=value][&...]]
@@ -366,18 +170,17 @@
366 # We ONLY check for True here because can_handle may return a string170 # We ONLY check for True here because can_handle may return a string
367 # explaining why it can't handle a given source.171 # explaining why it can't handle a given source.
368 handlers = [h for h in plugins() if h.can_handle(source) is True]172 handlers = [h for h in plugins() if h.can_handle(source) is True]
369 installed_to = None
370 for handler in handlers:173 for handler in handlers:
371 try:174 try:
372 installed_to = handler.install(source, *args, **kwargs)175 return handler.install(source, *args, **kwargs)
373 except UnhandledSource:176 except UnhandledSource as e:
374 pass177 log('Install source attempt unsuccessful: {}'.format(e),
375 if not installed_to:178 level='WARNING')
376 raise UnhandledSource("No handler found for source {}".format(source))179 raise UnhandledSource("No handler found for source {}".format(source))
377 return installed_to
378180
379181
380def install_from_config(config_var_name):182def install_from_config(config_var_name):
183 """Install a file from config."""
381 charm_config = config()184 charm_config = config()
382 source = charm_config[config_var_name]185 source = charm_config[config_var_name]
383 return install_remote(source)186 return install_remote(source)
@@ -394,46 +197,9 @@
394 importlib.import_module(package),197 importlib.import_module(package),
395 classname)198 classname)
396 plugin_list.append(handler_class())199 plugin_list.append(handler_class())
397 except (ImportError, AttributeError):200 except NotImplementedError:
398 # Skip missing plugins so that they can be ommitted from201 # Skip missing plugins so that they can be ommitted from
399 # installation if desired202 # installation if desired
400 log("FetchHandler {} not found, skipping plugin".format(203 log("FetchHandler {} not found, skipping plugin".format(
401 handler_name))204 handler_name))
402 return plugin_list205 return plugin_list
403
404
405def _run_apt_command(cmd, fatal=False):
406 """
407 Run an APT command, checking output and retrying if the fatal flag is set
408 to True.
409
410 :param: cmd: str: The apt command to run.
411 :param: fatal: bool: Whether the command's output should be checked and
412 retried.
413 """
414 env = os.environ.copy()
415
416 if 'DEBIAN_FRONTEND' not in env:
417 env['DEBIAN_FRONTEND'] = 'noninteractive'
418
419 if fatal:
420 retry_count = 0
421 result = None
422
423 # If the command is considered "fatal", we need to retry if the apt
424 # lock was not acquired.
425
426 while result is None or result == APT_NO_LOCK:
427 try:
428 result = subprocess.check_call(cmd, env=env)
429 except subprocess.CalledProcessError as e:
430 retry_count = retry_count + 1
431 if retry_count > APT_NO_LOCK_RETRY_COUNT:
432 raise
433 result = e.returncode
434 log("Couldn't acquire DPKG lock. Will retry in {} seconds."
435 "".format(APT_NO_LOCK_RETRY_DELAY))
436 time.sleep(APT_NO_LOCK_RETRY_DELAY)
437
438 else:
439 subprocess.call(cmd, env=env)
440206
=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
--- hooks/charmhelpers/fetch/archiveurl.py 2015-04-09 06:46:35 +0000
+++ hooks/charmhelpers/fetch/archiveurl.py 2017-07-25 09:17:37 +0000
@@ -1,18 +1,16 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17import os15import os
18import hashlib16import hashlib
@@ -77,6 +75,8 @@
77 def can_handle(self, source):75 def can_handle(self, source):
78 url_parts = self.parse_url(source)76 url_parts = self.parse_url(source)
79 if url_parts.scheme not in ('http', 'https', 'ftp', 'file'):77 if url_parts.scheme not in ('http', 'https', 'ftp', 'file'):
78 # XXX: Why is this returning a boolean and a string? It's
79 # doomed to fail since "bool(can_handle('foo://'))" will be True.
80 return "Wrong source type"80 return "Wrong source type"
81 if get_archive_handler(self.base_url(source)):81 if get_archive_handler(self.base_url(source)):
82 return True82 return True
@@ -106,7 +106,7 @@
106 install_opener(opener)106 install_opener(opener)
107 response = urlopen(source)107 response = urlopen(source)
108 try:108 try:
109 with open(dest, 'w') as dest_file:109 with open(dest, 'wb') as dest_file:
110 dest_file.write(response.read())110 dest_file.write(response.read())
111 except Exception as e:111 except Exception as e:
112 if os.path.isfile(dest):112 if os.path.isfile(dest):
@@ -155,7 +155,11 @@
155 else:155 else:
156 algorithms = hashlib.algorithms_available156 algorithms = hashlib.algorithms_available
157 if key in algorithms:157 if key in algorithms:
158 check_hash(dld_file, value, key)158 if len(value) != 1:
159 raise TypeError(
160 "Expected 1 hash value, not %d" % len(value))
161 expected = value[0]
162 check_hash(dld_file, expected, key)
159 if checksum:163 if checksum:
160 check_hash(dld_file, checksum, hash_type)164 check_hash(dld_file, checksum, hash_type)
161 return extract(dld_file, dest)165 return extract(dld_file, dest)
162166
=== modified file 'hooks/charmhelpers/fetch/bzrurl.py'
--- hooks/charmhelpers/fetch/bzrurl.py 2015-04-09 06:46:35 +0000
+++ hooks/charmhelpers/fetch/bzrurl.py 2017-07-25 09:17:37 +0000
@@ -1,78 +1,76 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17import os15import os
16from subprocess import check_call
18from charmhelpers.fetch import (17from charmhelpers.fetch import (
19 BaseFetchHandler,18 BaseFetchHandler,
20 UnhandledSource19 UnhandledSource,
20 filter_installed_packages,
21 install,
21)22)
22from charmhelpers.core.host import mkdir23from charmhelpers.core.host import mkdir
2324
24import six
25if six.PY3:
26 raise ImportError('bzrlib does not support Python3')
2725
28try:26if filter_installed_packages(['bzr']) != []:
29 from bzrlib.branch import Branch27 install(['bzr'])
30 from bzrlib import bzrdir, workingtree, errors28 if filter_installed_packages(['bzr']) != []:
31except ImportError:29 raise NotImplementedError('Unable to install bzr')
32 from charmhelpers.fetch import apt_install
33 apt_install("python-bzrlib")
34 from bzrlib.branch import Branch
35 from bzrlib import bzrdir, workingtree, errors
3630
3731
38class BzrUrlFetchHandler(BaseFetchHandler):32class BzrUrlFetchHandler(BaseFetchHandler):
39 """Handler for bazaar branches via generic and lp URLs"""33 """Handler for bazaar branches via generic and lp URLs."""
34
40 def can_handle(self, source):35 def can_handle(self, source):
41 url_parts = self.parse_url(source)36 url_parts = self.parse_url(source)
42 if url_parts.scheme not in ('bzr+ssh', 'lp'):37 if url_parts.scheme not in ('bzr+ssh', 'lp', ''):
43 return False38 return False
39 elif not url_parts.scheme:
40 return os.path.exists(os.path.join(source, '.bzr'))
44 else:41 else:
45 return True42 return True
4643
47 def branch(self, source, dest):44 def branch(self, source, dest, revno=None):
48 url_parts = self.parse_url(source)
49 # If we use lp:branchname scheme we need to load plugins
50 if not self.can_handle(source):45 if not self.can_handle(source):
51 raise UnhandledSource("Cannot handle {}".format(source))46 raise UnhandledSource("Cannot handle {}".format(source))
52 if url_parts.scheme == "lp":47 cmd_opts = []
53 from bzrlib.plugin import load_plugins48 if revno:
54 load_plugins()49 cmd_opts += ['-r', str(revno)]
55 try:50 if os.path.exists(dest):
56 local_branch = bzrdir.BzrDir.create_branch_convenience(dest)51 cmd = ['bzr', 'pull']
57 except errors.AlreadyControlDirError:52 cmd += cmd_opts
58 local_branch = Branch.open(dest)53 cmd += ['--overwrite', '-d', dest, source]
59 try:54 else:
60 remote_branch = Branch.open(source)55 cmd = ['bzr', 'branch']
61 remote_branch.push(local_branch)56 cmd += cmd_opts
62 tree = workingtree.WorkingTree.open(dest)57 cmd += [source, dest]
63 tree.update()58 check_call(cmd)
64 except Exception as e:
65 raise e
6659
67 def install(self, source):60 def install(self, source, dest=None, revno=None):
68 url_parts = self.parse_url(source)61 url_parts = self.parse_url(source)
69 branch_name = url_parts.path.strip("/").split("/")[-1]62 branch_name = url_parts.path.strip("/").split("/")[-1]
70 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",63 if dest:
71 branch_name)64 dest_dir = os.path.join(dest, branch_name)
72 if not os.path.exists(dest_dir):65 else:
73 mkdir(dest_dir, perms=0o755)66 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
67 branch_name)
68
69 if dest and not os.path.exists(dest):
70 mkdir(dest, perms=0o755)
71
74 try:72 try:
75 self.branch(source, dest_dir)73 self.branch(source, dest_dir, revno)
76 except OSError as e:74 except OSError as e:
77 raise UnhandledSource(e.strerror)75 raise UnhandledSource(e.strerror)
78 return dest_dir76 return dest_dir
7977
=== added file 'hooks/charmhelpers/fetch/centos.py'
--- hooks/charmhelpers/fetch/centos.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/fetch/centos.py 2017-07-25 09:17:37 +0000
@@ -0,0 +1,171 @@
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 subprocess
16import os
17import time
18import six
19import yum
20
21from tempfile import NamedTemporaryFile
22from charmhelpers.core.hookenv import log
23
24YUM_NO_LOCK = 1 # The return code for "couldn't acquire lock" in YUM.
25YUM_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks.
26YUM_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
27
28
29def filter_installed_packages(packages):
30 """Return a list of packages that require installation."""
31 yb = yum.YumBase()
32 package_list = yb.doPackageLists()
33 temp_cache = {p.base_package_name: 1 for p in package_list['installed']}
34
35 _pkgs = [p for p in packages if not temp_cache.get(p, False)]
36 return _pkgs
37
38
39def install(packages, options=None, fatal=False):
40 """Install one or more packages."""
41 cmd = ['yum', '--assumeyes']
42 if options is not None:
43 cmd.extend(options)
44 cmd.append('install')
45 if isinstance(packages, six.string_types):
46 cmd.append(packages)
47 else:
48 cmd.extend(packages)
49 log("Installing {} with options: {}".format(packages,
50 options))
51 _run_yum_command(cmd, fatal)
52
53
54def upgrade(options=None, fatal=False, dist=False):
55 """Upgrade all packages."""
56 cmd = ['yum', '--assumeyes']
57 if options is not None:
58 cmd.extend(options)
59 cmd.append('upgrade')
60 log("Upgrading with options: {}".format(options))
61 _run_yum_command(cmd, fatal)
62
63
64def update(fatal=False):
65 """Update local yum cache."""
66 cmd = ['yum', '--assumeyes', 'update']
67 log("Update with fatal: {}".format(fatal))
68 _run_yum_command(cmd, fatal)
69
70
71def purge(packages, fatal=False):
72 """Purge one or more packages."""
73 cmd = ['yum', '--assumeyes', 'remove']
74 if isinstance(packages, six.string_types):
75 cmd.append(packages)
76 else:
77 cmd.extend(packages)
78 log("Purging {}".format(packages))
79 _run_yum_command(cmd, fatal)
80
81
82def yum_search(packages):
83 """Search for a package."""
84 output = {}
85 cmd = ['yum', 'search']
86 if isinstance(packages, six.string_types):
87 cmd.append(packages)
88 else:
89 cmd.extend(packages)
90 log("Searching for {}".format(packages))
91 result = subprocess.check_output(cmd)
92 for package in list(packages):
93 output[package] = package in result
94 return output
95
96
97def add_source(source, key=None):
98 """Add a package source to this system.
99
100 @param source: a URL with a rpm package
101
102 @param key: A key to be added to the system's keyring and used
103 to verify the signatures on packages. Ideally, this should be an
104 ASCII format GPG public key including the block headers. A GPG key
105 id may also be used, but be aware that only insecure protocols are
106 available to retrieve the actual public key from a public keyserver
107 placing your Juju environment at risk.
108 """
109 if source is None:
110 log('Source is not present. Skipping')
111 return
112
113 if source.startswith('http'):
114 directory = '/etc/yum.repos.d/'
115 for filename in os.listdir(directory):
116 with open(directory + filename, 'r') as rpm_file:
117 if source in rpm_file.read():
118 break
119 else:
120 log("Add source: {!r}".format(source))
121 # write in the charms.repo
122 with open(directory + 'Charms.repo', 'a') as rpm_file:
123 rpm_file.write('[%s]\n' % source[7:].replace('/', '_'))
124 rpm_file.write('name=%s\n' % source[7:])
125 rpm_file.write('baseurl=%s\n\n' % source)
126 else:
127 log("Unknown source: {!r}".format(source))
128
129 if key:
130 if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
131 with NamedTemporaryFile('w+') as key_file:
132 key_file.write(key)
133 key_file.flush()
134 key_file.seek(0)
135 subprocess.check_call(['rpm', '--import', key_file.name])
136 else:
137 subprocess.check_call(['rpm', '--import', key])
138
139
140def _run_yum_command(cmd, fatal=False):
141 """Run an YUM command.
142
143 Checks the output and retry if the fatal flag is set to True.
144
145 :param: cmd: str: The yum command to run.
146 :param: fatal: bool: Whether the command's output should be checked and
147 retried.
148 """
149 env = os.environ.copy()
150
151 if fatal:
152 retry_count = 0
153 result = None
154
155 # If the command is considered "fatal", we need to retry if the yum
156 # lock was not acquired.
157
158 while result is None or result == YUM_NO_LOCK:
159 try:
160 result = subprocess.check_call(cmd, env=env)
161 except subprocess.CalledProcessError as e:
162 retry_count = retry_count + 1
163 if retry_count > YUM_NO_LOCK_RETRY_COUNT:
164 raise
165 result = e.returncode
166 log("Couldn't acquire YUM lock. Will retry in {} seconds."
167 "".format(YUM_NO_LOCK_RETRY_DELAY))
168 time.sleep(YUM_NO_LOCK_RETRY_DELAY)
169
170 else:
171 subprocess.call(cmd, env=env)
0172
=== modified file 'hooks/charmhelpers/fetch/giturl.py'
--- hooks/charmhelpers/fetch/giturl.py 2015-04-09 06:46:35 +0000
+++ hooks/charmhelpers/fetch/giturl.py 2017-07-25 09:17:37 +0000
@@ -1,58 +1,58 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2015 Canonical Limited.
2#2#
3# This file is part of charm-helpers.3# Licensed under the Apache License, Version 2.0 (the "License");
4#4# you may not use this file except in compliance with the License.
5# charm-helpers is free software: you can redistribute it and/or modify5# You may obtain a copy of the License at
6# it under the terms of the GNU Lesser General Public License version 3 as6#
7# published by the Free Software Foundation.7# http://www.apache.org/licenses/LICENSE-2.0
8#8#
9# charm-helpers is distributed in the hope that it will be useful,9# Unless required by applicable law or agreed to in writing, software
10# but WITHOUT ANY WARRANTY; without even the implied warranty of10# distributed under the License is distributed on an "AS IS" BASIS,
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# GNU Lesser General Public License for more details.12# See the License for the specific language governing permissions and
13#13# limitations under the License.
14# You should have received a copy of the GNU Lesser General Public License
15# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1614
17import os15import os
16from subprocess import check_call, CalledProcessError
18from charmhelpers.fetch import (17from charmhelpers.fetch import (
19 BaseFetchHandler,18 BaseFetchHandler,
20 UnhandledSource19 UnhandledSource,
20 filter_installed_packages,
21 install,
21)22)
22from charmhelpers.core.host import mkdir23
2324if filter_installed_packages(['git']) != []:
24import six25 install(['git'])
25if six.PY3:26 if filter_installed_packages(['git']) != []:
26 raise ImportError('GitPython does not support Python 3')27 raise NotImplementedError('Unable to install git')
27
28try:
29 from git import Repo
30except ImportError:
31 from charmhelpers.fetch import apt_install
32 apt_install("python-git")
33 from git import Repo
34
35from git.exc import GitCommandError # noqa E402
3628
3729
38class GitUrlFetchHandler(BaseFetchHandler):30class GitUrlFetchHandler(BaseFetchHandler):
39 """Handler for git branches via generic and github URLs"""31 """Handler for git branches via generic and github URLs."""
32
40 def can_handle(self, source):33 def can_handle(self, source):
41 url_parts = self.parse_url(source)34 url_parts = self.parse_url(source)
42 # TODO (mattyw) no support for ssh git@ yet35 # TODO (mattyw) no support for ssh git@ yet
43 if url_parts.scheme not in ('http', 'https', 'git'):36 if url_parts.scheme not in ('http', 'https', 'git', ''):
44 return False37 return False
38 elif not url_parts.scheme:
39 return os.path.exists(os.path.join(source, '.git'))
45 else:40 else:
46 return True41 return True
4742
48 def clone(self, source, dest, branch):43 def clone(self, source, dest, branch="master", depth=None):
49 if not self.can_handle(source):44 if not self.can_handle(source):
50 raise UnhandledSource("Cannot handle {}".format(source))45 raise UnhandledSource("Cannot handle {}".format(source))
5146
52 repo = Repo.clone_from(source, dest)47 if os.path.exists(dest):
53 repo.git.checkout(branch)48 cmd = ['git', '-C', dest, 'pull', source, branch]
49 else:
50 cmd = ['git', 'clone', source, dest, '--branch', branch]
51 if depth:
52 cmd.extend(['--depth', depth])
53 check_call(cmd)
5454
55 def install(self, source, branch="master", dest=None):55 def install(self, source, branch="master", dest=None, depth=None):
56 url_parts = self.parse_url(source)56 url_parts = self.parse_url(source)
57 branch_name = url_parts.path.strip("/").split("/")[-1]57 branch_name = url_parts.path.strip("/").split("/")[-1]
58 if dest:58 if dest:
@@ -60,12 +60,10 @@
60 else:60 else:
61 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",61 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
62 branch_name)62 branch_name)
63 if not os.path.exists(dest_dir):
64 mkdir(dest_dir, perms=0o755)
65 try:63 try:
66 self.clone(source, dest_dir, branch)64 self.clone(source, dest_dir, branch, depth)
67 except GitCommandError as e:65 except CalledProcessError as e:
68 raise UnhandledSource(e.message)66 raise UnhandledSource(e)
69 except OSError as e:67 except OSError as e:
70 raise UnhandledSource(e.strerror)68 raise UnhandledSource(e.strerror)
71 return dest_dir69 return dest_dir
7270
=== added file 'hooks/charmhelpers/fetch/snap.py'
--- hooks/charmhelpers/fetch/snap.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/fetch/snap.py 2017-07-25 09:17:37 +0000
@@ -0,0 +1,122 @@
1# Copyright 2014-2017 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"""
15Charm helpers snap for classic charms.
16
17If writing reactive charms, use the snap layer:
18https://lists.ubuntu.com/archives/snapcraft/2016-September/001114.html
19"""
20import subprocess
21from os import environ
22from time import sleep
23from charmhelpers.core.hookenv import log
24
25__author__ = 'Joseph Borg <joseph.borg@canonical.com>'
26
27SNAP_NO_LOCK = 1 # The return code for "couldn't acquire lock" in Snap (hopefully this will be improved).
28SNAP_NO_LOCK_RETRY_DELAY = 10 # Wait X seconds between Snap lock checks.
29SNAP_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
30
31
32class CouldNotAcquireLockException(Exception):
33 pass
34
35
36def _snap_exec(commands):
37 """
38 Execute snap commands.
39
40 :param commands: List commands
41 :return: Integer exit code
42 """
43 assert type(commands) == list
44
45 retry_count = 0
46 return_code = None
47
48 while return_code is None or return_code == SNAP_NO_LOCK:
49 try:
50 return_code = subprocess.check_call(['snap'] + commands, env=environ)
51 except subprocess.CalledProcessError as e:
52 retry_count += + 1
53 if retry_count > SNAP_NO_LOCK_RETRY_COUNT:
54 raise CouldNotAcquireLockException('Could not aquire lock after %s attempts' % SNAP_NO_LOCK_RETRY_COUNT)
55 return_code = e.returncode
56 log('Snap failed to acquire lock, trying again in %s seconds.' % SNAP_NO_LOCK_RETRY_DELAY, level='WARN')
57 sleep(SNAP_NO_LOCK_RETRY_DELAY)
58
59 return return_code
60
61
62def snap_install(packages, *flags):
63 """
64 Install a snap package.
65
66 :param packages: String or List String package name
67 :param flags: List String flags to pass to install command
68 :return: Integer return code from snap
69 """
70 if type(packages) is not list:
71 packages = [packages]
72
73 flags = list(flags)
74
75 message = 'Installing snap(s) "%s"' % ', '.join(packages)
76 if flags:
77 message += ' with option(s) "%s"' % ', '.join(flags)
78
79 log(message, level='INFO')
80 return _snap_exec(['install'] + flags + packages)
81
82
83def snap_remove(packages, *flags):
84 """
85 Remove a snap package.
86
87 :param packages: String or List String package name
88 :param flags: List String flags to pass to remove command
89 :return: Integer return code from snap
90 """
91 if type(packages) is not list:
92 packages = [packages]
93
94 flags = list(flags)
95
96 message = 'Removing snap(s) "%s"' % ', '.join(packages)
97 if flags:
98 message += ' with options "%s"' % ', '.join(flags)
99
100 log(message, level='INFO')
101 return _snap_exec(['remove'] + flags + packages)
102
103
104def snap_refresh(packages, *flags):
105 """
106 Refresh / Update snap package.
107
108 :param packages: String or List String package name
109 :param flags: List String flags to pass to refresh command
110 :return: Integer return code from snap
111 """
112 if type(packages) is not list:
113 packages = [packages]
114
115 flags = list(flags)
116
117 message = 'Refreshing snap(s) "%s"' % ', '.join(packages)
118 if flags:
119 message += ' with options "%s"' % ', '.join(flags)
120
121 log(message, level='INFO')
122 return _snap_exec(['refresh'] + flags + packages)
0123
=== added file 'hooks/charmhelpers/fetch/ubuntu.py'
--- hooks/charmhelpers/fetch/ubuntu.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/fetch/ubuntu.py 2017-07-25 09:17:37 +0000
@@ -0,0 +1,568 @@
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 collections import OrderedDict
16import os
17import platform
18import re
19import six
20import time
21import subprocess
22from tempfile import NamedTemporaryFile
23
24from charmhelpers.core.host import (
25 lsb_release
26)
27from charmhelpers.core.hookenv import (
28 log,
29 DEBUG,
30)
31from charmhelpers.fetch import SourceConfigError, GPGKeyError
32
33PROPOSED_POCKET = (
34 "# Proposed\n"
35 "deb http://archive.ubuntu.com/ubuntu {}-proposed main universe "
36 "multiverse restricted\n")
37PROPOSED_PORTS_POCKET = (
38 "# Proposed\n"
39 "deb http://ports.ubuntu.com/ubuntu-ports {}-proposed main universe "
40 "multiverse restricted\n")
41# Only supports 64bit and ppc64 at the moment.
42ARCH_TO_PROPOSED_POCKET = {
43 'x86_64': PROPOSED_POCKET,
44 'ppc64le': PROPOSED_PORTS_POCKET,
45 'aarch64': PROPOSED_PORTS_POCKET,
46}
47CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu"
48CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA'
49CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
50deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
51"""
52CLOUD_ARCHIVE_POCKETS = {
53 # Folsom
54 'folsom': 'precise-updates/folsom',
55 'folsom/updates': 'precise-updates/folsom',
56 'precise-folsom': 'precise-updates/folsom',
57 'precise-folsom/updates': 'precise-updates/folsom',
58 'precise-updates/folsom': 'precise-updates/folsom',
59 'folsom/proposed': 'precise-proposed/folsom',
60 'precise-folsom/proposed': 'precise-proposed/folsom',
61 'precise-proposed/folsom': 'precise-proposed/folsom',
62 # Grizzly
63 'grizzly': 'precise-updates/grizzly',
64 'grizzly/updates': 'precise-updates/grizzly',
65 'precise-grizzly': 'precise-updates/grizzly',
66 'precise-grizzly/updates': 'precise-updates/grizzly',
67 'precise-updates/grizzly': 'precise-updates/grizzly',
68 'grizzly/proposed': 'precise-proposed/grizzly',
69 'precise-grizzly/proposed': 'precise-proposed/grizzly',
70 'precise-proposed/grizzly': 'precise-proposed/grizzly',
71 # Havana
72 'havana': 'precise-updates/havana',
73 'havana/updates': 'precise-updates/havana',
74 'precise-havana': 'precise-updates/havana',
75 'precise-havana/updates': 'precise-updates/havana',
76 'precise-updates/havana': 'precise-updates/havana',
77 'havana/proposed': 'precise-proposed/havana',
78 'precise-havana/proposed': 'precise-proposed/havana',
79 'precise-proposed/havana': 'precise-proposed/havana',
80 # Icehouse
81 'icehouse': 'precise-updates/icehouse',
82 'icehouse/updates': 'precise-updates/icehouse',
83 'precise-icehouse': 'precise-updates/icehouse',
84 'precise-icehouse/updates': 'precise-updates/icehouse',
85 'precise-updates/icehouse': 'precise-updates/icehouse',
86 'icehouse/proposed': 'precise-proposed/icehouse',
87 'precise-icehouse/proposed': 'precise-proposed/icehouse',
88 'precise-proposed/icehouse': 'precise-proposed/icehouse',
89 # Juno
90 'juno': 'trusty-updates/juno',
91 'juno/updates': 'trusty-updates/juno',
92 'trusty-juno': 'trusty-updates/juno',
93 'trusty-juno/updates': 'trusty-updates/juno',
94 'trusty-updates/juno': 'trusty-updates/juno',
95 'juno/proposed': 'trusty-proposed/juno',
96 'trusty-juno/proposed': 'trusty-proposed/juno',
97 'trusty-proposed/juno': 'trusty-proposed/juno',
98 # Kilo
99 'kilo': 'trusty-updates/kilo',
100 'kilo/updates': 'trusty-updates/kilo',
101 'trusty-kilo': 'trusty-updates/kilo',
102 'trusty-kilo/updates': 'trusty-updates/kilo',
103 'trusty-updates/kilo': 'trusty-updates/kilo',
104 'kilo/proposed': 'trusty-proposed/kilo',
105 'trusty-kilo/proposed': 'trusty-proposed/kilo',
106 'trusty-proposed/kilo': 'trusty-proposed/kilo',
107 # Liberty
108 'liberty': 'trusty-updates/liberty',
109 'liberty/updates': 'trusty-updates/liberty',
110 'trusty-liberty': 'trusty-updates/liberty',
111 'trusty-liberty/updates': 'trusty-updates/liberty',
112 'trusty-updates/liberty': 'trusty-updates/liberty',
113 'liberty/proposed': 'trusty-proposed/liberty',
114 'trusty-liberty/proposed': 'trusty-proposed/liberty',
115 'trusty-proposed/liberty': 'trusty-proposed/liberty',
116 # Mitaka
117 'mitaka': 'trusty-updates/mitaka',
118 'mitaka/updates': 'trusty-updates/mitaka',
119 'trusty-mitaka': 'trusty-updates/mitaka',
120 'trusty-mitaka/updates': 'trusty-updates/mitaka',
121 'trusty-updates/mitaka': 'trusty-updates/mitaka',
122 'mitaka/proposed': 'trusty-proposed/mitaka',
123 'trusty-mitaka/proposed': 'trusty-proposed/mitaka',
124 'trusty-proposed/mitaka': 'trusty-proposed/mitaka',
125 # Newton
126 'newton': 'xenial-updates/newton',
127 'newton/updates': 'xenial-updates/newton',
128 'xenial-newton': 'xenial-updates/newton',
129 'xenial-newton/updates': 'xenial-updates/newton',
130 'xenial-updates/newton': 'xenial-updates/newton',
131 'newton/proposed': 'xenial-proposed/newton',
132 'xenial-newton/proposed': 'xenial-proposed/newton',
133 'xenial-proposed/newton': 'xenial-proposed/newton',
134 # Ocata
135 'ocata': 'xenial-updates/ocata',
136 'ocata/updates': 'xenial-updates/ocata',
137 'xenial-ocata': 'xenial-updates/ocata',
138 'xenial-ocata/updates': 'xenial-updates/ocata',
139 'xenial-updates/ocata': 'xenial-updates/ocata',
140 'ocata/proposed': 'xenial-proposed/ocata',
141 'xenial-ocata/proposed': 'xenial-proposed/ocata',
142 'xenial-ocata/newton': 'xenial-proposed/ocata',
143 # Pike
144 'pike': 'xenial-updates/pike',
145 'xenial-pike': 'xenial-updates/pike',
146 'xenial-pike/updates': 'xenial-updates/pike',
147 'xenial-updates/pike': 'xenial-updates/pike',
148 'pike/proposed': 'xenial-proposed/pike',
149 'xenial-pike/proposed': 'xenial-proposed/pike',
150 'xenial-pike/newton': 'xenial-proposed/pike',
151 # Queens
152 'queens': 'xenial-updates/queens',
153 'xenial-queens': 'xenial-updates/queens',
154 'xenial-queens/updates': 'xenial-updates/queens',
155 'xenial-updates/queens': 'xenial-updates/queens',
156 'queens/proposed': 'xenial-proposed/queens',
157 'xenial-queens/proposed': 'xenial-proposed/queens',
158 'xenial-queens/newton': 'xenial-proposed/queens',
159}
160
161
162APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
163CMD_RETRY_DELAY = 10 # Wait 10 seconds between command retries.
164CMD_RETRY_COUNT = 30 # Retry a failing fatal command X times.
165
166
167def filter_installed_packages(packages):
168 """Return a list of packages that require installation."""
169 cache = apt_cache()
170 _pkgs = []
171 for package in packages:
172 try:
173 p = cache[package]
174 p.current_ver or _pkgs.append(package)
175 except KeyError:
176 log('Package {} has no installation candidate.'.format(package),
177 level='WARNING')
178 _pkgs.append(package)
179 return _pkgs
180
181
182def apt_cache(in_memory=True, progress=None):
183 """Build and return an apt cache."""
184 from apt import apt_pkg
185 apt_pkg.init()
186 if in_memory:
187 apt_pkg.config.set("Dir::Cache::pkgcache", "")
188 apt_pkg.config.set("Dir::Cache::srcpkgcache", "")
189 return apt_pkg.Cache(progress)
190
191
192def apt_install(packages, options=None, fatal=False):
193 """Install one or more packages."""
194 if options is None:
195 options = ['--option=Dpkg::Options::=--force-confold']
196
197 cmd = ['apt-get', '--assume-yes']
198 cmd.extend(options)
199 cmd.append('install')
200 if isinstance(packages, six.string_types):
201 cmd.append(packages)
202 else:
203 cmd.extend(packages)
204 log("Installing {} with options: {}".format(packages,
205 options))
206 _run_apt_command(cmd, fatal)
207
208
209def apt_upgrade(options=None, fatal=False, dist=False):
210 """Upgrade all packages."""
211 if options is None:
212 options = ['--option=Dpkg::Options::=--force-confold']
213
214 cmd = ['apt-get', '--assume-yes']
215 cmd.extend(options)
216 if dist:
217 cmd.append('dist-upgrade')
218 else:
219 cmd.append('upgrade')
220 log("Upgrading with options: {}".format(options))
221 _run_apt_command(cmd, fatal)
222
223
224def apt_update(fatal=False):
225 """Update local apt cache."""
226 cmd = ['apt-get', 'update']
227 _run_apt_command(cmd, fatal)
228
229
230def apt_purge(packages, fatal=False):
231 """Purge one or more packages."""
232 cmd = ['apt-get', '--assume-yes', 'purge']
233 if isinstance(packages, six.string_types):
234 cmd.append(packages)
235 else:
236 cmd.extend(packages)
237 log("Purging {}".format(packages))
238 _run_apt_command(cmd, fatal)
239
240
241def apt_mark(packages, mark, fatal=False):
242 """Flag one or more packages using apt-mark."""
243 log("Marking {} as {}".format(packages, mark))
244 cmd = ['apt-mark', mark]
245 if isinstance(packages, six.string_types):
246 cmd.append(packages)
247 else:
248 cmd.extend(packages)
249
250 if fatal:
251 subprocess.check_call(cmd, universal_newlines=True)
252 else:
253 subprocess.call(cmd, universal_newlines=True)
254
255
256def apt_hold(packages, fatal=False):
257 return apt_mark(packages, 'hold', fatal=fatal)
258
259
260def apt_unhold(packages, fatal=False):
261 return apt_mark(packages, 'unhold', fatal=fatal)
262
263
264def import_key(keyid):
265 """Import a key in either ASCII Armor or Radix64 format.
266
267 `keyid` is either the keyid to fetch from a PGP server, or
268 the key in ASCII armor foramt.
269
270 :param keyid: String of key (or key id).
271 :raises: GPGKeyError if the key could not be imported
272 """
273 key = keyid.strip()
274 if (key.startswith('-----BEGIN PGP PUBLIC KEY BLOCK-----') and
275 key.endswith('-----END PGP PUBLIC KEY BLOCK-----')):
276 log("PGP key found (looks like ASCII Armor format)", level=DEBUG)
277 log("Importing ASCII Armor PGP key", level=DEBUG)
278 with NamedTemporaryFile() as keyfile:
279 with open(keyfile.name, 'w') as fd:
280 fd.write(key)
281 fd.write("\n")
282 cmd = ['apt-key', 'add', keyfile.name]
283 try:
284 subprocess.check_call(cmd)
285 except subprocess.CalledProcessError:
286 error = "Error importing PGP key '{}'".format(key)
287 log(error)
288 raise GPGKeyError(error)
289 else:
290 log("PGP key found (looks like Radix64 format)", level=DEBUG)
291 log("Importing PGP key from keyserver", level=DEBUG)
292 cmd = ['apt-key', 'adv', '--keyserver',
293 'hkp://keyserver.ubuntu.com:80', '--recv-keys', key]
294 try:
295 subprocess.check_call(cmd)
296 except subprocess.CalledProcessError:
297 error = "Error importing PGP key '{}'".format(key)
298 log(error)
299 raise GPGKeyError(error)
300
301
302def add_source(source, key=None, fail_invalid=False):
303 """Add a package source to this system.
304
305 @param source: a URL or sources.list entry, as supported by
306 add-apt-repository(1). Examples::
307
308 ppa:charmers/example
309 deb https://stub:key@private.example.com/ubuntu trusty main
310
311 In addition:
312 'proposed:' may be used to enable the standard 'proposed'
313 pocket for the release.
314 'cloud:' may be used to activate official cloud archive pockets,
315 such as 'cloud:icehouse'
316 'distro' may be used as a noop
317
318 Full list of source specifications supported by the function are:
319
320 'distro': A NOP; i.e. it has no effect.
321 'proposed': the proposed deb spec [2] is wrtten to
322 /etc/apt/sources.list/proposed
323 'distro-proposed': adds <version>-proposed to the debs [2]
324 'ppa:<ppa-name>': add-apt-repository --yes <ppa_name>
325 'deb <deb-spec>': add-apt-repository --yes deb <deb-spec>
326 'http://....': add-apt-repository --yes http://...
327 'cloud-archive:<spec>': add-apt-repository -yes cloud-archive:<spec>
328 'cloud:<release>[-staging]': specify a Cloud Archive pocket <release> with
329 optional staging version. If staging is used then the staging PPA [2]
330 with be used. If staging is NOT used then the cloud archive [3] will be
331 added, and the 'ubuntu-cloud-keyring' package will be added for the
332 current distro.
333
334 Otherwise the source is not recognised and this is logged to the juju log.
335 However, no error is raised, unless sys_error_on_exit is True.
336
337 [1] deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
338 where {} is replaced with the derived pocket name.
339 [2] deb http://archive.ubuntu.com/ubuntu {}-proposed \
340 main universe multiverse restricted
341 where {} is replaced with the lsb_release codename (e.g. xenial)
342 [3] deb http://ubuntu-cloud.archive.canonical.com/ubuntu <pocket>
343 to /etc/apt/sources.list.d/cloud-archive-list
344
345 @param key: A key to be added to the system's APT keyring and used
346 to verify the signatures on packages. Ideally, this should be an
347 ASCII format GPG public key including the block headers. A GPG key
348 id may also be used, but be aware that only insecure protocols are
349 available to retrieve the actual public key from a public keyserver
350 placing your Juju environment at risk. ppa and cloud archive keys
351 are securely added automtically, so sould not be provided.
352
353 @param fail_invalid: (boolean) if True, then the function raises a
354 SourceConfigError is there is no matching installation source.
355
356 @raises SourceConfigError() if for cloud:<pocket>, the <pocket> is not a
357 valid pocket in CLOUD_ARCHIVE_POCKETS
358 """
359 _mapping = OrderedDict([
360 (r"^distro$", lambda: None), # This is a NOP
361 (r"^(?:proposed|distro-proposed)$", _add_proposed),
362 (r"^cloud-archive:(.*)$", _add_apt_repository),
363 (r"^((?:deb |http:|https:|ppa:).*)$", _add_apt_repository),
364 (r"^cloud:(.*)-(.*)\/staging$", _add_cloud_staging),
365 (r"^cloud:(.*)-(.*)$", _add_cloud_distro_check),
366 (r"^cloud:(.*)$", _add_cloud_pocket),
367 ])
368 if source is None:
369 source = ''
370 for r, fn in six.iteritems(_mapping):
371 m = re.match(r, source)
372 if m:
373 # call the assoicated function with the captured groups
374 # raises SourceConfigError on error.
375 fn(*m.groups())
376 if key:
377 try:
378 import_key(key)
379 except GPGKeyError as e:
380 raise SourceConfigError(str(e))
381 break
382 else:
383 # nothing matched. log an error and maybe sys.exit
384 err = "Unknown source: {!r}".format(source)
385 log(err)
386 if fail_invalid:
387 raise SourceConfigError(err)
388
389
390def _add_proposed():
391 """Add the PROPOSED_POCKET as /etc/apt/source.list.d/proposed.list
392
393 Uses lsb_release()['DISTRIB_CODENAME'] to determine the correct staza for
394 the deb line.
395
396 For intel architecutres PROPOSED_POCKET is used for the release, but for
397 other architectures PROPOSED_PORTS_POCKET is used for the release.
398 """
399 release = lsb_release()['DISTRIB_CODENAME']
400 arch = platform.machine()
401 if arch not in six.iterkeys(ARCH_TO_PROPOSED_POCKET):
402 raise SourceConfigError("Arch {} not supported for (distro-)proposed"
403 .format(arch))
404 with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
405 apt.write(ARCH_TO_PROPOSED_POCKET[arch].format(release))
406
407
408def _add_apt_repository(spec):
409 """Add the spec using add_apt_repository
410
411 :param spec: the parameter to pass to add_apt_repository
412 """
413 _run_with_retries(['add-apt-repository', '--yes', spec])
414
415
416def _add_cloud_pocket(pocket):
417 """Add a cloud pocket as /etc/apt/sources.d/cloud-archive.list
418
419 Note that this overwrites the existing file if there is one.
420
421 This function also converts the simple pocket in to the actual pocket using
422 the CLOUD_ARCHIVE_POCKETS mapping.
423
424 :param pocket: string representing the pocket to add a deb spec for.
425 :raises: SourceConfigError if the cloud pocket doesn't exist or the
426 requested release doesn't match the current distro version.
427 """
428 apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
429 fatal=True)
430 if pocket not in CLOUD_ARCHIVE_POCKETS:
431 raise SourceConfigError(
432 'Unsupported cloud: source option %s' %
433 pocket)
434 actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
435 with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
436 apt.write(CLOUD_ARCHIVE.format(actual_pocket))
437
438
439def _add_cloud_staging(cloud_archive_release, openstack_release):
440 """Add the cloud staging repository which is in
441 ppa:ubuntu-cloud-archive/<openstack_release>-staging
442
443 This function checks that the cloud_archive_release matches the current
444 codename for the distro that charm is being installed on.
445
446 :param cloud_archive_release: string, codename for the release.
447 :param openstack_release: String, codename for the openstack release.
448 :raises: SourceConfigError if the cloud_archive_release doesn't match the
449 current version of the os.
450 """
451 _verify_is_ubuntu_rel(cloud_archive_release, openstack_release)
452 ppa = 'ppa:ubuntu-cloud-archive/{}-staging'.format(openstack_release)
453 cmd = 'add-apt-repository -y {}'.format(ppa)
454 _run_with_retries(cmd.split(' '))
455
456
457def _add_cloud_distro_check(cloud_archive_release, openstack_release):
458 """Add the cloud pocket, but also check the cloud_archive_release against
459 the current distro, and use the openstack_release as the full lookup.
460
461 This just calls _add_cloud_pocket() with the openstack_release as pocket
462 to get the correct cloud-archive.list for dpkg to work with.
463
464 :param cloud_archive_release:String, codename for the distro release.
465 :param openstack_release: String, spec for the release to look up in the
466 CLOUD_ARCHIVE_POCKETS
467 :raises: SourceConfigError if this is the wrong distro, or the pocket spec
468 doesn't exist.
469 """
470 _verify_is_ubuntu_rel(cloud_archive_release, openstack_release)
471 _add_cloud_pocket("{}-{}".format(cloud_archive_release, openstack_release))
472
473
474def _verify_is_ubuntu_rel(release, os_release):
475 """Verify that the release is in the same as the current ubuntu release.
476
477 :param release: String, lowercase for the release.
478 :param os_release: String, the os_release being asked for
479 :raises: SourceConfigError if the release is not the same as the ubuntu
480 release.
481 """
482 ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
483 if release != ubuntu_rel:
484 raise SourceConfigError(
485 'Invalid Cloud Archive release specified: {}-{} on this Ubuntu'
486 'version ({})'.format(release, os_release, ubuntu_rel))
487
488
489def _run_with_retries(cmd, max_retries=CMD_RETRY_COUNT, retry_exitcodes=(1,),
490 retry_message="", cmd_env=None):
491 """Run a command and retry until success or max_retries is reached.
492
493 :param: cmd: str: The apt command to run.
494 :param: max_retries: int: The number of retries to attempt on a fatal
495 command. Defaults to CMD_RETRY_COUNT.
496 :param: retry_exitcodes: tuple: Optional additional exit codes to retry.
497 Defaults to retry on exit code 1.
498 :param: retry_message: str: Optional log prefix emitted during retries.
499 :param: cmd_env: dict: Environment variables to add to the command run.
500 """
501
502 env = None
503 kwargs = {}
504 if cmd_env:
505 env = os.environ.copy()
506 env.update(cmd_env)
507 kwargs['env'] = env
508
509 if not retry_message:
510 retry_message = "Failed executing '{}'".format(" ".join(cmd))
511 retry_message += ". Will retry in {} seconds".format(CMD_RETRY_DELAY)
512
513 retry_count = 0
514 result = None
515
516 retry_results = (None,) + retry_exitcodes
517 while result in retry_results:
518 try:
519 # result = subprocess.check_call(cmd, env=env)
520 result = subprocess.check_call(cmd, **kwargs)
521 except subprocess.CalledProcessError as e:
522 retry_count = retry_count + 1
523 if retry_count > max_retries:
524 raise
525 result = e.returncode
526 log(retry_message)
527 time.sleep(CMD_RETRY_DELAY)
528
529
530def _run_apt_command(cmd, fatal=False):
531 """Run an apt command with optional retries.
532
533 :param: cmd: str: The apt command to run.
534 :param: fatal: bool: Whether the command's output should be checked and
535 retried.
536 """
537 # Provide DEBIAN_FRONTEND=noninteractive if not present in the environment.
538 cmd_env = {
539 'DEBIAN_FRONTEND': os.environ.get('DEBIAN_FRONTEND', 'noninteractive')}
540
541 if fatal:
542 _run_with_retries(
543 cmd, cmd_env=cmd_env, retry_exitcodes=(1, APT_NO_LOCK,),
544 retry_message="Couldn't acquire DPKG lock")
545 else:
546 env = os.environ.copy()
547 env.update(cmd_env)
548 subprocess.call(cmd, env=env)
549
550
551def get_upstream_version(package):
552 """Determine upstream version based on installed package
553
554 @returns None (if not installed) or the upstream version
555 """
556 import apt_pkg
557 cache = apt_cache()
558 try:
559 pkg = cache[package]
560 except:
561 # the package is unknown to the current apt cache.
562 return None
563
564 if not pkg.current_ver:
565 # package is known, but no version is currently installed.
566 return None
567
568 return apt_pkg.upstream_version(pkg.current_ver.ver_str)
0569
=== added file 'hooks/charmhelpers/osplatform.py'
--- hooks/charmhelpers/osplatform.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/osplatform.py 2017-07-25 09:17:37 +0000
@@ -0,0 +1,25 @@
1import platform
2
3
4def get_platform():
5 """Return the current OS platform.
6
7 For example: if current os platform is Ubuntu then a string "ubuntu"
8 will be returned (which is the name of the module).
9 This string is used to decide which platform module should be imported.
10 """
11 # linux_distribution is deprecated and will be removed in Python 3.7
12 # Warings *not* disabled, as we certainly need to fix this.
13 tuple_platform = platform.linux_distribution()
14 current_platform = tuple_platform[0]
15 if "Ubuntu" in current_platform:
16 return "ubuntu"
17 elif "CentOS" in current_platform:
18 return "centos"
19 elif "debian" in current_platform:
20 # Stock Python does not detect Ubuntu and instead returns debian.
21 # Or at least it does in some build environments like Travis CI
22 return "ubuntu"
23 else:
24 raise RuntimeError("This module is not supported on {}."
25 .format(current_platform))
026
=== modified file 'hooks/install.real'
--- hooks/install.real 2017-03-21 04:14:41 +0000
+++ hooks/install.real 2017-07-25 09:17:37 +0000
@@ -15,12 +15,10 @@
15 # e.g.: from charmhelpers import fetch15 # e.g.: from charmhelpers import fetch
16 # fetch.apt_install(fetch.filter_installed_packages(['nginx']))16 # fetch.apt_install(fetch.filter_installed_packages(['nginx']))
17 config = hookenv.config()17 config = hookenv.config()
18 ppa = config.get('source')18 add_source(config.get('source'), config.get('key', None))
19 if ppa is not None:19 apt_update(fatal=True)
20 add_source(ppa)20 package_list = ["thruk", "pwgen", "apache2-utils"]
21 apt_update()21 apt_install(packages=package_list, fatal=True)
22
23 apt_install(["thruk", "pwgen", "apache2-utils"])
2422
2523
26if __name__ == "__main__":24if __name__ == "__main__":
2725
=== modified file 'templates/thruk_local.conf'
--- templates/thruk_local.conf 2015-04-28 05:49:19 +0000
+++ templates/thruk_local.conf 2017-07-25 09:17:37 +0000
@@ -13,3 +13,7 @@
13{% endfor -%}13{% endfor -%}
14</Component>14</Component>
1515
16{% if config['enable-lmd'] -%}
17use_lmd_core=1
18lmd_core_bin=/usr/bin/lmd
19{% endif -%}

Subscribers

People subscribed via source and target branches