Merge lp:~ivoks/charms/trusty/elasticsearch/refresh into lp:charms/trusty/elasticsearch

Proposed by Ante Karamatić
Status: Needs review
Proposed branch: lp:~ivoks/charms/trusty/elasticsearch/refresh
Merge into: lp:charms/trusty/elasticsearch
Diff against target: 5480 lines (+3717/-626)
42 files modified
README.md (+3/-3)
charm-helpers.yaml (+1/-0)
config.yaml (+1/-1)
hooks/charmhelpers/__init__.py (+36/-0)
hooks/charmhelpers/contrib/__init__.py (+13/-0)
hooks/charmhelpers/contrib/ansible/__init__.py (+86/-5)
hooks/charmhelpers/contrib/templating/__init__.py (+13/-0)
hooks/charmhelpers/contrib/templating/contexts.py (+25/-4)
hooks/charmhelpers/core/__init__.py (+13/-0)
hooks/charmhelpers/core/decorators.py (+55/-0)
hooks/charmhelpers/core/files.py (+43/-0)
hooks/charmhelpers/core/fstab.py (+28/-12)
hooks/charmhelpers/core/hookenv.py (+594/-58)
hooks/charmhelpers/core/host.py (+677/-146)
hooks/charmhelpers/core/host_factory/centos.py (+72/-0)
hooks/charmhelpers/core/host_factory/ubuntu.py (+88/-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 (+14/-0)
hooks/charmhelpers/core/services/base.py (+57/-19)
hooks/charmhelpers/core/services/helpers.py (+64/-13)
hooks/charmhelpers/core/strutils.py (+123/-0)
hooks/charmhelpers/core/sysctl.py (+26/-6)
hooks/charmhelpers/core/templating.py (+44/-11)
hooks/charmhelpers/core/unitdata.py (+518/-0)
hooks/charmhelpers/fetch/__init__.py (+58/-275)
hooks/charmhelpers/fetch/archiveurl.py (+75/-18)
hooks/charmhelpers/fetch/bzrurl.py (+52/-26)
hooks/charmhelpers/fetch/centos.py (+171/-0)
hooks/charmhelpers/fetch/giturl.py (+45/-20)
hooks/charmhelpers/fetch/snap.py (+122/-0)
hooks/charmhelpers/fetch/ubuntu.py (+364/-0)
hooks/charmhelpers/osplatform.py (+25/-0)
hooks/charmhelpers/payload/__init__.py (+14/-0)
hooks/charmhelpers/payload/execd.py (+17/-2)
hooks/hooks.py (+1/-1)
metadata.yaml (+3/-0)
tasks/install-elasticsearch.yml (+1/-1)
templates/elasticsearch.yml (+2/-3)
unit_tests/test_hooks.py (+2/-2)
To merge this branch: bzr merge lp:~ivoks/charms/trusty/elasticsearch/refresh
Reviewer Review Type Date Requested Status
charmers Pending
Review via email: mp+321247@code.launchpad.net

Description of the change

Elasticsearch charm refreshment. Adds support for newer versions of Elasticsearch, drops obsolete workarounds and adds support for 16.04.

To post a comment you must log in.
Revision history for this message
Ante Karamatić (ivoks) wrote :

You might want to drop changes from README.md. This charm obviously doesn't work with outdated Kibana charm, so new Kibana charm is being proposed. That charm is available at https://code.launchpad.net/~ivoks/charms/xenial/kibana/kibana-xenial and is pending charm publication in charmstore.

57. By Ante Karamatić

Allow elasticsearch to start when there's only one unit

discovery.zen.ping.unicast.hosts cannot be empty, so it should not
be configured when there are no peers

58. By Ante Karamatić

len() doesn't exist in ansible

Check if relations.peer is not empty instead

Unmerged revisions

58. By Ante Karamatić

len() doesn't exist in ansible

Check if relations.peer is not empty instead

57. By Ante Karamatić

Allow elasticsearch to start when there's only one unit

discovery.zen.ping.unicast.hosts cannot be empty, so it should not
be configured when there are no peers

56. By Ante Karamatić

Update README.md

55. By Ante Karamatić

Drop support for precise

Precise will EOL in few weeks and it doesn't have python3. There's
no justification to invest time to workaround python2/python3 problem.

54. By Ante Karamatić

Adjust tests for new charmhelpers

53. By Ante Karamatić

Drop obsolete config options in the template

Removed two config options that are not needed any more and
only break newer versions of Elasticsearch

52. By Ante Karamatić

Allow distribution to select JRE

By switching to default-jre-headless we make sure available JRE
is always installable. This doesn't resolve the requirement
on the user to know which Elasticsearch works with wich version
of JRE

51. By Ante Karamatić

Add series in metadata.yaml and add support for xenial

Previous 2 commits enable successful deployment of Elasticsearch
on Xenial and this commit only declares that in metadata

50. By Ante Karamatić

Switch hooks to python3

This is a requirement for deployment in Ubuntu 16.04 or later

49. By Ante Karamatić

Change default repo for elasticsearch

New repo is in line with upstream's instructions and also pulls in
latest 5.x version, instead of old 2.x

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'README.md'
--- README.md 2014-10-31 03:43:31 +0000
+++ README.md 2017-05-11 14:56:57 +0000
@@ -14,11 +14,11 @@
1414
15You can simply deploy one node with:15You can simply deploy one node with:
1616
17 juju deploy elasticsearch17 juju deploy cs:~ivoks/elasticsearch
1818
19You can also deploy and relate the Kibana dashboard:19You can also deploy and relate the Kibana dashboard:
2020
21 juju deploy kibana21 juju deploy cs:~ivoks/kibana
22 juju add-relation kibana elasticsearch22 juju add-relation kibana elasticsearch
23 juju expose kibana23 juju expose kibana
2424
@@ -29,7 +29,7 @@
2929
30Deploy three or more units with:30Deploy three or more units with:
3131
32 juju deploy -n3 elasticsearch32 juju deploy -n3 cs:~ivoks/elasticsearch
3333
34And when they have started you can inspect the cluster health:34And when they have started you can inspect the cluster health:
3535
3636
=== modified file 'charm-helpers.yaml'
--- charm-helpers.yaml 2014-04-08 14:58:22 +0000
+++ charm-helpers.yaml 2017-05-11 14:56:57 +0000
@@ -6,3 +6,4 @@
6 - contrib.ansible|inc=*6 - contrib.ansible|inc=*
7 - contrib.templating.contexts7 - contrib.templating.contexts
8 - payload.execd|inc=*8 - payload.execd|inc=*
9 - osplatform
910
=== modified file 'config.yaml'
--- config.yaml 2016-03-08 16:38:17 +0000
+++ config.yaml 2017-05-11 14:56:57 +0000
@@ -1,7 +1,7 @@
1options:1options:
2 apt-repository:2 apt-repository:
3 type: string3 type: string
4 default: "deb http://packages.elastic.co/elasticsearch/2.x/debian stable main"4 default: "deb https://artifacts.elastic.co/packages/5.x/apt stable main"
5 description: |5 description: |
6 A deb-line for the apt archive which contains the elasticsearch package.6 A deb-line for the apt archive which contains the elasticsearch package.
7 This is necessary until elasticsearch gets into the debian/ubuntu archives.7 This is necessary until elasticsearch gets into the debian/ubuntu archives.
88
=== modified file 'hooks/charmhelpers/__init__.py'
--- hooks/charmhelpers/__init__.py 2014-02-06 12:54:59 +0000
+++ hooks/charmhelpers/__init__.py 2017-05-11 14:56:57 +0000
@@ -0,0 +1,36 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15# Bootstrap charm-helpers, installing its dependencies if necessary using
16# only standard libraries.
17import subprocess
18import sys
19
20try:
21 import six # flake8: noqa
22except ImportError:
23 if sys.version_info.major == 2:
24 subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
25 else:
26 subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
27 import six # flake8: noqa
28
29try:
30 import yaml # flake8: noqa
31except ImportError:
32 if sys.version_info.major == 2:
33 subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
34 else:
35 subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
36 import yaml # flake8: noqa
037
=== modified file 'hooks/charmhelpers/contrib/__init__.py'
--- hooks/charmhelpers/contrib/__init__.py 2014-02-06 12:54:59 +0000
+++ hooks/charmhelpers/contrib/__init__.py 2017-05-11 14:56:57 +0000
@@ -0,0 +1,13 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
014
=== modified file 'hooks/charmhelpers/contrib/ansible/__init__.py'
--- hooks/charmhelpers/contrib/ansible/__init__.py 2014-10-24 16:35:00 +0000
+++ hooks/charmhelpers/contrib/ansible/__init__.py 2017-05-11 14:56:57 +0000
@@ -1,3 +1,17 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
1# Copyright 2013 Canonical Ltd.15# Copyright 2013 Canonical Ltd.
2#16#
3# Authors:17# Authors:
@@ -59,9 +73,36 @@
59.. _playbooks: http://www.ansibleworks.com/docs/playbooks.html73.. _playbooks: http://www.ansibleworks.com/docs/playbooks.html
60.. _modules: http://www.ansibleworks.com/docs/modules.html74.. _modules: http://www.ansibleworks.com/docs/modules.html
6175
76A further feature os the ansible hooks is to provide a light weight "action"
77scripting tool. This is a decorator that you apply to a function, and that
78function can now receive cli args, and can pass extra args to the playbook.
79
80e.g.
81
82
83@hooks.action()
84def some_action(amount, force="False"):
85 "Usage: some-action AMOUNT [force=True]" # <-- shown on error
86 # process the arguments
87 # do some calls
88 # return extra-vars to be passed to ansible-playbook
89 return {
90 'amount': int(amount),
91 'type': force,
92 }
93
94You can now create a symlink to hooks.py that can be invoked like a hook, but
95with cli params:
96
97# link actions/some-action to hooks/hooks.py
98
99actions/some-action amount=10 force=true
100
62"""101"""
63import os102import os
103import stat
64import subprocess104import subprocess
105import functools
65106
66import charmhelpers.contrib.templating.contexts107import charmhelpers.contrib.templating.contexts
67import charmhelpers.core.host108import charmhelpers.core.host
@@ -96,12 +137,13 @@
96 hosts_file.write('localhost ansible_connection=local')137 hosts_file.write('localhost ansible_connection=local')
97138
98139
99def apply_playbook(playbook, tags=None):140def apply_playbook(playbook, tags=None, extra_vars=None):
100 tags = tags or []141 tags = tags or []
101 tags = ",".join(tags)142 tags = ",".join(tags)
102 charmhelpers.contrib.templating.contexts.juju_state_to_yaml(143 charmhelpers.contrib.templating.contexts.juju_state_to_yaml(
103 ansible_vars_path, namespace_separator='__',144 ansible_vars_path, namespace_separator='__',
104 allow_hyphens_in_keys=False)145 allow_hyphens_in_keys=False, mode=(stat.S_IRUSR | stat.S_IWUSR))
146
105 # we want ansible's log output to be unbuffered147 # we want ansible's log output to be unbuffered
106 env = os.environ.copy()148 env = os.environ.copy()
107 env['PYTHONUNBUFFERED'] = "1"149 env['PYTHONUNBUFFERED'] = "1"
@@ -113,6 +155,9 @@
113 ]155 ]
114 if tags:156 if tags:
115 call.extend(['--tags', '{}'.format(tags)])157 call.extend(['--tags', '{}'.format(tags)])
158 if extra_vars:
159 extra = ["%s=%s" % (k, v) for k, v in extra_vars.items()]
160 call.extend(['--extra-vars', " ".join(extra)])
116 subprocess.check_call(call, env=env)161 subprocess.check_call(call, env=env)
117162
118163
@@ -156,16 +201,52 @@
156 """Register any hooks handled by ansible."""201 """Register any hooks handled by ansible."""
157 super(AnsibleHooks, self).__init__()202 super(AnsibleHooks, self).__init__()
158203
204 self._actions = {}
159 self.playbook_path = playbook_path205 self.playbook_path = playbook_path
160206
161 default_hooks = default_hooks or []207 default_hooks = default_hooks or []
162 noop = lambda *args, **kwargs: None208
209 def noop(*args, **kwargs):
210 pass
211
163 for hook in default_hooks:212 for hook in default_hooks:
164 self.register(hook, noop)213 self.register(hook, noop)
165214
215 def register_action(self, name, function):
216 """Register a hook"""
217 self._actions[name] = function
218
166 def execute(self, args):219 def execute(self, args):
167 """Execute the hook followed by the playbook using the hook as tag."""220 """Execute the hook followed by the playbook using the hook as tag."""
168 super(AnsibleHooks, self).execute(args)
169 hook_name = os.path.basename(args[0])221 hook_name = os.path.basename(args[0])
222 extra_vars = None
223 if hook_name in self._actions:
224 extra_vars = self._actions[hook_name](args[1:])
225 else:
226 super(AnsibleHooks, self).execute(args)
227
170 charmhelpers.contrib.ansible.apply_playbook(228 charmhelpers.contrib.ansible.apply_playbook(
171 self.playbook_path, tags=[hook_name])229 self.playbook_path, tags=[hook_name], extra_vars=extra_vars)
230
231 def action(self, *action_names):
232 """Decorator, registering them as actions"""
233 def action_wrapper(decorated):
234
235 @functools.wraps(decorated)
236 def wrapper(argv):
237 kwargs = dict(arg.split('=') for arg in argv)
238 try:
239 return decorated(**kwargs)
240 except TypeError as e:
241 if decorated.__doc__:
242 e.args += (decorated.__doc__,)
243 raise
244
245 self.register_action(decorated.__name__, wrapper)
246 if '_' in decorated.__name__:
247 self.register_action(
248 decorated.__name__.replace('_', '-'), wrapper)
249
250 return wrapper
251
252 return action_wrapper
172253
=== modified file 'hooks/charmhelpers/contrib/templating/__init__.py'
--- hooks/charmhelpers/contrib/templating/__init__.py 2014-02-06 12:54:59 +0000
+++ hooks/charmhelpers/contrib/templating/__init__.py 2017-05-11 14:56:57 +0000
@@ -0,0 +1,13 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
014
=== modified file 'hooks/charmhelpers/contrib/templating/contexts.py'
--- hooks/charmhelpers/contrib/templating/contexts.py 2014-10-24 16:35:00 +0000
+++ hooks/charmhelpers/contrib/templating/contexts.py 2017-05-11 14:56:57 +0000
@@ -1,3 +1,17 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
1# Copyright 2013 Canonical Ltd.15# Copyright 2013 Canonical Ltd.
2#16#
3# Authors:17# Authors:
@@ -6,6 +20,8 @@
6import os20import os
7import yaml21import yaml
822
23import six
24
9import charmhelpers.core.hookenv25import charmhelpers.core.hookenv
1026
1127
@@ -62,7 +78,7 @@
6278
6379
64def juju_state_to_yaml(yaml_path, namespace_separator=':',80def juju_state_to_yaml(yaml_path, namespace_separator=':',
65 allow_hyphens_in_keys=True):81 allow_hyphens_in_keys=True, mode=None):
66 """Update the juju config and state in a yaml file.82 """Update the juju config and state in a yaml file.
6783
68 This includes any current relation-get data, and the charm84 This includes any current relation-get data, and the charm
@@ -92,9 +108,9 @@
92108
93 # Don't use non-standard tags for unicode which will not109 # Don't use non-standard tags for unicode which will not
94 # work when salt uses yaml.load_safe.110 # work when salt uses yaml.load_safe.
95 yaml.add_representer(unicode, lambda dumper,111 yaml.add_representer(six.text_type,
96 value: dumper.represent_scalar(112 lambda dumper, value: dumper.represent_scalar(
97 u'tag:yaml.org,2002:str', value))113 six.u('tag:yaml.org,2002:str'), value))
98114
99 yaml_dir = os.path.dirname(yaml_path)115 yaml_dir = os.path.dirname(yaml_path)
100 if not os.path.exists(yaml_dir):116 if not os.path.exists(yaml_dir):
@@ -104,8 +120,13 @@
104 with open(yaml_path, "r") as existing_vars_file:120 with open(yaml_path, "r") as existing_vars_file:
105 existing_vars = yaml.load(existing_vars_file.read())121 existing_vars = yaml.load(existing_vars_file.read())
106 else:122 else:
123 with open(yaml_path, "w+"):
124 pass
107 existing_vars = {}125 existing_vars = {}
108126
127 if mode is not None:
128 os.chmod(yaml_path, mode)
129
109 if not allow_hyphens_in_keys:130 if not allow_hyphens_in_keys:
110 config = dict_keys_without_hyphens(config)131 config = dict_keys_without_hyphens(config)
111 existing_vars.update(config)132 existing_vars.update(config)
112133
=== modified file 'hooks/charmhelpers/core/__init__.py'
--- hooks/charmhelpers/core/__init__.py 2014-02-06 12:54:59 +0000
+++ hooks/charmhelpers/core/__init__.py 2017-05-11 14:56:57 +0000
@@ -0,0 +1,13 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
014
=== added file 'hooks/charmhelpers/core/decorators.py'
--- hooks/charmhelpers/core/decorators.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/decorators.py 2017-05-11 14:56:57 +0000
@@ -0,0 +1,55 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15#
16# Copyright 2014 Canonical Ltd.
17#
18# Authors:
19# Edward Hope-Morley <opentastic@gmail.com>
20#
21
22import time
23
24from charmhelpers.core.hookenv import (
25 log,
26 INFO,
27)
28
29
30def retry_on_exception(num_retries, base_delay=0, exc_type=Exception):
31 """If the decorated function raises exception exc_type, allow num_retries
32 retry attempts before raise the exception.
33 """
34 def _retry_on_exception_inner_1(f):
35 def _retry_on_exception_inner_2(*args, **kwargs):
36 retries = num_retries
37 multiplier = 1
38 while True:
39 try:
40 return f(*args, **kwargs)
41 except exc_type:
42 if not retries:
43 raise
44
45 delay = base_delay * multiplier
46 multiplier += 1
47 log("Retrying '%s' %d more times (delay=%s)" %
48 (f.__name__, retries, delay), level=INFO)
49 retries -= 1
50 if delay:
51 time.sleep(delay)
52
53 return _retry_on_exception_inner_2
54
55 return _retry_on_exception_inner_1
056
=== 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-05-11 14:56:57 +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 2014-10-24 16:35:00 +0000
+++ hooks/charmhelpers/core/fstab.py 2017-05-11 14:56:57 +0000
@@ -1,12 +1,27 @@
1#!/usr/bin/env python1#!/usr/bin/env python
2# -*- coding: utf-8 -*-2# -*- coding: utf-8 -*-
33
4# Copyright 2014-2015 Canonical Limited.
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17
18import io
19import os
20
4__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'21__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
522
6import os23
724class Fstab(io.FileIO):
8
9class Fstab(file):
10 """This class extends file in order to implement a file reader/writer25 """This class extends file in order to implement a file reader/writer
11 for file `/etc/fstab`26 for file `/etc/fstab`
12 """27 """
@@ -24,8 +39,8 @@
24 options = "defaults"39 options = "defaults"
2540
26 self.options = options41 self.options = options
27 self.d = d42 self.d = int(d)
28 self.p = p43 self.p = int(p)
2944
30 def __eq__(self, o):45 def __eq__(self, o):
31 return str(self) == str(o)46 return str(self) == str(o)
@@ -45,7 +60,7 @@
45 self._path = path60 self._path = path
46 else:61 else:
47 self._path = self.DEFAULT_PATH62 self._path = self.DEFAULT_PATH
48 file.__init__(self, self._path, 'r+')63 super(Fstab, self).__init__(self._path, 'rb+')
4964
50 def _hydrate_entry(self, line):65 def _hydrate_entry(self, line):
51 # NOTE: use split with no arguments to split on any66 # NOTE: use split with no arguments to split on any
@@ -58,8 +73,9 @@
58 def entries(self):73 def entries(self):
59 self.seek(0)74 self.seek(0)
60 for line in self.readlines():75 for line in self.readlines():
76 line = line.decode('us-ascii')
61 try:77 try:
62 if not line.startswith("#"):78 if line.strip() and not line.strip().startswith("#"):
63 yield self._hydrate_entry(line)79 yield self._hydrate_entry(line)
64 except ValueError:80 except ValueError:
65 pass81 pass
@@ -75,18 +91,18 @@
75 if self.get_entry_by_attr('device', entry.device):91 if self.get_entry_by_attr('device', entry.device):
76 return False92 return False
7793
78 self.write(str(entry) + '\n')94 self.write((str(entry) + '\n').encode('us-ascii'))
79 self.truncate()95 self.truncate()
80 return entry96 return entry
8197
82 def remove_entry(self, entry):98 def remove_entry(self, entry):
83 self.seek(0)99 self.seek(0)
84100
85 lines = self.readlines()101 lines = [l.decode('us-ascii') for l in self.readlines()]
86102
87 found = False103 found = False
88 for index, line in enumerate(lines):104 for index, line in enumerate(lines):
89 if not line.startswith("#"):105 if line.strip() and not line.strip().startswith("#"):
90 if self._hydrate_entry(line) == entry:106 if self._hydrate_entry(line) == entry:
91 found = True107 found = True
92 break108 break
@@ -97,7 +113,7 @@
97 lines.remove(line)113 lines.remove(line)
98114
99 self.seek(0)115 self.seek(0)
100 self.write(''.join(lines))116 self.write(''.join(lines).encode('us-ascii'))
101 self.truncate()117 self.truncate()
102 return True118 return True
103119
104120
=== modified file 'hooks/charmhelpers/core/hookenv.py'
--- hooks/charmhelpers/core/hookenv.py 2014-10-24 16:35:00 +0000
+++ hooks/charmhelpers/core/hookenv.py 2017-05-11 14:56:57 +0000
@@ -1,17 +1,43 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
1"Interactions with the Juju environment"15"Interactions with the Juju environment"
2# Copyright 2013 Canonical Ltd.16# Copyright 2013 Canonical Ltd.
3#17#
4# Authors:18# Authors:
5# Charm Helpers Developers <juju@lists.ubuntu.com>19# Charm Helpers Developers <juju@lists.ubuntu.com>
620
21from __future__ import print_function
22import copy
23from distutils.version import LooseVersion
24from functools import wraps
25import glob
7import os26import os
8import json27import json
9import yaml28import yaml
10import subprocess29import subprocess
11import sys30import sys
12import UserDict31import errno
32import tempfile
13from subprocess import CalledProcessError33from subprocess import CalledProcessError
1434
35import six
36if not six.PY3:
37 from UserDict import UserDict
38else:
39 from collections import UserDict
40
15CRITICAL = "CRITICAL"41CRITICAL = "CRITICAL"
16ERROR = "ERROR"42ERROR = "ERROR"
17WARNING = "WARNING"43WARNING = "WARNING"
@@ -35,15 +61,18 @@
3561
36 will cache the result of unit_get + 'test' for future calls.62 will cache the result of unit_get + 'test' for future calls.
37 """63 """
64 @wraps(func)
38 def wrapper(*args, **kwargs):65 def wrapper(*args, **kwargs):
39 global cache66 global cache
40 key = str((func, args, kwargs))67 key = str((func, args, kwargs))
41 try:68 try:
42 return cache[key]69 return cache[key]
43 except KeyError:70 except KeyError:
44 res = func(*args, **kwargs)71 pass # Drop out of the exception handler scope.
45 cache[key] = res72 res = func(*args, **kwargs)
46 return res73 cache[key] = res
74 return res
75 wrapper._wrapped = func
47 return wrapper76 return wrapper
4877
4978
@@ -63,16 +92,29 @@
63 command = ['juju-log']92 command = ['juju-log']
64 if level:93 if level:
65 command += ['-l', level]94 command += ['-l', level]
95 if not isinstance(message, six.string_types):
96 message = repr(message)
66 command += [message]97 command += [message]
67 subprocess.call(command)98 # Missing juju-log should not cause failures in unit tests
6899 # Send log output to stderr
69100 try:
70class Serializable(UserDict.IterableUserDict):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
110
111
112class Serializable(UserDict):
71 """Wrapper, an object that can be serialized to yaml or json"""113 """Wrapper, an object that can be serialized to yaml or json"""
72114
73 def __init__(self, obj):115 def __init__(self, obj):
74 # wrap the object116 # wrap the object
75 UserDict.IterableUserDict.__init__(self)117 UserDict.__init__(self)
76 self.data = obj118 self.data = obj
77119
78 def __getattr__(self, attr):120 def __getattr__(self, attr):
@@ -130,9 +172,19 @@
130 return os.environ.get('JUJU_RELATION', None)172 return os.environ.get('JUJU_RELATION', None)
131173
132174
133def relation_id():175@cached
134 """The relation ID for the current relation hook"""176def relation_id(relation_name=None, service_or_unit=None):
135 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')
136188
137189
138def local_unit():190def local_unit():
@@ -142,7 +194,7 @@
142194
143def remote_unit():195def remote_unit():
144 """The remote unit for the current relation hook"""196 """The remote unit for the current relation hook"""
145 return os.environ['JUJU_REMOTE_UNIT']197 return os.environ.get('JUJU_REMOTE_UNIT', None)
146198
147199
148def service_name():200def service_name():
@@ -150,9 +202,20 @@
150 return local_unit().split('/')[0]202 return local_unit().split('/')[0]
151203
152204
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
153def hook_name():216def hook_name():
154 """The name of the currently executing hook"""217 """The name of the currently executing hook"""
155 return os.path.basename(sys.argv[0])218 return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0]))
156219
157220
158class Config(dict):221class Config(dict):
@@ -202,23 +265,7 @@
202 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)265 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
203 if os.path.exists(self.path):266 if os.path.exists(self.path):
204 self.load_previous()267 self.load_previous()
205268 atexit(self._implicit_save)
206 def __getitem__(self, key):
207 """For regular dict lookups, check the current juju config first,
208 then the previous (saved) copy. This ensures that user-saved values
209 will be returned by a dict lookup.
210
211 """
212 try:
213 return dict.__getitem__(self, key)
214 except KeyError:
215 return (self._prev_dict or {})[key]
216
217 def keys(self):
218 prev_keys = []
219 if self._prev_dict is not None:
220 prev_keys = self._prev_dict.keys()
221 return list(set(prev_keys + dict.keys(self)))
222269
223 def load_previous(self, path=None):270 def load_previous(self, path=None):
224 """Load previous copy of config from disk.271 """Load previous copy of config from disk.
@@ -237,6 +284,9 @@
237 self.path = path or self.path284 self.path = path or self.path
238 with open(self.path) as f:285 with open(self.path) as f:
239 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
240290
241 def changed(self, key):291 def changed(self, key):
242 """Return True if the current value for this key is different from292 """Return True if the current value for this key is different from
@@ -268,13 +318,13 @@
268 instance.318 instance.
269319
270 """320 """
271 if self._prev_dict:
272 for k, v in self._prev_dict.iteritems():
273 if k not in self:
274 self[k] = v
275 with open(self.path, 'w') as f:321 with open(self.path, 'w') as f:
276 json.dump(self, f)322 json.dump(self, f)
277323
324 def _implicit_save(self):
325 if self.implicit_save:
326 self.save()
327
278328
279@cached329@cached
280def config(scope=None):330def config(scope=None):
@@ -282,9 +332,12 @@
282 config_cmd_line = ['config-get']332 config_cmd_line = ['config-get']
283 if scope is not None:333 if scope is not None:
284 config_cmd_line.append(scope)334 config_cmd_line.append(scope)
335 else:
336 config_cmd_line.append('--all')
285 config_cmd_line.append('--format=json')337 config_cmd_line.append('--format=json')
286 try:338 try:
287 config_data = json.loads(subprocess.check_output(config_cmd_line))339 config_data = json.loads(
340 subprocess.check_output(config_cmd_line).decode('UTF-8'))
288 if scope is not None:341 if scope is not None:
289 return config_data342 return config_data
290 return Config(config_data)343 return Config(config_data)
@@ -303,10 +356,10 @@
303 if unit:356 if unit:
304 _args.append(unit)357 _args.append(unit)
305 try:358 try:
306 return json.loads(subprocess.check_output(_args))359 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
307 except ValueError:360 except ValueError:
308 return None361 return None
309 except CalledProcessError, e:362 except CalledProcessError as e:
310 if e.returncode == 2:363 if e.returncode == 2:
311 return None364 return None
312 raise365 raise
@@ -316,18 +369,49 @@
316 """Set relation information for the current unit"""369 """Set relation information for the current unit"""
317 relation_settings = relation_settings if relation_settings else {}370 relation_settings = relation_settings if relation_settings else {}
318 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)
319 if relation_id is not None:374 if relation_id is not None:
320 relation_cmd_line.extend(('-r', relation_id))375 relation_cmd_line.extend(('-r', relation_id))
321 for k, v in (relation_settings.items() + kwargs.items()):376 settings = relation_settings.copy()
322 if v is None:377 settings.update(kwargs)
323 relation_cmd_line.append('{}='.format(k))378 for key, value in settings.items():
324 else:379 # Force value to be a string: it always should, but some call
325 relation_cmd_line.append('{}={}'.format(k, v))380 # sites pass in things like dicts or numbers.
326 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)
327 # Flush cache of any relation-gets for local unit400 # Flush cache of any relation-gets for local unit
328 flush(local_unit())401 flush(local_unit())
329402
330403
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
331@cached415@cached
332def relation_ids(reltype=None):416def relation_ids(reltype=None):
333 """A list of relation_ids"""417 """A list of relation_ids"""
@@ -335,7 +419,8 @@
335 relid_cmd_line = ['relation-ids', '--format=json']419 relid_cmd_line = ['relation-ids', '--format=json']
336 if reltype is not None:420 if reltype is not None:
337 relid_cmd_line.append(reltype)421 relid_cmd_line.append(reltype)
338 return json.loads(subprocess.check_output(relid_cmd_line)) or []422 return json.loads(
423 subprocess.check_output(relid_cmd_line).decode('UTF-8')) or []
339 return []424 return []
340425
341426
@@ -346,7 +431,8 @@
346 units_cmd_line = ['relation-list', '--format=json']431 units_cmd_line = ['relation-list', '--format=json']
347 if relid is not None:432 if relid is not None:
348 units_cmd_line.extend(('-r', relid))433 units_cmd_line.extend(('-r', relid))
349 return json.loads(subprocess.check_output(units_cmd_line)) or []434 return json.loads(
435 subprocess.check_output(units_cmd_line).decode('UTF-8')) or []
350436
351437
352@cached438@cached
@@ -386,21 +472,101 @@
386472
387473
388@cached474@cached
475def metadata():
476 """Get the current charm metadata.yaml contents as a python object"""
477 with open(os.path.join(charm_dir(), 'metadata.yaml')) as md:
478 return yaml.safe_load(md)
479
480
481@cached
389def relation_types():482def relation_types():
390 """Get a list of relation types supported by this charm"""483 """Get a list of relation types supported by this charm"""
391 charmdir = os.environ.get('CHARM_DIR', '')
392 mdf = open(os.path.join(charmdir, 'metadata.yaml'))
393 md = yaml.safe_load(mdf)
394 rel_types = []484 rel_types = []
485 md = metadata()
395 for key in ('provides', 'requires', 'peers'):486 for key in ('provides', 'requires', 'peers'):
396 section = md.get(key)487 section = md.get(key)
397 if section:488 if section:
398 rel_types.extend(section.keys())489 rel_types.extend(section.keys())
399 mdf.close()
400 return rel_types490 return rel_types
401491
402492
403@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
564def charm_name():
565 """Get the name of the current charm as is specified on metadata.yaml"""
566 return metadata().get('name')
567
568
569@cached
404def relations():570def relations():
405 """Get a nested dictionary of relation data for all related units"""571 """Get a nested dictionary of relation data for all related units"""
406 rels = {}572 rels = {}
@@ -450,21 +616,72 @@
450 subprocess.check_call(_args)616 subprocess.check_call(_args)
451617
452618
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
453@cached633@cached
454def unit_get(attribute):634def unit_get(attribute):
455 """Get the unit ID for the remote unit"""635 """Get the unit ID for the remote unit"""
456 _args = ['unit-get', '--format=json', attribute]636 _args = ['unit-get', '--format=json', attribute]
457 try:637 try:
458 return json.loads(subprocess.check_output(_args))638 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
459 except ValueError:639 except ValueError:
460 return None640 return None
461641
462642
643def unit_public_ip():
644 """Get this unit's public IP address"""
645 return unit_get('public-address')
646
647
463def unit_private_ip():648def unit_private_ip():
464 """Get this unit's private IP address"""649 """Get this unit's private IP address"""
465 return unit_get('private-address')650 return unit_get('private-address')
466651
467652
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
468class UnregisteredHookError(Exception):685class UnregisteredHookError(Exception):
469 """Raised when an undefined hook is called"""686 """Raised when an undefined hook is called"""
470 pass687 pass
@@ -492,10 +709,14 @@
492 hooks.execute(sys.argv)709 hooks.execute(sys.argv)
493 """710 """
494711
495 def __init__(self, config_save=True):712 def __init__(self, config_save=None):
496 super(Hooks, self).__init__()713 super(Hooks, self).__init__()
497 self._hooks = {}714 self._hooks = {}
498 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
499720
500 def register(self, name, function):721 def register(self, name, function):
501 """Register a hook"""722 """Register a hook"""
@@ -503,13 +724,16 @@
503724
504 def execute(self, args):725 def execute(self, args):
505 """Execute a registered hook based on args[0]"""726 """Execute a registered hook based on args[0]"""
727 _run_atstart()
506 hook_name = os.path.basename(args[0])728 hook_name = os.path.basename(args[0])
507 if hook_name in self._hooks:729 if hook_name in self._hooks:
508 self._hooks[hook_name]()730 try:
509 if self._config_save:731 self._hooks[hook_name]()
510 cfg = config()732 except SystemExit as x:
511 if cfg.implicit_save:733 if x.code is None or x.code == 0:
512 cfg.save()734 _run_atexit()
735 raise
736 _run_atexit()
513 else:737 else:
514 raise UnregisteredHookError(hook_name)738 raise UnregisteredHookError(hook_name)
515739
@@ -530,3 +754,315 @@
530def charm_dir():754def charm_dir():
531 """Return the root directory of the current charm"""755 """Return the root directory of the current charm"""
532 return os.environ.get('CHARM_DIR')756 return os.environ.get('CHARM_DIR')
757
758
759@cached
760def action_get(key=None):
761 """Gets the value of an action parameter, or all key/value param pairs"""
762 cmd = ['action-get']
763 if key is not None:
764 cmd.append(key)
765 cmd.append('--format=json')
766 action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
767 return action_data
768
769
770def action_set(values):
771 """Sets the values to be returned after the action finishes"""
772 cmd = ['action-set']
773 for k, v in list(values.items()):
774 cmd.append('{}={}'.format(k, v))
775 subprocess.check_call(cmd)
776
777
778def action_fail(message):
779 """Sets the action status to failed and sets the error message.
780
781 The results set by action_set are preserved."""
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')
5331069
=== modified file 'hooks/charmhelpers/core/host.py'
--- hooks/charmhelpers/core/host.py 2014-10-24 16:35:00 +0000
+++ hooks/charmhelpers/core/host.py 2017-05-11 14:56:57 +0000
@@ -1,3 +1,17 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
1"""Tools for working with the host system"""15"""Tools for working with the host system"""
2# Copyright 2012 Canonical Ltd.16# Copyright 2012 Canonical Ltd.
3#17#
@@ -8,80 +22,328 @@
8import os22import os
9import re23import re
10import pwd24import pwd
25import glob
11import grp26import grp
12import random27import random
13import string28import string
14import subprocess29import subprocess
15import hashlib30import hashlib
31import functools
32import itertools
33import six
34
16from contextlib import contextmanager35from contextlib import contextmanager
17
18from collections import OrderedDict36from collections import OrderedDict
1937from .hookenv import log
20from hookenv import log38from .fstab import Fstab
21from fstab import Fstab39from charmhelpers.osplatform import get_platform
2240
2341__platform__ = get_platform()
24def service_start(service_name):42if __platform__ == "ubuntu":
25 """Start a system service"""43 from charmhelpers.core.host_factory.ubuntu import (
26 return service('start', service_name)44 service_available,
2745 add_new_group,
2846 lsb_release,
29def service_stop(service_name):47 cmp_pkgrevno,
30 """Stop a system service"""48 CompareHostReleases,
31 return service('stop', service_name)49 ) # flake8: noqa -- ignore F401 for this import
3250elif __platform__ == "centos":
3351 from charmhelpers.core.host_factory.centos import (
34def service_restart(service_name):52 service_available,
35 """Restart a system service"""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 """
36 return service('restart', service_name)137 return service('restart', service_name)
37138
38139
39def service_reload(service_name, restart_on_failure=False):140def service_reload(service_name, restart_on_failure=False, **kwargs):
40 """Reload a system service, optionally falling back to restart if141 """Reload a system service, optionally falling back to restart if
41 reload fails"""142 reload fails.
42 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)
43 if not service_result and restart_on_failure:168 if not service_result and restart_on_failure:
44 service_result = service('restart', service_name)169 service_result = service('restart', service_name, **kwargs)
45 return service_result170 return service_result
46171
47172
48def service(action, service_name):173def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d",
49 """Control a system service"""174 **kwargs):
50 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 elif os.path.exists(upstart_file):
196 override_path = os.path.join(
197 init_dir, '{}.override'.format(service_name))
198 with open(override_path, 'w') as fh:
199 fh.write("manual\n")
200 elif os.path.exists(sysv_file):
201 subprocess.check_call(["update-rc.d", service_name, "disable"])
202 else:
203 raise ValueError(
204 "Unable to detect {0} as SystemD, Upstart {1} or"
205 " SysV {2}".format(
206 service_name, upstart_file, sysv_file))
207 return stopped
208
209
210def service_resume(service_name, init_dir="/etc/init",
211 initd_dir="/etc/init.d", **kwargs):
212 """Resume a system service.
213
214 Reenable starting again at boot. Start the service.
215
216 :param service_name: the name of the service to resume
217 :param init_dir: the path to the init dir
218 :param initd dir: the path to the initd dir
219 :param **kwargs: additional parameters to pass to the init system when
220 managing services. These will be passed as key=value
221 parameters to the init system's commandline. kwargs
222 are ignored for systemd enabled systems.
223 """
224 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
225 sysv_file = os.path.join(initd_dir, service_name)
226 if init_is_systemd():
227 service('enable', service_name)
228 elif os.path.exists(upstart_file):
229 override_path = os.path.join(
230 init_dir, '{}.override'.format(service_name))
231 if os.path.exists(override_path):
232 os.unlink(override_path)
233 elif os.path.exists(sysv_file):
234 subprocess.check_call(["update-rc.d", service_name, "enable"])
235 else:
236 raise ValueError(
237 "Unable to detect {0} as SystemD, Upstart {1} or"
238 " SysV {2}".format(
239 service_name, upstart_file, sysv_file))
240 started = service_running(service_name, **kwargs)
241
242 if not started:
243 started = service_start(service_name, **kwargs)
244 return started
245
246
247def service(action, service_name, **kwargs):
248 """Control a system service.
249
250 :param action: the action to take on the service
251 :param service_name: the name of the service to perform th action on
252 :param **kwargs: additional params to be passed to the service command in
253 the form of key=value.
254 """
255 if init_is_systemd():
256 cmd = ['systemctl', action, service_name]
257 else:
258 cmd = ['service', service_name, action]
259 for key, value in six.iteritems(kwargs):
260 parameter = '%s=%s' % (key, value)
261 cmd.append(parameter)
51 return subprocess.call(cmd) == 0262 return subprocess.call(cmd) == 0
52263
53264
54def service_running(service):265_UPSTART_CONF = "/etc/init/{}.conf"
55 """Determine whether a system service is running"""266_INIT_D_CONF = "/etc/init.d/{}"
56 try:267
57 output = subprocess.check_output(['service', service, 'status'], stderr=subprocess.STDOUT)268
58 except subprocess.CalledProcessError:269def service_running(service_name, **kwargs):
59 return False270 """Determine whether a system service is running.
60 else:271
61 if ("start/running" in output or "is running" in output):272 :param service_name: the name of the service
62 return True273 :param **kwargs: additional args to pass to the service command. This is
63 else:274 used to pass additional key=value arguments to the
64 return False275 service command line for managing specific instance
65276 units (e.g. service ceph-osd status id=2). The kwargs
66277 are ignored in systemd services.
67def service_available(service_name):278 """
68 """Determine whether a system service is available"""279 if init_is_systemd():
69 try:280 return service('is-active', service_name)
70 subprocess.check_output(['service', service_name, 'status'], stderr=subprocess.STDOUT)281 else:
71 except subprocess.CalledProcessError as e:282 if os.path.exists(_UPSTART_CONF.format(service_name)):
72 return 'unrecognized service' not in e.output283 try:
73 else:284 cmd = ['status', service_name]
74 return True285 for key, value in six.iteritems(kwargs):
75286 parameter = '%s=%s' % (key, value)
76287 cmd.append(parameter)
77def adduser(username, password=None, shell='/bin/bash', system_user=False):288 output = subprocess.check_output(cmd,
78 """Add a user to the system"""289 stderr=subprocess.STDOUT).decode('UTF-8')
290 except subprocess.CalledProcessError:
291 return False
292 else:
293 # This works for upstart scripts where the 'service' command
294 # returns a consistent string to represent running
295 # 'start/running'
296 if ("start/running" in output or
297 "is running" in output or
298 "up and running" in output):
299 return True
300 elif os.path.exists(_INIT_D_CONF.format(service_name)):
301 # Check System V scripts init script return codes
302 return service('status', service_name)
303 return False
304
305
306SYSTEMD_SYSTEM = '/run/systemd/system'
307
308
309def init_is_systemd():
310 """Return True if the host system uses systemd, False otherwise."""
311 if lsb_release()['DISTRIB_CODENAME'] == 'trusty':
312 return False
313 return os.path.isdir(SYSTEMD_SYSTEM)
314
315
316def adduser(username, password=None, shell='/bin/bash',
317 system_user=False, primary_group=None,
318 secondary_groups=None, uid=None, home_dir=None):
319 """Add a user to the system.
320
321 Will log but otherwise succeed if the user already exists.
322
323 :param str username: Username to create
324 :param str password: Password for user; if ``None``, create a system user
325 :param str shell: The default shell for the user
326 :param bool system_user: Whether to create a login or system user
327 :param str primary_group: Primary group for user; defaults to username
328 :param list secondary_groups: Optional list of additional groups
329 :param int uid: UID for user being created
330 :param str home_dir: Home directory for user
331
332 :returns: The password database entry struct, as returned by `pwd.getpwnam`
333 """
79 try:334 try:
80 user_info = pwd.getpwnam(username)335 user_info = pwd.getpwnam(username)
81 log('user {0} already exists!'.format(username))336 log('user {0} already exists!'.format(username))
337 if uid:
338 user_info = pwd.getpwuid(int(uid))
339 log('user with uid {0} already exists!'.format(uid))
82 except KeyError:340 except KeyError:
83 log('creating user {0}'.format(username))341 log('creating user {0}'.format(username))
84 cmd = ['useradd']342 cmd = ['useradd']
343 if uid:
344 cmd.extend(['--uid', str(uid)])
345 if home_dir:
346 cmd.extend(['--home', str(home_dir)])
85 if system_user or password is None:347 if system_user or password is None:
86 cmd.append('--system')348 cmd.append('--system')
87 else:349 else:
@@ -90,32 +352,104 @@
90 '--shell', shell,352 '--shell', shell,
91 '--password', password,353 '--password', password,
92 ])354 ])
355 if not primary_group:
356 try:
357 grp.getgrnam(username)
358 primary_group = username # avoid "group exists" error
359 except KeyError:
360 pass
361 if primary_group:
362 cmd.extend(['-g', primary_group])
363 if secondary_groups:
364 cmd.extend(['-G', ','.join(secondary_groups)])
93 cmd.append(username)365 cmd.append(username)
94 subprocess.check_call(cmd)366 subprocess.check_call(cmd)
95 user_info = pwd.getpwnam(username)367 user_info = pwd.getpwnam(username)
96 return user_info368 return user_info
97369
98370
371def user_exists(username):
372 """Check if a user exists"""
373 try:
374 pwd.getpwnam(username)
375 user_exists = True
376 except KeyError:
377 user_exists = False
378 return user_exists
379
380
381def uid_exists(uid):
382 """Check if a uid exists"""
383 try:
384 pwd.getpwuid(uid)
385 uid_exists = True
386 except KeyError:
387 uid_exists = False
388 return uid_exists
389
390
391def group_exists(groupname):
392 """Check if a group exists"""
393 try:
394 grp.getgrnam(groupname)
395 group_exists = True
396 except KeyError:
397 group_exists = False
398 return group_exists
399
400
401def gid_exists(gid):
402 """Check if a gid exists"""
403 try:
404 grp.getgrgid(gid)
405 gid_exists = True
406 except KeyError:
407 gid_exists = False
408 return gid_exists
409
410
411def add_group(group_name, system_group=False, gid=None):
412 """Add a group to the system
413
414 Will log but otherwise succeed if the group already exists.
415
416 :param str group_name: group to create
417 :param bool system_group: Create system group
418 :param int gid: GID for user being created
419
420 :returns: The password database entry struct, as returned by `grp.getgrnam`
421 """
422 try:
423 group_info = grp.getgrnam(group_name)
424 log('group {0} already exists!'.format(group_name))
425 if gid:
426 group_info = grp.getgrgid(gid)
427 log('group with gid {0} already exists!'.format(gid))
428 except KeyError:
429 log('creating group {0}'.format(group_name))
430 add_new_group(group_name, system_group, gid)
431 group_info = grp.getgrnam(group_name)
432 return group_info
433
434
99def add_user_to_group(username, group):435def add_user_to_group(username, group):
100 """Add a user to a group"""436 """Add a user to a group"""
101 cmd = [437 cmd = ['gpasswd', '-a', username, group]
102 'gpasswd', '-a',
103 username,
104 group
105 ]
106 log("Adding user {} to group {}".format(username, group))438 log("Adding user {} to group {}".format(username, group))
107 subprocess.check_call(cmd)439 subprocess.check_call(cmd)
108440
109441
110def rsync(from_path, to_path, flags='-r', options=None):442def rsync(from_path, to_path, flags='-r', options=None, timeout=None):
111 """Replicate the contents of a path"""443 """Replicate the contents of a path"""
112 options = options or ['--delete', '--executability']444 options = options or ['--delete', '--executability']
113 cmd = ['/usr/bin/rsync', flags]445 cmd = ['/usr/bin/rsync', flags]
446 if timeout:
447 cmd = ['timeout', str(timeout)] + cmd
114 cmd.extend(options)448 cmd.extend(options)
115 cmd.append(from_path)449 cmd.append(from_path)
116 cmd.append(to_path)450 cmd.append(to_path)
117 log(" ".join(cmd))451 log(" ".join(cmd))
118 return subprocess.check_output(cmd).strip()452 return subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode('UTF-8').strip()
119453
120454
121def symlink(source, destination):455def symlink(source, destination):
@@ -130,42 +464,43 @@
130 subprocess.check_call(cmd)464 subprocess.check_call(cmd)
131465
132466
133def mkdir(path, owner='root', group='root', perms=0555, force=False):467def mkdir(path, owner='root', group='root', perms=0o555, force=False):
134 """Create a directory"""468 """Create a directory"""
135 log("Making dir {} {}:{} {:o}".format(path, owner, group,469 log("Making dir {} {}:{} {:o}".format(path, owner, group,
136 perms))470 perms))
137 uid = pwd.getpwnam(owner).pw_uid471 uid = pwd.getpwnam(owner).pw_uid
138 gid = grp.getgrnam(group).gr_gid472 gid = grp.getgrnam(group).gr_gid
139 realpath = os.path.abspath(path)473 realpath = os.path.abspath(path)
140 if os.path.exists(realpath):474 path_exists = os.path.exists(realpath)
141 if force and not os.path.isdir(realpath):475 if path_exists and force:
476 if not os.path.isdir(realpath):
142 log("Removing non-directory file {} prior to mkdir()".format(path))477 log("Removing non-directory file {} prior to mkdir()".format(path))
143 os.unlink(realpath)478 os.unlink(realpath)
144 else:479 os.makedirs(realpath, perms)
480 elif not path_exists:
145 os.makedirs(realpath, perms)481 os.makedirs(realpath, perms)
146 os.chown(realpath, uid, gid)482 os.chown(realpath, uid, gid)
147483 os.chmod(realpath, perms)
148484
149def write_file(path, content, owner='root', group='root', perms=0444):485
150 """Create or overwrite a file with the contents of a string"""486def write_file(path, content, owner='root', group='root', perms=0o444):
487 """Create or overwrite a file with the contents of a byte string."""
151 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))488 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
152 uid = pwd.getpwnam(owner).pw_uid489 uid = pwd.getpwnam(owner).pw_uid
153 gid = grp.getgrnam(group).gr_gid490 gid = grp.getgrnam(group).gr_gid
154 with open(path, 'w') as target:491 with open(path, 'wb') as target:
155 os.fchown(target.fileno(), uid, gid)492 os.fchown(target.fileno(), uid, gid)
156 os.fchmod(target.fileno(), perms)493 os.fchmod(target.fileno(), perms)
157 target.write(content)494 target.write(content)
158495
159496
160def fstab_remove(mp):497def fstab_remove(mp):
161 """Remove the given mountpoint entry from /etc/fstab498 """Remove the given mountpoint entry from /etc/fstab"""
162 """
163 return Fstab.remove_by_mountpoint(mp)499 return Fstab.remove_by_mountpoint(mp)
164500
165501
166def fstab_add(dev, mp, fs, options=None):502def fstab_add(dev, mp, fs, options=None):
167 """Adds the given device entry to the /etc/fstab file503 """Adds the given device entry to the /etc/fstab file"""
168 """
169 return Fstab.add(dev, mp, fs, options=options)504 return Fstab.add(dev, mp, fs, options=options)
170505
171506
@@ -177,7 +512,7 @@
177 cmd_args.extend([device, mountpoint])512 cmd_args.extend([device, mountpoint])
178 try:513 try:
179 subprocess.check_output(cmd_args)514 subprocess.check_output(cmd_args)
180 except subprocess.CalledProcessError, e:515 except subprocess.CalledProcessError as e:
181 log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))516 log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
182 return False517 return False
183518
@@ -191,7 +526,7 @@
191 cmd_args = ['umount', mountpoint]526 cmd_args = ['umount', mountpoint]
192 try:527 try:
193 subprocess.check_output(cmd_args)528 subprocess.check_output(cmd_args)
194 except subprocess.CalledProcessError, e:529 except subprocess.CalledProcessError as e:
195 log('Error unmounting {}\n{}'.format(mountpoint, e.output))530 log('Error unmounting {}\n{}'.format(mountpoint, e.output))
196 return False531 return False
197532
@@ -209,25 +544,48 @@
209 return system_mounts544 return system_mounts
210545
211546
547def fstab_mount(mountpoint):
548 """Mount filesystem using fstab"""
549 cmd_args = ['mount', mountpoint]
550 try:
551 subprocess.check_output(cmd_args)
552 except subprocess.CalledProcessError as e:
553 log('Error unmounting {}\n{}'.format(mountpoint, e.output))
554 return False
555 return True
556
557
212def file_hash(path, hash_type='md5'):558def file_hash(path, hash_type='md5'):
213 """559 """Generate a hash checksum of the contents of 'path' or None if not found.
214 Generate a hash checksum of the contents of 'path' or None if not found.
215560
216 :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,561 :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
217 such as md5, sha1, sha256, sha512, etc.562 such as md5, sha1, sha256, sha512, etc.
218 """563 """
219 if os.path.exists(path):564 if os.path.exists(path):
220 h = getattr(hashlib, hash_type)()565 h = getattr(hashlib, hash_type)()
221 with open(path, 'r') as source:566 with open(path, 'rb') as source:
222 h.update(source.read()) # IGNORE:E1101 - it does have update567 h.update(source.read())
223 return h.hexdigest()568 return h.hexdigest()
224 else:569 else:
225 return None570 return None
226571
227572
573def path_hash(path):
574 """Generate a hash checksum of all files matching 'path'. Standard
575 wildcards like '*' and '?' are supported, see documentation for the 'glob'
576 module for more information.
577
578 :return: dict: A { filename: hash } dictionary for all matched files.
579 Empty if none found.
580 """
581 return {
582 filename: file_hash(filename)
583 for filename in glob.iglob(path)
584 }
585
586
228def check_hash(path, checksum, hash_type='md5'):587def check_hash(path, checksum, hash_type='md5'):
229 """588 """Validate a file using a cryptographic checksum.
230 Validate a file using a cryptographic checksum.
231589
232 :param str checksum: Value of the checksum used to validate the file.590 :param str checksum: Value of the checksum used to validate the file.
233 :param str hash_type: Hash algorithm used to generate `checksum`.591 :param str hash_type: Hash algorithm used to generate `checksum`.
@@ -242,100 +600,184 @@
242600
243601
244class ChecksumError(ValueError):602class ChecksumError(ValueError):
603 """A class derived from Value error to indicate the checksum failed."""
245 pass604 pass
246605
247606
248def restart_on_change(restart_map, stopstart=False):607def restart_on_change(restart_map, stopstart=False, restart_functions=None):
249 """Restart services based on configuration files changing608 """Restart services based on configuration files changing
250609
251 This function is used a decorator, for example::610 This function is used a decorator, for example::
252611
253 @restart_on_change({612 @restart_on_change({
254 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]613 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
614 '/etc/apache/sites-enabled/*': [ 'apache2' ]
255 })615 })
256 def ceph_client_changed():616 def config_changed():
257 pass # your code here617 pass # your code here
258618
259 In this example, the cinder-api and cinder-volume services619 In this example, the cinder-api and cinder-volume services
260 would be restarted if /etc/ceph/ceph.conf is changed by the620 would be restarted if /etc/ceph/ceph.conf is changed by the
261 ceph_client_changed function.621 ceph_client_changed function. The apache2 service would be
622 restarted if any file matching the pattern got changed, created
623 or removed. Standard wildcards are supported, see documentation
624 for the 'glob' module for more information.
625
626 @param restart_map: {path_file_name: [service_name, ...]
627 @param stopstart: DEFAULT false; whether to stop, start OR restart
628 @param restart_functions: nonstandard functions to use to restart services
629 {svc: func, ...}
630 @returns result from decorated function
262 """631 """
263 def wrap(f):632 def wrap(f):
264 def wrapped_f(*args):633 @functools.wraps(f)
265 checksums = {}634 def wrapped_f(*args, **kwargs):
266 for path in restart_map:635 return restart_on_change_helper(
267 checksums[path] = file_hash(path)636 (lambda: f(*args, **kwargs)), restart_map, stopstart,
268 f(*args)637 restart_functions)
269 restarts = []
270 for path in restart_map:
271 if checksums[path] != file_hash(path):
272 restarts += restart_map[path]
273 services_list = list(OrderedDict.fromkeys(restarts))
274 if not stopstart:
275 for service_name in services_list:
276 service('restart', service_name)
277 else:
278 for action in ['stop', 'start']:
279 for service_name in services_list:
280 service(action, service_name)
281 return wrapped_f638 return wrapped_f
282 return wrap639 return wrap
283640
284641
285def lsb_release():642def restart_on_change_helper(lambda_f, restart_map, stopstart=False,
286 """Return /etc/lsb-release in a dict"""643 restart_functions=None):
287 d = {}644 """Helper function to perform the restart_on_change function.
288 with open('/etc/lsb-release', 'r') as lsb:645
289 for l in lsb:646 This is provided for decorators to restart services if files described
290 k, v = l.split('=')647 in the restart_map have changed after an invocation of lambda_f().
291 d[k.strip()] = v.strip()648
292 return d649 @param lambda_f: function to call.
650 @param restart_map: {file: [service, ...]}
651 @param stopstart: whether to stop, start or restart a service
652 @param restart_functions: nonstandard functions to use to restart services
653 {svc: func, ...}
654 @returns result of lambda_f()
655 """
656 if restart_functions is None:
657 restart_functions = {}
658 checksums = {path: path_hash(path) for path in restart_map}
659 r = lambda_f()
660 # create a list of lists of the services to restart
661 restarts = [restart_map[path]
662 for path in restart_map
663 if path_hash(path) != checksums[path]]
664 # create a flat list of ordered services without duplicates from lists
665 services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts)))
666 if services_list:
667 actions = ('stop', 'start') if stopstart else ('restart',)
668 for service_name in services_list:
669 if service_name in restart_functions:
670 restart_functions[service_name](service_name)
671 else:
672 for action in actions:
673 service(action, service_name)
674 return r
293675
294676
295def pwgen(length=None):677def pwgen(length=None):
296 """Generate a random pasword."""678 """Generate a random pasword."""
297 if length is None:679 if length is None:
680 # A random length is ok to use a weak PRNG
298 length = random.choice(range(35, 45))681 length = random.choice(range(35, 45))
299 alphanumeric_chars = [682 alphanumeric_chars = [
300 l for l in (string.letters + string.digits)683 l for l in (string.ascii_letters + string.digits)
301 if l not in 'l0QD1vAEIOUaeiou']684 if l not in 'l0QD1vAEIOUaeiou']
685 # Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
686 # actual password
687 random_generator = random.SystemRandom()
302 random_chars = [688 random_chars = [
303 random.choice(alphanumeric_chars) for _ in range(length)]689 random_generator.choice(alphanumeric_chars) for _ in range(length)]
304 return(''.join(random_chars))690 return(''.join(random_chars))
305691
306692
307def list_nics(nic_type):693def is_phy_iface(interface):
308 '''Return a list of nics of given type(s)'''694 """Returns True if interface is not virtual, otherwise False."""
309 if isinstance(nic_type, basestring):695 if interface:
696 sys_net = '/sys/class/net'
697 if os.path.isdir(sys_net):
698 for iface in glob.glob(os.path.join(sys_net, '*')):
699 if '/virtual/' in os.path.realpath(iface):
700 continue
701
702 if interface == os.path.basename(iface):
703 return True
704
705 return False
706
707
708def get_bond_master(interface):
709 """Returns bond master if interface is bond slave otherwise None.
710
711 NOTE: the provided interface is expected to be physical
712 """
713 if interface:
714 iface_path = '/sys/class/net/%s' % (interface)
715 if os.path.exists(iface_path):
716 if '/virtual/' in os.path.realpath(iface_path):
717 return None
718
719 master = os.path.join(iface_path, 'master')
720 if os.path.exists(master):
721 master = os.path.realpath(master)
722 # make sure it is a bond master
723 if os.path.exists(os.path.join(master, 'bonding')):
724 return os.path.basename(master)
725
726 return None
727
728
729def list_nics(nic_type=None):
730 """Return a list of nics of given type(s)"""
731 if isinstance(nic_type, six.string_types):
310 int_types = [nic_type]732 int_types = [nic_type]
311 else:733 else:
312 int_types = nic_type734 int_types = nic_type
735
313 interfaces = []736 interfaces = []
314 for int_type in int_types:737 if nic_type:
315 cmd = ['ip', 'addr', 'show', 'label', int_type + '*']738 for int_type in int_types:
316 ip_output = subprocess.check_output(cmd).split('\n')739 cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
317 ip_output = (line for line in ip_output if line)740 ip_output = subprocess.check_output(cmd).decode('UTF-8')
741 ip_output = ip_output.split('\n')
742 ip_output = (line for line in ip_output if line)
743 for line in ip_output:
744 if line.split()[1].startswith(int_type):
745 matched = re.search('.*: (' + int_type +
746 r'[0-9]+\.[0-9]+)@.*', line)
747 if matched:
748 iface = matched.groups()[0]
749 else:
750 iface = line.split()[1].replace(":", "")
751
752 if iface not in interfaces:
753 interfaces.append(iface)
754 else:
755 cmd = ['ip', 'a']
756 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
757 ip_output = (line.strip() for line in ip_output if line)
758
759 key = re.compile('^[0-9]+:\s+(.+):')
318 for line in ip_output:760 for line in ip_output:
319 if line.split()[1].startswith(int_type):761 matched = re.search(key, line)
320 matched = re.search('.*: (bond[0-9]+\.[0-9]+)@.*', line)762 if matched:
321 if matched:763 iface = matched.group(1)
322 interface = matched.groups()[0]764 iface = iface.partition("@")[0]
323 else:765 if iface not in interfaces:
324 interface = line.split()[1].replace(":", "")766 interfaces.append(iface)
325 interfaces.append(interface)
326767
327 return interfaces768 return interfaces
328769
329770
330def set_nic_mtu(nic, mtu):771def set_nic_mtu(nic, mtu):
331 '''Set MTU on a network interface'''772 """Set the Maximum Transmission Unit (MTU) on a network interface."""
332 cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]773 cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
333 subprocess.check_call(cmd)774 subprocess.check_call(cmd)
334775
335776
336def get_nic_mtu(nic):777def get_nic_mtu(nic):
778 """Return the Maximum Transmission Unit (MTU) for a network interface."""
337 cmd = ['ip', 'addr', 'show', nic]779 cmd = ['ip', 'addr', 'show', nic]
338 ip_output = subprocess.check_output(cmd).split('\n')780 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
339 mtu = ""781 mtu = ""
340 for line in ip_output:782 for line in ip_output:
341 words = line.split()783 words = line.split()
@@ -345,8 +787,9 @@
345787
346788
347def get_nic_hwaddr(nic):789def get_nic_hwaddr(nic):
790 """Return the Media Access Control (MAC) for a network interface."""
348 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]791 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
349 ip_output = subprocess.check_output(cmd)792 ip_output = subprocess.check_output(cmd).decode('UTF-8')
350 hwaddr = ""793 hwaddr = ""
351 words = ip_output.split()794 words = ip_output.split()
352 if 'link/ether' in words:795 if 'link/ether' in words:
@@ -354,38 +797,126 @@
354 return hwaddr797 return hwaddr
355798
356799
357def cmp_pkgrevno(package, revno, pkgcache=None):
358 '''Compare supplied revno with the revno of the installed package
359
360 * 1 => Installed revno is greater than supplied arg
361 * 0 => Installed revno is the same as supplied arg
362 * -1 => Installed revno is less than supplied arg
363
364 '''
365 import apt_pkg
366 from charmhelpers.fetch import apt_cache
367 if not pkgcache:
368 pkgcache = apt_cache()
369 pkg = pkgcache[package]
370 return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
371
372
373@contextmanager800@contextmanager
374def chdir(d):801def chdir(directory):
802 """Change the current working directory to a different directory for a code
803 block and return the previous directory after the block exits. Useful to
804 run commands from a specificed directory.
805
806 :param str directory: The directory path to change to for this context.
807 """
375 cur = os.getcwd()808 cur = os.getcwd()
376 try:809 try:
377 yield os.chdir(d)810 yield os.chdir(directory)
378 finally:811 finally:
379 os.chdir(cur)812 os.chdir(cur)
380813
381814
382def chownr(path, owner, group):815def chownr(path, owner, group, follow_links=True, chowntopdir=False):
816 """Recursively change user and group ownership of files and directories
817 in given path. Doesn't chown path itself by default, only its children.
818
819 :param str path: The string path to start changing ownership.
820 :param str owner: The owner string to use when looking up the uid.
821 :param str group: The group string to use when looking up the gid.
822 :param bool follow_links: Also follow and chown links if True
823 :param bool chowntopdir: Also chown path itself if True
824 """
383 uid = pwd.getpwnam(owner).pw_uid825 uid = pwd.getpwnam(owner).pw_uid
384 gid = grp.getgrnam(group).gr_gid826 gid = grp.getgrnam(group).gr_gid
827 if follow_links:
828 chown = os.chown
829 else:
830 chown = os.lchown
385831
386 for root, dirs, files in os.walk(path):832 if chowntopdir:
833 broken_symlink = os.path.lexists(path) and not os.path.exists(path)
834 if not broken_symlink:
835 chown(path, uid, gid)
836 for root, dirs, files in os.walk(path, followlinks=follow_links):
387 for name in dirs + files:837 for name in dirs + files:
388 full = os.path.join(root, name)838 full = os.path.join(root, name)
389 broken_symlink = os.path.lexists(full) and not os.path.exists(full)839 broken_symlink = os.path.lexists(full) and not os.path.exists(full)
390 if not broken_symlink:840 if not broken_symlink:
391 os.chown(full, uid, gid)841 chown(full, uid, gid)
842
843
844def lchownr(path, owner, group):
845 """Recursively change user and group ownership of files and directories
846 in a given path, not following symbolic links. See the documentation for
847 'os.lchown' for more information.
848
849 :param str path: The string path to start changing ownership.
850 :param str owner: The owner string to use when looking up the uid.
851 :param str group: The group string to use when looking up the gid.
852 """
853 chownr(path, owner, group, follow_links=False)
854
855
856def owner(path):
857 """Returns a tuple containing the username & groupname owning the path.
858
859 :param str path: the string path to retrieve the ownership
860 :return tuple(str, str): A (username, groupname) tuple containing the
861 name of the user and group owning the path.
862 :raises OSError: if the specified path does not exist
863 """
864 stat = os.stat(path)
865 username = pwd.getpwuid(stat.st_uid)[0]
866 groupname = grp.getgrgid(stat.st_gid)[0]
867 return username, groupname
868
869
870def get_total_ram():
871 """The total amount of system RAM in bytes.
872
873 This is what is reported by the OS, and may be overcommitted when
874 there are multiple containers hosted on the same machine.
875 """
876 with open('/proc/meminfo', 'r') as f:
877 for line in f.readlines():
878 if line:
879 key, value, unit = line.split()
880 if key == 'MemTotal:':
881 assert unit == 'kB', 'Unknown unit'
882 return int(value) * 1024 # Classic, not KiB.
883 raise NotImplementedError()
884
885
886UPSTART_CONTAINER_TYPE = '/run/container_type'
887
888
889def is_container():
890 """Determine whether unit is running in a container
891
892 @return: boolean indicating if unit is in a container
893 """
894 if init_is_systemd():
895 # Detect using systemd-detect-virt
896 return subprocess.call(['systemd-detect-virt',
897 '--container']) == 0
898 else:
899 # Detect using upstart container file marker
900 return os.path.exists(UPSTART_CONTAINER_TYPE)
901
902
903def add_to_updatedb_prunepath(path, updatedb_path=UPDATEDB_PATH):
904 with open(updatedb_path, 'r+') as f_id:
905 updatedb_text = f_id.read()
906 output = updatedb(updatedb_text, path)
907 f_id.seek(0)
908 f_id.write(output)
909 f_id.truncate()
910
911
912def updatedb(updatedb_text, new_path):
913 lines = [line for line in updatedb_text.split("\n")]
914 for i, line in enumerate(lines):
915 if line.startswith("PRUNEPATHS="):
916 paths_line = line.split("=")[1].replace('"', '')
917 paths = paths_line.split(" ")
918 if new_path not in paths:
919 paths.append(new_path)
920 lines[i] = 'PRUNEPATHS="{}"'.format(' '.join(paths))
921 output = "\n".join(lines)
922 return output
392923
=== 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-05-11 14:56:57 +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-05-11 14:56:57 +0000
@@ -0,0 +1,88 @@
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)
23
24
25class CompareHostReleases(BasicStringComparator):
26 """Provide comparisons of Ubuntu releases.
27
28 Use in the form of
29
30 if CompareHostReleases(release) > 'trusty':
31 # do something with mitaka
32 """
33 _list = UBUNTU_RELEASES
34
35
36def service_available(service_name):
37 """Determine whether a system service is available"""
38 try:
39 subprocess.check_output(
40 ['service', service_name, 'status'],
41 stderr=subprocess.STDOUT).decode('UTF-8')
42 except subprocess.CalledProcessError as e:
43 return b'unrecognized service' not in e.output
44 else:
45 return True
46
47
48def add_new_group(group_name, system_group=False, gid=None):
49 cmd = ['addgroup']
50 if gid:
51 cmd.extend(['--gid', str(gid)])
52 if system_group:
53 cmd.append('--system')
54 else:
55 cmd.extend([
56 '--group',
57 ])
58 cmd.append(group_name)
59 subprocess.check_call(cmd)
60
61
62def lsb_release():
63 """Return /etc/lsb-release in a dict"""
64 d = {}
65 with open('/etc/lsb-release', 'r') as lsb:
66 for l in lsb:
67 k, v = l.split('=')
68 d[k.strip()] = v.strip()
69 return d
70
71
72def cmp_pkgrevno(package, revno, pkgcache=None):
73 """Compare supplied revno with the revno of the installed package.
74
75 * 1 => Installed revno is greater than supplied arg
76 * 0 => Installed revno is the same as supplied arg
77 * -1 => Installed revno is less than supplied arg
78
79 This function imports apt_cache function from charmhelpers.fetch if
80 the pkgcache argument is None. Be sure to add charmhelpers.fetch if
81 you call this function, or pass an apt_pkg.Cache() instance.
82 """
83 import apt_pkg
84 if not pkgcache:
85 from charmhelpers.fetch import apt_cache
86 pkgcache = apt_cache()
87 pkg = pkgcache[package]
88 return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
089
=== 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-05-11 14:56:57 +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-05-11 14:56:57 +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-05-11 14:56:57 +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-05-11 14:56:57 +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 2014-10-24 16:35:00 +0000
+++ hooks/charmhelpers/core/services/__init__.py 2017-05-11 14:56:57 +0000
@@ -1,2 +1,16 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
1from .base import * # NOQA15from .base import * # NOQA
2from .helpers import * # NOQA16from .helpers import * # NOQA
317
=== modified file 'hooks/charmhelpers/core/services/base.py'
--- hooks/charmhelpers/core/services/base.py 2014-10-24 16:35:00 +0000
+++ hooks/charmhelpers/core/services/base.py 2017-05-11 14:56:57 +0000
@@ -1,7 +1,21 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
1import os15import os
2import re
3import json16import json
4from collections import Iterable17from inspect import getargspec
18from collections import Iterable, OrderedDict
519
6from charmhelpers.core import host20from charmhelpers.core import host
7from charmhelpers.core import hookenv21from charmhelpers.core import hookenv
@@ -103,7 +117,7 @@
103 """117 """
104 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')
105 self._ready = None119 self._ready = None
106 self.services = {}120 self.services = OrderedDict()
107 for service in services or []:121 for service in services or []:
108 service_name = service['service']122 service_name = service['service']
109 self.services[service_name] = service123 self.services[service_name] = service
@@ -112,15 +126,18 @@
112 """126 """
113 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.
114 """128 """
115 hook_name = hookenv.hook_name()129 hookenv._run_atstart()
116 if hook_name == 'stop':130 try:
117 self.stop_services()131 hook_name = hookenv.hook_name()
118 else:132 if hook_name == 'stop':
119 self.provide_data()133 self.stop_services()
120 self.reconfigure_services()134 else:
121 cfg = hookenv.config()135 self.reconfigure_services()
122 if cfg.implicit_save:136 self.provide_data()
123 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()
124141
125 def provide_data(self):142 def provide_data(self):
126 """143 """
@@ -129,15 +146,36 @@
129 A provider must have a `name` attribute, which indicates which relation146 A provider must have a `name` attribute, which indicates which relation
130 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
131 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.
132 """163 """
133 hook_name = hookenv.hook_name()164 for service_name, service in self.services.items():
134 for service in self.services.values():165 service_ready = self.is_ready(service_name)
135 for provider in service.get('provided_data', []):166 for provider in service.get('provided_data', []):
136 if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):167 for relid in hookenv.relation_ids(provider.name):
137 data = provider.provide_data()168 units = hookenv.related_units(relid)
138 _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data169 if not units:
139 if _ready:170 continue
140 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)
141179
142 def reconfigure_services(self, *service_names):180 def reconfigure_services(self, *service_names):
143 """181 """
144182
=== modified file 'hooks/charmhelpers/core/services/helpers.py'
--- hooks/charmhelpers/core/services/helpers.py 2014-10-24 16:35:00 +0000
+++ hooks/charmhelpers/core/services/helpers.py 2017-05-11 14:56:57 +0000
@@ -1,6 +1,22 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
1import os15import os
2import yaml16import yaml
17
3from charmhelpers.core import hookenv18from charmhelpers.core import hookenv
19from charmhelpers.core import host
4from charmhelpers.core import templating20from charmhelpers.core import templating
521
6from charmhelpers.core.services.base import ManagerCallback22from charmhelpers.core.services.base import ManagerCallback
@@ -29,12 +45,14 @@
29 """45 """
30 name = None46 name = None
31 interface = None47 interface = None
32 required_keys = []
3348
34 def __init__(self, name=None, additional_required_keys=None):49 def __init__(self, name=None, additional_required_keys=None):
50 if not hasattr(self, 'required_keys'):
51 self.required_keys = []
52
35 if name is not None:53 if name is not None:
36 self.name = name54 self.name = name
37 if additional_required_keys is not None:55 if additional_required_keys:
38 self.required_keys.extend(additional_required_keys)56 self.required_keys.extend(additional_required_keys)
39 self.get_data()57 self.get_data()
4058
@@ -118,7 +136,10 @@
118 """136 """
119 name = 'db'137 name = 'db'
120 interface = 'mysql'138 interface = 'mysql'
121 required_keys = ['host', 'user', 'password', 'database']139
140 def __init__(self, *args, **kwargs):
141 self.required_keys = ['host', 'user', 'password', 'database']
142 RelationContext.__init__(self, *args, **kwargs)
122143
123144
124class HttpRelation(RelationContext):145class HttpRelation(RelationContext):
@@ -130,7 +151,10 @@
130 """151 """
131 name = 'website'152 name = 'website'
132 interface = 'http'153 interface = 'http'
133 required_keys = ['host', 'port']154
155 def __init__(self, *args, **kwargs):
156 self.required_keys = ['host', 'port']
157 RelationContext.__init__(self, *args, **kwargs)
134158
135 def provide_data(self):159 def provide_data(self):
136 return {160 return {
@@ -196,7 +220,7 @@
196 if not os.path.isabs(file_name):220 if not os.path.isabs(file_name):
197 file_name = os.path.join(hookenv.charm_dir(), file_name)221 file_name = os.path.join(hookenv.charm_dir(), file_name)
198 with open(file_name, 'w') as file_stream:222 with open(file_name, 'w') as file_stream:
199 os.fchmod(file_stream.fileno(), 0600)223 os.fchmod(file_stream.fileno(), 0o600)
200 yaml.dump(config_data, file_stream)224 yaml.dump(config_data, file_stream)
201225
202 def read_context(self, file_name):226 def read_context(self, file_name):
@@ -211,28 +235,55 @@
211235
212class TemplateCallback(ManagerCallback):236class TemplateCallback(ManagerCallback):
213 """237 """
214 Callback class that will render a Jinja2 template, for use as a ready action.238 Callback class that will render a Jinja2 template, for use as a ready
215239 action.
216 :param str source: The template source file, relative to `$CHARM_DIR/templates`240
217 :param str target: The target to write the rendered template to241 :param str source: The template source file, relative to
242 `$CHARM_DIR/templates`
243
244 :param str target: The target to write the rendered template to (or None)
218 :param str owner: The owner of the rendered file245 :param str owner: The owner of the rendered file
219 :param str group: The group of the rendered file246 :param str group: The group of the rendered file
220 :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
221 """253 """
222 def __init__(self, source, target, owner='root', group='root', perms=0444):254 def __init__(self, source, target,
255 owner='root', group='root', perms=0o444,
256 on_change_action=None, template_loader=None):
223 self.source = source257 self.source = source
224 self.target = target258 self.target = target
225 self.owner = owner259 self.owner = owner
226 self.group = group260 self.group = group
227 self.perms = perms261 self.perms = perms
262 self.on_change_action = on_change_action
263 self.template_loader = template_loader
228264
229 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)
230 service = manager.get_service(service_name)269 service = manager.get_service(service_name)
231 context = {}270 context = {'ctx': {}}
232 for ctx in service.get('required_data', []):271 for ctx in service.get('required_data', []):
233 context.update(ctx)272 context.update(ctx)
234 templating.render(self.source, self.target, context,273 context['ctx'].update(ctx)
235 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
236287
237288
238# Convenience aliases for templates289# Convenience aliases for templates
239290
=== added file 'hooks/charmhelpers/core/strutils.py'
--- hooks/charmhelpers/core/strutils.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/strutils.py 2017-05-11 14:56:57 +0000
@@ -0,0 +1,123 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4# Copyright 2014-2015 Canonical Limited.
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17
18import six
19import re
20
21
22def bool_from_string(value):
23 """Interpret string value as boolean.
24
25 Returns True if value translates to True otherwise False.
26 """
27 if isinstance(value, six.string_types):
28 value = six.text_type(value)
29 else:
30 msg = "Unable to interpret non-string value '%s' as boolean" % (value)
31 raise ValueError(msg)
32
33 value = value.strip().lower()
34
35 if value in ['y', 'yes', 'true', 't', 'on']:
36 return True
37 elif value in ['n', 'no', 'false', 'f', 'off']:
38 return False
39
40 msg = "Unable to interpret string value '%s' as boolean" % (value)
41 raise ValueError(msg)
42
43
44def bytes_from_string(value):
45 """Interpret human readable string value as bytes.
46
47 Returns int
48 """
49 BYTE_POWER = {
50 'K': 1,
51 'KB': 1,
52 'M': 2,
53 'MB': 2,
54 'G': 3,
55 'GB': 3,
56 'T': 4,
57 'TB': 4,
58 'P': 5,
59 'PB': 5,
60 }
61 if isinstance(value, six.string_types):
62 value = six.text_type(value)
63 else:
64 msg = "Unable to interpret non-string value '%s' as boolean" % (value)
65 raise ValueError(msg)
66 matches = re.match("([0-9]+)([a-zA-Z]+)", value)
67 if not matches:
68 msg = "Unable to interpret string value '%s' as bytes" % (value)
69 raise ValueError(msg)
70 return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)])
71
72
73class BasicStringComparator(object):
74 """Provides a class that will compare strings from an iterator type object.
75 Used to provide > and < comparisons on strings that may not necessarily be
76 alphanumerically ordered. e.g. OpenStack or Ubuntu releases AFTER the
77 z-wrap.
78 """
79
80 _list = None
81
82 def __init__(self, item):
83 if self._list is None:
84 raise Exception("Must define the _list in the class definition!")
85 try:
86 self.index = self._list.index(item)
87 except Exception:
88 raise KeyError("Item '{}' is not in list '{}'"
89 .format(item, self._list))
90
91 def __eq__(self, other):
92 assert isinstance(other, str) or isinstance(other, self.__class__)
93 return self.index == self._list.index(other)
94
95 def __ne__(self, other):
96 return not self.__eq__(other)
97
98 def __lt__(self, other):
99 assert isinstance(other, str) or isinstance(other, self.__class__)
100 return self.index < self._list.index(other)
101
102 def __ge__(self, other):
103 return not self.__lt__(other)
104
105 def __gt__(self, other):
106 assert isinstance(other, str) or isinstance(other, self.__class__)
107 return self.index > self._list.index(other)
108
109 def __le__(self, other):
110 return not self.__gt__(other)
111
112 def __str__(self):
113 """Always give back the item at the index so it can be used in
114 comparisons like:
115
116 s_mitaka = CompareOpenStack('mitaka')
117 s_newton = CompareOpenstack('newton')
118
119 assert s_newton > s_mitaka
120
121 @returns: <string>
122 """
123 return self._list[self.index]
0124
=== modified file 'hooks/charmhelpers/core/sysctl.py'
--- hooks/charmhelpers/core/sysctl.py 2014-10-24 16:35:00 +0000
+++ hooks/charmhelpers/core/sysctl.py 2017-05-11 14:56:57 +0000
@@ -1,7 +1,19 @@
1#!/usr/bin/env python1#!/usr/bin/env python
2# -*- coding: utf-8 -*-2# -*- coding: utf-8 -*-
33
4__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'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.
517
6import yaml18import yaml
719
@@ -10,25 +22,33 @@
10from charmhelpers.core.hookenv import (22from charmhelpers.core.hookenv import (
11 log,23 log,
12 DEBUG,24 DEBUG,
25 ERROR,
13)26)
1427
28__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
29
1530
16def create(sysctl_dict, sysctl_file):31def create(sysctl_dict, sysctl_file):
17 """Creates a sysctl.conf file from a YAML associative array32 """Creates a sysctl.conf file from a YAML associative array
1833
19 :param sysctl_dict: a dict of sysctl options eg { 'kernel.max_pid': 1337 }34 :param sysctl_dict: a YAML-formatted string of sysctl options eg "{ 'kernel.max_pid': 1337 }"
20 :type sysctl_dict: dict35 :type sysctl_dict: str
21 :param sysctl_file: path to the sysctl file to be saved36 :param sysctl_file: path to the sysctl file to be saved
22 :type sysctl_file: str or unicode37 :type sysctl_file: str or unicode
23 :returns: None38 :returns: None
24 """39 """
25 sysctl_dict = yaml.load(sysctl_dict)40 try:
41 sysctl_dict_parsed = yaml.safe_load(sysctl_dict)
42 except yaml.YAMLError:
43 log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict),
44 level=ERROR)
45 return
2646
27 with open(sysctl_file, "w") as fd:47 with open(sysctl_file, "w") as fd:
28 for key, value in sysctl_dict.items():48 for key, value in sysctl_dict_parsed.items():
29 fd.write("{}={}\n".format(key, value))49 fd.write("{}={}\n".format(key, value))
3050
31 log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict),51 log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict_parsed),
32 level=DEBUG)52 level=DEBUG)
3353
34 check_call(["sysctl", "-p", sysctl_file])54 check_call(["sysctl", "-p", sysctl_file])
3555
=== modified file 'hooks/charmhelpers/core/templating.py'
--- hooks/charmhelpers/core/templating.py 2014-10-24 16:35:00 +0000
+++ hooks/charmhelpers/core/templating.py 2017-05-11 14:56:57 +0000
@@ -1,16 +1,33 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
1import os15import os
16import sys
217
3from charmhelpers.core import host18from charmhelpers.core import host
4from charmhelpers.core import hookenv19from charmhelpers.core import hookenv
520
621
7def render(source, target, context, owner='root', group='root', perms=0444, templates_dir=None):22def render(source, target, context, owner='root', group='root',
23 perms=0o444, templates_dir=None, encoding='UTF-8', template_loader=None):
8 """24 """
9 Render a template.25 Render a template.
1026
11 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`.
1228
13 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.
1431
15 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
16 template.33 template.
@@ -19,8 +36,12 @@
1936
20 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.
2138
22 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
23 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.
24 """45 """
25 try:46 try:
26 from jinja2 import FileSystemLoader, Environment, exceptions47 from jinja2 import FileSystemLoader, Environment, exceptions
@@ -32,20 +53,32 @@
32 'charmhelpers.fetch to install it',53 'charmhelpers.fetch to install it',
33 level=hookenv.ERROR)54 level=hookenv.ERROR)
34 raise55 raise
35 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)
36 from jinja2 import FileSystemLoader, Environment, exceptions60 from jinja2 import FileSystemLoader, Environment, exceptions
3761
38 if templates_dir is None:62 if template_loader:
39 templates_dir = os.path.join(hookenv.charm_dir(), 'templates')63 template_env = Environment(loader=template_loader)
40 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))
41 try:68 try:
42 source = source69 source = source
43 template = loader.get_template(source)70 template = template_env.get_template(source)
44 except exceptions.TemplateNotFound as e:71 except exceptions.TemplateNotFound as e:
45 hookenv.log('Could not load template %s from %s.' %72 hookenv.log('Could not load template %s from %s.' %
46 (source, templates_dir),73 (source, templates_dir),
47 level=hookenv.ERROR)74 level=hookenv.ERROR)
48 raise e75 raise e
49 content = template.render(context)76 content = template.render(context)
50 host.mkdir(os.path.dirname(target))77 if target is not None:
51 host.write_file(target, content, 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
5285
=== added file 'hooks/charmhelpers/core/unitdata.py'
--- hooks/charmhelpers/core/unitdata.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/core/unitdata.py 2017-05-11 14:56:57 +0000
@@ -0,0 +1,518 @@
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#
4# Copyright 2014-2015 Canonical Limited.
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17#
18# Authors:
19# Kapil Thangavelu <kapil.foss@gmail.com>
20#
21"""
22Intro
23-----
24
25A simple way to store state in units. This provides a key value
26storage with support for versioned, transactional operation,
27and can calculate deltas from previous values to simplify unit logic
28when processing changes.
29
30
31Hook Integration
32----------------
33
34There are several extant frameworks for hook execution, including
35
36 - charmhelpers.core.hookenv.Hooks
37 - charmhelpers.core.services.ServiceManager
38
39The storage classes are framework agnostic, one simple integration is
40via the HookData contextmanager. It will record the current hook
41execution environment (including relation data, config data, etc.),
42setup a transaction and allow easy access to the changes from
43previously seen values. One consequence of the integration is the
44reservation of particular keys ('rels', 'unit', 'env', 'config',
45'charm_revisions') for their respective values.
46
47Here's a fully worked integration example using hookenv.Hooks::
48
49 from charmhelper.core import hookenv, unitdata
50
51 hook_data = unitdata.HookData()
52 db = unitdata.kv()
53 hooks = hookenv.Hooks()
54
55 @hooks.hook
56 def config_changed():
57 # Print all changes to configuration from previously seen
58 # values.
59 for changed, (prev, cur) in hook_data.conf.items():
60 print('config changed', changed,
61 'previous value', prev,
62 'current value', cur)
63
64 # Get some unit specific bookeeping
65 if not db.get('pkg_key'):
66 key = urllib.urlopen('https://example.com/pkg_key').read()
67 db.set('pkg_key', key)
68
69 # Directly access all charm config as a mapping.
70 conf = db.getrange('config', True)
71
72 # Directly access all relation data as a mapping
73 rels = db.getrange('rels', True)
74
75 if __name__ == '__main__':
76 with hook_data():
77 hook.execute()
78
79
80A more basic integration is via the hook_scope context manager which simply
81manages transaction scope (and records hook name, and timestamp)::
82
83 >>> from unitdata import kv
84 >>> db = kv()
85 >>> with db.hook_scope('install'):
86 ... # do work, in transactional scope.
87 ... db.set('x', 1)
88 >>> db.get('x')
89 1
90
91
92Usage
93-----
94
95Values are automatically json de/serialized to preserve basic typing
96and complex data struct capabilities (dicts, lists, ints, booleans, etc).
97
98Individual values can be manipulated via get/set::
99
100 >>> kv.set('y', True)
101 >>> kv.get('y')
102 True
103
104 # We can set complex values (dicts, lists) as a single key.
105 >>> kv.set('config', {'a': 1, 'b': True'})
106
107 # Also supports returning dictionaries as a record which
108 # provides attribute access.
109 >>> config = kv.get('config', record=True)
110 >>> config.b
111 True
112
113
114Groups of keys can be manipulated with update/getrange::
115
116 >>> kv.update({'z': 1, 'y': 2}, prefix="gui.")
117 >>> kv.getrange('gui.', strip=True)
118 {'z': 1, 'y': 2}
119
120When updating values, its very helpful to understand which values
121have actually changed and how have they changed. The storage
122provides a delta method to provide for this::
123
124 >>> data = {'debug': True, 'option': 2}
125 >>> delta = kv.delta(data, 'config.')
126 >>> delta.debug.previous
127 None
128 >>> delta.debug.current
129 True
130 >>> delta
131 {'debug': (None, True), 'option': (None, 2)}
132
133Note the delta method does not persist the actual change, it needs to
134be explicitly saved via 'update' method::
135
136 >>> kv.update(data, 'config.')
137
138Values modified in the context of a hook scope retain historical values
139associated to the hookname.
140
141 >>> with db.hook_scope('config-changed'):
142 ... db.set('x', 42)
143 >>> db.gethistory('x')
144 [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'),
145 (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')]
146
147"""
148
149import collections
150import contextlib
151import datetime
152import itertools
153import json
154import os
155import pprint
156import sqlite3
157import sys
158
159__author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>'
160
161
162class Storage(object):
163 """Simple key value database for local unit state within charms.
164
165 Modifications are not persisted unless :meth:`flush` is called.
166
167 To support dicts, lists, integer, floats, and booleans values
168 are automatically json encoded/decoded.
169 """
170 def __init__(self, path=None):
171 self.db_path = path
172 if path is None:
173 if 'UNIT_STATE_DB' in os.environ:
174 self.db_path = os.environ['UNIT_STATE_DB']
175 else:
176 self.db_path = os.path.join(
177 os.environ.get('CHARM_DIR', ''), '.unit-state.db')
178 self.conn = sqlite3.connect('%s' % self.db_path)
179 self.cursor = self.conn.cursor()
180 self.revision = None
181 self._closed = False
182 self._init()
183
184 def close(self):
185 if self._closed:
186 return
187 self.flush(False)
188 self.cursor.close()
189 self.conn.close()
190 self._closed = True
191
192 def get(self, key, default=None, record=False):
193 self.cursor.execute('select data from kv where key=?', [key])
194 result = self.cursor.fetchone()
195 if not result:
196 return default
197 if record:
198 return Record(json.loads(result[0]))
199 return json.loads(result[0])
200
201 def getrange(self, key_prefix, strip=False):
202 """
203 Get a range of keys starting with a common prefix as a mapping of
204 keys to values.
205
206 :param str key_prefix: Common prefix among all keys
207 :param bool strip: Optionally strip the common prefix from the key
208 names in the returned dict
209 :return dict: A (possibly empty) dict of key-value mappings
210 """
211 self.cursor.execute("select key, data from kv where key like ?",
212 ['%s%%' % key_prefix])
213 result = self.cursor.fetchall()
214
215 if not result:
216 return {}
217 if not strip:
218 key_prefix = ''
219 return dict([
220 (k[len(key_prefix):], json.loads(v)) for k, v in result])
221
222 def update(self, mapping, prefix=""):
223 """
224 Set the values of multiple keys at once.
225
226 :param dict mapping: Mapping of keys to values
227 :param str prefix: Optional prefix to apply to all keys in `mapping`
228 before setting
229 """
230 for k, v in mapping.items():
231 self.set("%s%s" % (prefix, k), v)
232
233 def unset(self, key):
234 """
235 Remove a key from the database entirely.
236 """
237 self.cursor.execute('delete from kv where key=?', [key])
238 if self.revision and self.cursor.rowcount:
239 self.cursor.execute(
240 'insert into kv_revisions values (?, ?, ?)',
241 [key, self.revision, json.dumps('DELETED')])
242
243 def unsetrange(self, keys=None, prefix=""):
244 """
245 Remove a range of keys starting with a common prefix, from the database
246 entirely.
247
248 :param list keys: List of keys to remove.
249 :param str prefix: Optional prefix to apply to all keys in ``keys``
250 before removing.
251 """
252 if keys is not None:
253 keys = ['%s%s' % (prefix, key) for key in keys]
254 self.cursor.execute('delete from kv where key in (%s)' % ','.join(['?'] * len(keys)), keys)
255 if self.revision and self.cursor.rowcount:
256 self.cursor.execute(
257 'insert into kv_revisions values %s' % ','.join(['(?, ?, ?)'] * len(keys)),
258 list(itertools.chain.from_iterable((key, self.revision, json.dumps('DELETED')) for key in keys)))
259 else:
260 self.cursor.execute('delete from kv where key like ?',
261 ['%s%%' % prefix])
262 if self.revision and self.cursor.rowcount:
263 self.cursor.execute(
264 'insert into kv_revisions values (?, ?, ?)',
265 ['%s%%' % prefix, self.revision, json.dumps('DELETED')])
266
267 def set(self, key, value):
268 """
269 Set a value in the database.
270
271 :param str key: Key to set the value for
272 :param value: Any JSON-serializable value to be set
273 """
274 serialized = json.dumps(value)
275
276 self.cursor.execute('select data from kv where key=?', [key])
277 exists = self.cursor.fetchone()
278
279 # Skip mutations to the same value
280 if exists:
281 if exists[0] == serialized:
282 return value
283
284 if not exists:
285 self.cursor.execute(
286 'insert into kv (key, data) values (?, ?)',
287 (key, serialized))
288 else:
289 self.cursor.execute('''
290 update kv
291 set data = ?
292 where key = ?''', [serialized, key])
293
294 # Save
295 if not self.revision:
296 return value
297
298 self.cursor.execute(
299 'select 1 from kv_revisions where key=? and revision=?',
300 [key, self.revision])
301 exists = self.cursor.fetchone()
302
303 if not exists:
304 self.cursor.execute(
305 '''insert into kv_revisions (
306 revision, key, data) values (?, ?, ?)''',
307 (self.revision, key, serialized))
308 else:
309 self.cursor.execute(
310 '''
311 update kv_revisions
312 set data = ?
313 where key = ?
314 and revision = ?''',
315 [serialized, key, self.revision])
316
317 return value
318
319 def delta(self, mapping, prefix):
320 """
321 return a delta containing values that have changed.
322 """
323 previous = self.getrange(prefix, strip=True)
324 if not previous:
325 pk = set()
326 else:
327 pk = set(previous.keys())
328 ck = set(mapping.keys())
329 delta = DeltaSet()
330
331 # added
332 for k in ck.difference(pk):
333 delta[k] = Delta(None, mapping[k])
334
335 # removed
336 for k in pk.difference(ck):
337 delta[k] = Delta(previous[k], None)
338
339 # changed
340 for k in pk.intersection(ck):
341 c = mapping[k]
342 p = previous[k]
343 if c != p:
344 delta[k] = Delta(p, c)
345
346 return delta
347
348 @contextlib.contextmanager
349 def hook_scope(self, name=""):
350 """Scope all future interactions to the current hook execution
351 revision."""
352 assert not self.revision
353 self.cursor.execute(
354 'insert into hooks (hook, date) values (?, ?)',
355 (name or sys.argv[0],
356 datetime.datetime.utcnow().isoformat()))
357 self.revision = self.cursor.lastrowid
358 try:
359 yield self.revision
360 self.revision = None
361 except:
362 self.flush(False)
363 self.revision = None
364 raise
365 else:
366 self.flush()
367
368 def flush(self, save=True):
369 if save:
370 self.conn.commit()
371 elif self._closed:
372 return
373 else:
374 self.conn.rollback()
375
376 def _init(self):
377 self.cursor.execute('''
378 create table if not exists kv (
379 key text,
380 data text,
381 primary key (key)
382 )''')
383 self.cursor.execute('''
384 create table if not exists kv_revisions (
385 key text,
386 revision integer,
387 data text,
388 primary key (key, revision)
389 )''')
390 self.cursor.execute('''
391 create table if not exists hooks (
392 version integer primary key autoincrement,
393 hook text,
394 date text
395 )''')
396 self.conn.commit()
397
398 def gethistory(self, key, deserialize=False):
399 self.cursor.execute(
400 '''
401 select kv.revision, kv.key, kv.data, h.hook, h.date
402 from kv_revisions kv,
403 hooks h
404 where kv.key=?
405 and kv.revision = h.version
406 ''', [key])
407 if deserialize is False:
408 return self.cursor.fetchall()
409 return map(_parse_history, self.cursor.fetchall())
410
411 def debug(self, fh=sys.stderr):
412 self.cursor.execute('select * from kv')
413 pprint.pprint(self.cursor.fetchall(), stream=fh)
414 self.cursor.execute('select * from kv_revisions')
415 pprint.pprint(self.cursor.fetchall(), stream=fh)
416
417
418def _parse_history(d):
419 return (d[0], d[1], json.loads(d[2]), d[3],
420 datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f"))
421
422
423class HookData(object):
424 """Simple integration for existing hook exec frameworks.
425
426 Records all unit information, and stores deltas for processing
427 by the hook.
428
429 Sample::
430
431 from charmhelper.core import hookenv, unitdata
432
433 changes = unitdata.HookData()
434 db = unitdata.kv()
435 hooks = hookenv.Hooks()
436
437 @hooks.hook
438 def config_changed():
439 # View all changes to configuration
440 for changed, (prev, cur) in changes.conf.items():
441 print('config changed', changed,
442 'previous value', prev,
443 'current value', cur)
444
445 # Get some unit specific bookeeping
446 if not db.get('pkg_key'):
447 key = urllib.urlopen('https://example.com/pkg_key').read()
448 db.set('pkg_key', key)
449
450 if __name__ == '__main__':
451 with changes():
452 hook.execute()
453
454 """
455 def __init__(self):
456 self.kv = kv()
457 self.conf = None
458 self.rels = None
459
460 @contextlib.contextmanager
461 def __call__(self):
462 from charmhelpers.core import hookenv
463 hook_name = hookenv.hook_name()
464
465 with self.kv.hook_scope(hook_name):
466 self._record_charm_version(hookenv.charm_dir())
467 delta_config, delta_relation = self._record_hook(hookenv)
468 yield self.kv, delta_config, delta_relation
469
470 def _record_charm_version(self, charm_dir):
471 # Record revisions.. charm revisions are meaningless
472 # to charm authors as they don't control the revision.
473 # so logic dependnent on revision is not particularly
474 # useful, however it is useful for debugging analysis.
475 charm_rev = open(
476 os.path.join(charm_dir, 'revision')).read().strip()
477 charm_rev = charm_rev or '0'
478 revs = self.kv.get('charm_revisions', [])
479 if charm_rev not in revs:
480 revs.append(charm_rev.strip() or '0')
481 self.kv.set('charm_revisions', revs)
482
483 def _record_hook(self, hookenv):
484 data = hookenv.execution_environment()
485 self.conf = conf_delta = self.kv.delta(data['conf'], 'config')
486 self.rels = rels_delta = self.kv.delta(data['rels'], 'rels')
487 self.kv.set('env', dict(data['env']))
488 self.kv.set('unit', data['unit'])
489 self.kv.set('relid', data.get('relid'))
490 return conf_delta, rels_delta
491
492
493class Record(dict):
494
495 __slots__ = ()
496
497 def __getattr__(self, k):
498 if k in self:
499 return self[k]
500 raise AttributeError(k)
501
502
503class DeltaSet(Record):
504
505 __slots__ = ()
506
507
508Delta = collections.namedtuple('Delta', ['previous', 'current'])
509
510
511_KV = None
512
513
514def kv():
515 global _KV
516 if _KV is None:
517 _KV = Storage()
518 return _KV
0519
=== modified file 'hooks/charmhelpers/fetch/__init__.py'
--- hooks/charmhelpers/fetch/__init__.py 2014-10-24 16:35:00 +0000
+++ hooks/charmhelpers/fetch/__init__.py 2017-05-11 14:56:57 +0000
@@ -1,71 +1,31 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
1import importlib15import importlib
2from tempfile import NamedTemporaryFile16from charmhelpers.osplatform import get_platform
3import time
4from yaml import safe_load17from yaml import safe_load
5from charmhelpers.core.host import (
6 lsb_release
7)
8from urlparse import (
9 urlparse,
10 urlunparse,
11)
12import subprocess
13from charmhelpers.core.hookenv import (18from charmhelpers.core.hookenv import (
14 config,19 config,
15 log,20 log,
16)21)
17import os22
1823import six
1924if six.PY3:
20CLOUD_ARCHIVE = """# Ubuntu Cloud Archive25 from urllib.parse import urlparse, urlunparse
21deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main26else:
22"""27 from urlparse import urlparse, urlunparse
23PROPOSED_POCKET = """# Proposed28
24deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
25"""
26CLOUD_ARCHIVE_POCKETS = {
27 # Folsom
28 'folsom': 'precise-updates/folsom',
29 'precise-folsom': 'precise-updates/folsom',
30 'precise-folsom/updates': 'precise-updates/folsom',
31 'precise-updates/folsom': 'precise-updates/folsom',
32 'folsom/proposed': 'precise-proposed/folsom',
33 'precise-folsom/proposed': 'precise-proposed/folsom',
34 'precise-proposed/folsom': 'precise-proposed/folsom',
35 # Grizzly
36 'grizzly': 'precise-updates/grizzly',
37 'precise-grizzly': 'precise-updates/grizzly',
38 'precise-grizzly/updates': 'precise-updates/grizzly',
39 'precise-updates/grizzly': 'precise-updates/grizzly',
40 'grizzly/proposed': 'precise-proposed/grizzly',
41 'precise-grizzly/proposed': 'precise-proposed/grizzly',
42 'precise-proposed/grizzly': 'precise-proposed/grizzly',
43 # Havana
44 'havana': 'precise-updates/havana',
45 'precise-havana': 'precise-updates/havana',
46 'precise-havana/updates': 'precise-updates/havana',
47 'precise-updates/havana': 'precise-updates/havana',
48 'havana/proposed': 'precise-proposed/havana',
49 'precise-havana/proposed': 'precise-proposed/havana',
50 'precise-proposed/havana': 'precise-proposed/havana',
51 # Icehouse
52 'icehouse': 'precise-updates/icehouse',
53 'precise-icehouse': 'precise-updates/icehouse',
54 'precise-icehouse/updates': 'precise-updates/icehouse',
55 'precise-updates/icehouse': 'precise-updates/icehouse',
56 'icehouse/proposed': 'precise-proposed/icehouse',
57 'precise-icehouse/proposed': 'precise-proposed/icehouse',
58 'precise-proposed/icehouse': 'precise-proposed/icehouse',
59 # Juno
60 'juno': 'trusty-updates/juno',
61 'trusty-juno': 'trusty-updates/juno',
62 'trusty-juno/updates': 'trusty-updates/juno',
63 'trusty-updates/juno': 'trusty-updates/juno',
64 'juno/proposed': 'trusty-proposed/juno',
65 'juno/proposed': 'trusty-proposed/juno',
66 'trusty-juno/proposed': 'trusty-proposed/juno',
67 'trusty-proposed/juno': 'trusty-proposed/juno',
68}
6929
70# 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
71# least- to most-specific URL matching.31# least- to most-specific URL matching.
@@ -75,10 +35,6 @@
75 'charmhelpers.fetch.giturl.GitUrlFetchHandler',35 'charmhelpers.fetch.giturl.GitUrlFetchHandler',
76)36)
7737
78APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
79APT_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks.
80APT_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
81
8238
83class SourceConfigError(Exception):39class SourceConfigError(Exception):
84 pass40 pass
@@ -116,172 +72,38 @@
116 return urlunparse(parts)72 return urlunparse(parts)
11773
11874
119def filter_installed_packages(packages):75__platform__ = get_platform()
120 """Returns a list of packages that require installation"""76module = "charmhelpers.fetch.%s" % __platform__
121 cache = apt_cache()77fetch = importlib.import_module(module)
122 _pkgs = []78
123 for package in packages:79filter_installed_packages = fetch.filter_installed_packages
124 try:80install = fetch.install
125 p = cache[package]81upgrade = fetch.upgrade
126 p.current_ver or _pkgs.append(package)82update = fetch.update
127 except KeyError:83purge = fetch.purge
128 log('Package {} has no installation candidate.'.format(package),84add_source = fetch.add_source
129 level='WARNING')85
130 _pkgs.append(package)86if __platform__ == "ubuntu":
131 return _pkgs87 apt_cache = fetch.apt_cache
13288 apt_install = fetch.install
13389 apt_update = fetch.update
134def apt_cache(in_memory=True):90 apt_upgrade = fetch.upgrade
135 """Build and return an apt cache"""91 apt_purge = fetch.purge
136 import apt_pkg92 apt_mark = fetch.apt_mark
137 apt_pkg.init()93 apt_hold = fetch.apt_hold
138 if in_memory:94 apt_unhold = fetch.apt_unhold
139 apt_pkg.config.set("Dir::Cache::pkgcache", "")95 get_upstream_version = fetch.get_upstream_version
140 apt_pkg.config.set("Dir::Cache::srcpkgcache", "")96elif __platform__ == "centos":
141 return apt_pkg.Cache()97 yum_search = fetch.yum_search
142
143
144def apt_install(packages, options=None, fatal=False):
145 """Install one or more packages"""
146 if options is None:
147 options = ['--option=Dpkg::Options::=--force-confold']
148
149 cmd = ['apt-get', '--assume-yes']
150 cmd.extend(options)
151 cmd.append('install')
152 if isinstance(packages, basestring):
153 cmd.append(packages)
154 else:
155 cmd.extend(packages)
156 log("Installing {} with options: {}".format(packages,
157 options))
158 _run_apt_command(cmd, fatal)
159
160
161def apt_upgrade(options=None, fatal=False, dist=False):
162 """Upgrade all packages"""
163 if options is None:
164 options = ['--option=Dpkg::Options::=--force-confold']
165
166 cmd = ['apt-get', '--assume-yes']
167 cmd.extend(options)
168 if dist:
169 cmd.append('dist-upgrade')
170 else:
171 cmd.append('upgrade')
172 log("Upgrading with options: {}".format(options))
173 _run_apt_command(cmd, fatal)
174
175
176def apt_update(fatal=False):
177 """Update local apt cache"""
178 cmd = ['apt-get', 'update']
179 _run_apt_command(cmd, fatal)
180
181
182def apt_purge(packages, fatal=False):
183 """Purge one or more packages"""
184 cmd = ['apt-get', '--assume-yes', 'purge']
185 if isinstance(packages, basestring):
186 cmd.append(packages)
187 else:
188 cmd.extend(packages)
189 log("Purging {}".format(packages))
190 _run_apt_command(cmd, fatal)
191
192
193def apt_hold(packages, fatal=False):
194 """Hold one or more packages"""
195 cmd = ['apt-mark', 'hold']
196 if isinstance(packages, basestring):
197 cmd.append(packages)
198 else:
199 cmd.extend(packages)
200 log("Holding {}".format(packages))
201
202 if fatal:
203 subprocess.check_call(cmd)
204 else:
205 subprocess.call(cmd)
206
207
208def add_source(source, key=None):
209 """Add a package source to this system.
210
211 @param source: a URL or sources.list entry, as supported by
212 add-apt-repository(1). Examples::
213
214 ppa:charmers/example
215 deb https://stub:key@private.example.com/ubuntu trusty main
216
217 In addition:
218 'proposed:' may be used to enable the standard 'proposed'
219 pocket for the release.
220 'cloud:' may be used to activate official cloud archive pockets,
221 such as 'cloud:icehouse'
222 'distro' may be used as a noop
223
224 @param key: A key to be added to the system's APT keyring and used
225 to verify the signatures on packages. Ideally, this should be an
226 ASCII format GPG public key including the block headers. A GPG key
227 id may also be used, but be aware that only insecure protocols are
228 available to retrieve the actual public key from a public keyserver
229 placing your Juju environment at risk. ppa and cloud archive keys
230 are securely added automtically, so sould not be provided.
231 """
232 if source is None:
233 log('Source is not present. Skipping')
234 return
235
236 if (source.startswith('ppa:') or
237 source.startswith('http') or
238 source.startswith('deb ') or
239 source.startswith('cloud-archive:')):
240 subprocess.check_call(['add-apt-repository', '--yes', source])
241 elif source.startswith('cloud:'):
242 apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
243 fatal=True)
244 pocket = source.split(':')[-1]
245 if pocket not in CLOUD_ARCHIVE_POCKETS:
246 raise SourceConfigError(
247 'Unsupported cloud: source option %s' %
248 pocket)
249 actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
250 with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
251 apt.write(CLOUD_ARCHIVE.format(actual_pocket))
252 elif source == 'proposed':
253 release = lsb_release()['DISTRIB_CODENAME']
254 with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
255 apt.write(PROPOSED_POCKET.format(release))
256 elif source == 'distro':
257 pass
258 else:
259 log("Unknown source: {!r}".format(source))
260
261 if key:
262 if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
263 with NamedTemporaryFile() as key_file:
264 key_file.write(key)
265 key_file.flush()
266 key_file.seek(0)
267 subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file)
268 else:
269 # Note that hkp: is in no way a secure protocol. Using a
270 # GPG key id is pointless from a security POV unless you
271 # absolutely trust your network and DNS.
272 subprocess.check_call(['apt-key', 'adv', '--keyserver',
273 'hkp://keyserver.ubuntu.com:80', '--recv',
274 key])
27598
27699
277def configure_sources(update=False,100def configure_sources(update=False,
278 sources_var='install_sources',101 sources_var='install_sources',
279 keys_var='install_keys'):102 keys_var='install_keys'):
280 """103 """Configure multiple sources from charm configuration.
281 Configure multiple sources from charm configuration.
282104
283 The lists are encoded as yaml fragments in the configuration.105 The lists are encoded as yaml fragments in the configuration.
284 The frament needs to be included as a string. Sources and their106 The fragment needs to be included as a string. Sources and their
285 corresponding keys are of the types supported by add_source().107 corresponding keys are of the types supported by add_source().
286108
287 Example config:109 Example config:
@@ -297,14 +119,14 @@
297 sources = safe_load((config(sources_var) or '').strip()) or []119 sources = safe_load((config(sources_var) or '').strip()) or []
298 keys = safe_load((config(keys_var) or '').strip()) or None120 keys = safe_load((config(keys_var) or '').strip()) or None
299121
300 if isinstance(sources, basestring):122 if isinstance(sources, six.string_types):
301 sources = [sources]123 sources = [sources]
302124
303 if keys is None:125 if keys is None:
304 for source in sources:126 for source in sources:
305 add_source(source, None)127 add_source(source, None)
306 else:128 else:
307 if isinstance(keys, basestring):129 if isinstance(keys, six.string_types):
308 keys = [keys]130 keys = [keys]
309131
310 if len(sources) != len(keys):132 if len(sources) != len(keys):
@@ -313,12 +135,11 @@
313 for source, key in zip(sources, keys):135 for source, key in zip(sources, keys):
314 add_source(source, key)136 add_source(source, key)
315 if update:137 if update:
316 apt_update(fatal=True)138 fetch.update(fatal=True)
317139
318140
319def install_remote(source, *args, **kwargs):141def install_remote(source, *args, **kwargs):
320 """142 """Install a file tree from a remote source.
321 Install a file tree from a remote source
322143
323 The specified source should be a url of the form:144 The specified source should be a url of the form:
324 scheme://[host]/path[#[option=value][&...]]145 scheme://[host]/path[#[option=value][&...]]
@@ -341,18 +162,17 @@
341 # We ONLY check for True here because can_handle may return a string162 # We ONLY check for True here because can_handle may return a string
342 # explaining why it can't handle a given source.163 # explaining why it can't handle a given source.
343 handlers = [h for h in plugins() if h.can_handle(source) is True]164 handlers = [h for h in plugins() if h.can_handle(source) is True]
344 installed_to = None
345 for handler in handlers:165 for handler in handlers:
346 try:166 try:
347 installed_to = handler.install(source, *args, **kwargs)167 return handler.install(source, *args, **kwargs)
348 except UnhandledSource:168 except UnhandledSource as e:
349 pass169 log('Install source attempt unsuccessful: {}'.format(e),
350 if not installed_to:170 level='WARNING')
351 raise UnhandledSource("No handler found for source {}".format(source))171 raise UnhandledSource("No handler found for source {}".format(source))
352 return installed_to
353172
354173
355def install_from_config(config_var_name):174def install_from_config(config_var_name):
175 """Install a file from config."""
356 charm_config = config()176 charm_config = config()
357 source = charm_config[config_var_name]177 source = charm_config[config_var_name]
358 return install_remote(source)178 return install_remote(source)
@@ -369,46 +189,9 @@
369 importlib.import_module(package),189 importlib.import_module(package),
370 classname)190 classname)
371 plugin_list.append(handler_class())191 plugin_list.append(handler_class())
372 except (ImportError, AttributeError):192 except NotImplementedError:
373 # Skip missing plugins so that they can be ommitted from193 # Skip missing plugins so that they can be ommitted from
374 # installation if desired194 # installation if desired
375 log("FetchHandler {} not found, skipping plugin".format(195 log("FetchHandler {} not found, skipping plugin".format(
376 handler_name))196 handler_name))
377 return plugin_list197 return plugin_list
378
379
380def _run_apt_command(cmd, fatal=False):
381 """
382 Run an APT command, checking output and retrying if the fatal flag is set
383 to True.
384
385 :param: cmd: str: The apt command to run.
386 :param: fatal: bool: Whether the command's output should be checked and
387 retried.
388 """
389 env = os.environ.copy()
390
391 if 'DEBIAN_FRONTEND' not in env:
392 env['DEBIAN_FRONTEND'] = 'noninteractive'
393
394 if fatal:
395 retry_count = 0
396 result = None
397
398 # If the command is considered "fatal", we need to retry if the apt
399 # lock was not acquired.
400
401 while result is None or result == APT_NO_LOCK:
402 try:
403 result = subprocess.check_call(cmd, env=env)
404 except subprocess.CalledProcessError, e:
405 retry_count = retry_count + 1
406 if retry_count > APT_NO_LOCK_RETRY_COUNT:
407 raise
408 result = e.returncode
409 log("Couldn't acquire DPKG lock. Will retry in {} seconds."
410 "".format(APT_NO_LOCK_RETRY_DELAY))
411 time.sleep(APT_NO_LOCK_RETRY_DELAY)
412
413 else:
414 subprocess.call(cmd, env=env)
415198
=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
--- hooks/charmhelpers/fetch/archiveurl.py 2014-10-24 16:35:00 +0000
+++ hooks/charmhelpers/fetch/archiveurl.py 2017-05-11 14:56:57 +0000
@@ -1,8 +1,20 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
1import os15import os
2import urllib2
3from urllib import urlretrieve
4import urlparse
5import hashlib16import hashlib
17import re
618
7from charmhelpers.fetch import (19from charmhelpers.fetch import (
8 BaseFetchHandler,20 BaseFetchHandler,
@@ -14,6 +26,41 @@
14)26)
15from charmhelpers.core.host import mkdir, check_hash27from charmhelpers.core.host import mkdir, check_hash
1628
29import six
30if six.PY3:
31 from urllib.request import (
32 build_opener, install_opener, urlopen, urlretrieve,
33 HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler,
34 )
35 from urllib.parse import urlparse, urlunparse, parse_qs
36 from urllib.error import URLError
37else:
38 from urllib import urlretrieve
39 from urllib2 import (
40 build_opener, install_opener, urlopen,
41 HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler,
42 URLError
43 )
44 from urlparse import urlparse, urlunparse, parse_qs
45
46
47def splituser(host):
48 '''urllib.splituser(), but six's support of this seems broken'''
49 _userprog = re.compile('^(.*)@(.*)$')
50 match = _userprog.match(host)
51 if match:
52 return match.group(1, 2)
53 return None, host
54
55
56def splitpasswd(user):
57 '''urllib.splitpasswd(), but six's support of this is missing'''
58 _passwdprog = re.compile('^([^:]*):(.*)$', re.S)
59 match = _passwdprog.match(user)
60 if match:
61 return match.group(1, 2)
62 return user, None
63
1764
18class ArchiveUrlFetchHandler(BaseFetchHandler):65class ArchiveUrlFetchHandler(BaseFetchHandler):
19 """66 """
@@ -28,6 +75,8 @@
28 def can_handle(self, source):75 def can_handle(self, source):
29 url_parts = self.parse_url(source)76 url_parts = self.parse_url(source)
30 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.
31 return "Wrong source type"80 return "Wrong source type"
32 if get_archive_handler(self.base_url(source)):81 if get_archive_handler(self.base_url(source)):
33 return True82 return True
@@ -42,22 +91,22 @@
42 """91 """
43 # propogate all exceptions92 # propogate all exceptions
44 # URLError, OSError, etc93 # URLError, OSError, etc
45 proto, netloc, path, params, query, fragment = urlparse.urlparse(source)94 proto, netloc, path, params, query, fragment = urlparse(source)
46 if proto in ('http', 'https'):95 if proto in ('http', 'https'):
47 auth, barehost = urllib2.splituser(netloc)96 auth, barehost = splituser(netloc)
48 if auth is not None:97 if auth is not None:
49 source = urlparse.urlunparse((proto, barehost, path, params, query, fragment))98 source = urlunparse((proto, barehost, path, params, query, fragment))
50 username, password = urllib2.splitpasswd(auth)99 username, password = splitpasswd(auth)
51 passman = urllib2.HTTPPasswordMgrWithDefaultRealm()100 passman = HTTPPasswordMgrWithDefaultRealm()
52 # Realm is set to None in add_password to force the username and password101 # Realm is set to None in add_password to force the username and password
53 # to be used whatever the realm102 # to be used whatever the realm
54 passman.add_password(None, source, username, password)103 passman.add_password(None, source, username, password)
55 authhandler = urllib2.HTTPBasicAuthHandler(passman)104 authhandler = HTTPBasicAuthHandler(passman)
56 opener = urllib2.build_opener(authhandler)105 opener = build_opener(authhandler)
57 urllib2.install_opener(opener)106 install_opener(opener)
58 response = urllib2.urlopen(source)107 response = urlopen(source)
59 try:108 try:
60 with open(dest, 'w') as dest_file:109 with open(dest, 'wb') as dest_file:
61 dest_file.write(response.read())110 dest_file.write(response.read())
62 except Exception as e:111 except Exception as e:
63 if os.path.isfile(dest):112 if os.path.isfile(dest):
@@ -91,18 +140,26 @@
91 url_parts = self.parse_url(source)140 url_parts = self.parse_url(source)
92 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), 'fetched')141 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), 'fetched')
93 if not os.path.exists(dest_dir):142 if not os.path.exists(dest_dir):
94 mkdir(dest_dir, perms=0755)143 mkdir(dest_dir, perms=0o755)
95 dld_file = os.path.join(dest_dir, os.path.basename(url_parts.path))144 dld_file = os.path.join(dest_dir, os.path.basename(url_parts.path))
96 try:145 try:
97 self.download(source, dld_file)146 self.download(source, dld_file)
98 except urllib2.URLError as e:147 except URLError as e:
99 raise UnhandledSource(e.reason)148 raise UnhandledSource(e.reason)
100 except OSError as e:149 except OSError as e:
101 raise UnhandledSource(e.strerror)150 raise UnhandledSource(e.strerror)
102 options = urlparse.parse_qs(url_parts.fragment)151 options = parse_qs(url_parts.fragment)
103 for key, value in options.items():152 for key, value in options.items():
104 if key in hashlib.algorithms:153 if not six.PY3:
105 check_hash(dld_file, value, key)154 algorithms = hashlib.algorithms
155 else:
156 algorithms = hashlib.algorithms_available
157 if key in algorithms:
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)
106 if checksum:163 if checksum:
107 check_hash(dld_file, checksum, hash_type)164 check_hash(dld_file, checksum, hash_type)
108 return extract(dld_file, dest)165 return extract(dld_file, dest)
109166
=== modified file 'hooks/charmhelpers/fetch/bzrurl.py'
--- hooks/charmhelpers/fetch/bzrurl.py 2014-10-24 16:35:00 +0000
+++ hooks/charmhelpers/fetch/bzrurl.py 2017-05-11 14:56:57 +0000
@@ -1,50 +1,76 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
1import os15import os
16from subprocess import check_call
2from charmhelpers.fetch import (17from charmhelpers.fetch import (
3 BaseFetchHandler,18 BaseFetchHandler,
4 UnhandledSource19 UnhandledSource,
20 filter_installed_packages,
21 install,
5)22)
6from charmhelpers.core.host import mkdir23from charmhelpers.core.host import mkdir
724
8try:25
9 from bzrlib.branch import Branch26if filter_installed_packages(['bzr']) != []:
10except ImportError:27 install(['bzr'])
11 from charmhelpers.fetch import apt_install28 if filter_installed_packages(['bzr']) != []:
12 apt_install("python-bzrlib")29 raise NotImplementedError('Unable to install bzr')
13 from bzrlib.branch import Branch
1430
1531
16class BzrUrlFetchHandler(BaseFetchHandler):32class BzrUrlFetchHandler(BaseFetchHandler):
17 """Handler for bazaar branches via generic and lp URLs"""33 """Handler for bazaar branches via generic and lp URLs."""
34
18 def can_handle(self, source):35 def can_handle(self, source):
19 url_parts = self.parse_url(source)36 url_parts = self.parse_url(source)
20 if url_parts.scheme not in ('bzr+ssh', 'lp'):37 if url_parts.scheme not in ('bzr+ssh', 'lp', ''):
21 return False38 return False
39 elif not url_parts.scheme:
40 return os.path.exists(os.path.join(source, '.bzr'))
22 else:41 else:
23 return True42 return True
2443
25 def branch(self, source, dest):44 def branch(self, source, dest, revno=None):
26 url_parts = self.parse_url(source)
27 # If we use lp:branchname scheme we need to load plugins
28 if not self.can_handle(source):45 if not self.can_handle(source):
29 raise UnhandledSource("Cannot handle {}".format(source))46 raise UnhandledSource("Cannot handle {}".format(source))
30 if url_parts.scheme == "lp":47 cmd_opts = []
31 from bzrlib.plugin import load_plugins48 if revno:
32 load_plugins()49 cmd_opts += ['-r', str(revno)]
33 try:50 if os.path.exists(dest):
34 remote_branch = Branch.open(source)51 cmd = ['bzr', 'pull']
35 remote_branch.bzrdir.sprout(dest).open_branch()52 cmd += cmd_opts
36 except Exception as e:53 cmd += ['--overwrite', '-d', dest, source]
37 raise e54 else:
55 cmd = ['bzr', 'branch']
56 cmd += cmd_opts
57 cmd += [source, dest]
58 check_call(cmd)
3859
39 def install(self, source):60 def install(self, source, dest=None, revno=None):
40 url_parts = self.parse_url(source)61 url_parts = self.parse_url(source)
41 branch_name = url_parts.path.strip("/").split("/")[-1]62 branch_name = url_parts.path.strip("/").split("/")[-1]
42 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",63 if dest:
43 branch_name)64 dest_dir = os.path.join(dest, branch_name)
44 if not os.path.exists(dest_dir):65 else:
45 mkdir(dest_dir, perms=0755)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
46 try:72 try:
47 self.branch(source, dest_dir)73 self.branch(source, dest_dir, revno)
48 except OSError as e:74 except OSError as e:
49 raise UnhandledSource(e.strerror)75 raise UnhandledSource(e.strerror)
50 return dest_dir76 return dest_dir
5177
=== 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-05-11 14:56:57 +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])
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 2014-10-24 16:35:00 +0000
+++ hooks/charmhelpers/fetch/giturl.py 2017-05-11 14:56:57 +0000
@@ -1,44 +1,69 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
1import os15import os
16from subprocess import check_call, CalledProcessError
2from charmhelpers.fetch import (17from charmhelpers.fetch import (
3 BaseFetchHandler,18 BaseFetchHandler,
4 UnhandledSource19 UnhandledSource,
20 filter_installed_packages,
21 install,
5)22)
6from charmhelpers.core.host import mkdir
723
8try:24if filter_installed_packages(['git']) != []:
9 from git import Repo25 install(['git'])
10except ImportError:26 if filter_installed_packages(['git']) != []:
11 from charmhelpers.fetch import apt_install27 raise NotImplementedError('Unable to install git')
12 apt_install("python-git")
13 from git import Repo
1428
1529
16class GitUrlFetchHandler(BaseFetchHandler):30class GitUrlFetchHandler(BaseFetchHandler):
17 """Handler for git branches via generic and github URLs"""31 """Handler for git branches via generic and github URLs."""
32
18 def can_handle(self, source):33 def can_handle(self, source):
19 url_parts = self.parse_url(source)34 url_parts = self.parse_url(source)
20 #TODO (mattyw) no support for ssh git@ yet35 # TODO (mattyw) no support for ssh git@ yet
21 if url_parts.scheme not in ('http', 'https', 'git'):36 if url_parts.scheme not in ('http', 'https', 'git', ''):
22 return False37 return False
38 elif not url_parts.scheme:
39 return os.path.exists(os.path.join(source, '.git'))
23 else:40 else:
24 return True41 return True
2542
26 def clone(self, source, dest, branch):43 def clone(self, source, dest, branch="master", depth=None):
27 if not self.can_handle(source):44 if not self.can_handle(source):
28 raise UnhandledSource("Cannot handle {}".format(source))45 raise UnhandledSource("Cannot handle {}".format(source))
2946
30 repo = Repo.clone_from(source, dest)47 if os.path.exists(dest):
31 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)
3254
33 def install(self, source, branch="master"):55 def install(self, source, branch="master", dest=None, depth=None):
34 url_parts = self.parse_url(source)56 url_parts = self.parse_url(source)
35 branch_name = url_parts.path.strip("/").split("/")[-1]57 branch_name = url_parts.path.strip("/").split("/")[-1]
36 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",58 if dest:
37 branch_name)59 dest_dir = os.path.join(dest, branch_name)
38 if not os.path.exists(dest_dir):60 else:
39 mkdir(dest_dir, perms=0755)61 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
62 branch_name)
40 try:63 try:
41 self.clone(source, dest_dir, branch)64 self.clone(source, dest_dir, branch, depth)
65 except CalledProcessError as e:
66 raise UnhandledSource(e)
42 except OSError as e:67 except OSError as e:
43 raise UnhandledSource(e.strerror)68 raise UnhandledSource(e.strerror)
44 return dest_dir69 return dest_dir
4570
=== 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-05-11 14:56:57 +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-05-11 14:56:57 +0000
@@ -0,0 +1,364 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import os
16import six
17import time
18import subprocess
19
20from tempfile import NamedTemporaryFile
21from charmhelpers.core.host import (
22 lsb_release
23)
24from charmhelpers.core.hookenv import log
25from charmhelpers.fetch import SourceConfigError
26
27CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
28deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
29"""
30
31PROPOSED_POCKET = """# Proposed
32deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
33"""
34
35CLOUD_ARCHIVE_POCKETS = {
36 # Folsom
37 'folsom': 'precise-updates/folsom',
38 'precise-folsom': 'precise-updates/folsom',
39 'precise-folsom/updates': 'precise-updates/folsom',
40 'precise-updates/folsom': 'precise-updates/folsom',
41 'folsom/proposed': 'precise-proposed/folsom',
42 'precise-folsom/proposed': 'precise-proposed/folsom',
43 'precise-proposed/folsom': 'precise-proposed/folsom',
44 # Grizzly
45 'grizzly': 'precise-updates/grizzly',
46 'precise-grizzly': 'precise-updates/grizzly',
47 'precise-grizzly/updates': 'precise-updates/grizzly',
48 'precise-updates/grizzly': 'precise-updates/grizzly',
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches