Merge lp:~xavpaice/charms/trusty/thruk-agent/trunk into lp:~canonical-bootstack/charms/trusty/thruk-agent/trunk
- Trusty Tahr (14.04)
- trunk
- Merge into trunk
Proposed by
Xav Paice
Status: | Merged |
---|---|
Approved by: | James Troup |
Approved revision: | 35 |
Merged at revision: | 35 |
Proposed branch: | lp:~xavpaice/charms/trusty/thruk-agent/trunk |
Merge into: | lp:~canonical-bootstack/charms/trusty/thruk-agent/trunk |
Diff against target: |
4790 lines (+3050/-815) 33 files modified
charm-helpers.yaml (+1/-0) config.yaml (+18/-4) hooks/actions.py (+17/-0) hooks/charmhelpers/__init__.py (+72/-13) hooks/charmhelpers/core/__init__.py (+11/-13) hooks/charmhelpers/core/decorators.py (+11/-13) hooks/charmhelpers/core/files.py (+43/-0) hooks/charmhelpers/core/fstab.py (+11/-13) hooks/charmhelpers/core/hookenv.py (+530/-56) hooks/charmhelpers/core/host.py (+629/-155) hooks/charmhelpers/core/host_factory/centos.py (+72/-0) hooks/charmhelpers/core/host_factory/ubuntu.py (+89/-0) hooks/charmhelpers/core/hugepage.py (+69/-0) hooks/charmhelpers/core/kernel.py (+72/-0) hooks/charmhelpers/core/kernel_factory/centos.py (+17/-0) hooks/charmhelpers/core/kernel_factory/ubuntu.py (+13/-0) hooks/charmhelpers/core/services/__init__.py (+11/-13) hooks/charmhelpers/core/services/base.py (+54/-32) hooks/charmhelpers/core/services/helpers.py (+42/-19) hooks/charmhelpers/core/strutils.py (+96/-15) hooks/charmhelpers/core/sysctl.py (+11/-13) hooks/charmhelpers/core/templating.py (+40/-24) hooks/charmhelpers/core/unitdata.py (+72/-31) hooks/charmhelpers/fetch/__init__.py (+54/-288) hooks/charmhelpers/fetch/archiveurl.py (+19/-15) hooks/charmhelpers/fetch/bzrurl.py (+48/-50) hooks/charmhelpers/fetch/centos.py (+171/-0) hooks/charmhelpers/fetch/giturl.py (+37/-39) hooks/charmhelpers/fetch/snap.py (+122/-0) hooks/charmhelpers/fetch/ubuntu.py (+568/-0) hooks/charmhelpers/osplatform.py (+25/-0) hooks/install (+4/-9) hooks/services.py (+1/-0) |
To merge this branch: | bzr merge lp:~xavpaice/charms/trusty/thruk-agent/trunk |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
James Troup (community) | Approve | ||
Review via email: mp+327579@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'charm-helpers.yaml' | |||
2 | --- charm-helpers.yaml 2015-04-08 05:57:06 +0000 | |||
3 | +++ charm-helpers.yaml 2017-07-18 06:08:45 +0000 | |||
4 | @@ -3,3 +3,4 @@ | |||
5 | 3 | include: | 3 | include: |
6 | 4 | - core | 4 | - core |
7 | 5 | - fetch | 5 | - fetch |
8 | 6 | - osplatform | ||
9 | 6 | 7 | ||
10 | === modified file 'config.yaml' | |||
11 | --- config.yaml 2015-06-15 00:19:17 +0000 | |||
12 | +++ config.yaml 2017-07-18 06:08:45 +0000 | |||
13 | @@ -3,10 +3,6 @@ | |||
14 | 3 | type: string | 3 | type: string |
15 | 4 | default: "ppa:canonical-bootstack/thruk" | 4 | default: "ppa:canonical-bootstack/thruk" |
16 | 5 | description: "PPA to install thruk" | 5 | description: "PPA to install thruk" |
17 | 6 | nagios_context: | ||
18 | 7 | type: string | ||
19 | 8 | default: "bootstack" | ||
20 | 9 | description: "Name for the thruk instance" | ||
21 | 10 | livestatus_path: | 6 | livestatus_path: |
22 | 11 | type: string | 7 | type: string |
23 | 12 | default: "/var/lib/nagios3/livestatus/socket" | 8 | default: "/var/lib/nagios3/livestatus/socket" |
24 | @@ -24,4 +20,22 @@ | |||
25 | 24 | 20 | ||
26 | 25 | If you're running multiple environments with the same services in them | 21 | If you're running multiple environments with the same services in them |
27 | 26 | this allows you to differentiate between them. | 22 | this allows you to differentiate between them. |
28 | 23 | source: | ||
29 | 24 | type: string | ||
30 | 25 | default: "http://labs.consol.de/repo/stable/ubuntu xenial main" | ||
31 | 26 | description: | | ||
32 | 27 | Optional configuration to support use of additional sources such as: | ||
33 | 28 | |||
34 | 29 | - ppa:myteam/ppa | ||
35 | 30 | - cloud:trusty-proposed/kilo | ||
36 | 31 | - http://my.archive.com/ubuntu main | ||
37 | 32 | |||
38 | 33 | The last option should be used in conjunction with the key configuration | ||
39 | 34 | option. | ||
40 | 35 | key: | ||
41 | 36 | type: string | ||
42 | 37 | default: F8C1CA08A57B9ED7 | ||
43 | 38 | description: | | ||
44 | 39 | Key ID to import to the apt keyring to support use with arbitary source | ||
45 | 40 | configuration from outside of Launchpad archives or PPA's. | ||
46 | 27 | 41 | ||
47 | 28 | 42 | ||
48 | === modified file 'hooks/actions.py' | |||
49 | --- hooks/actions.py 2015-08-25 22:59:55 +0000 | |||
50 | +++ hooks/actions.py 2017-07-18 06:08:45 +0000 | |||
51 | @@ -8,6 +8,10 @@ | |||
52 | 8 | import hashlib | 8 | import hashlib |
53 | 9 | # import thruk_helpers | 9 | # import thruk_helpers |
54 | 10 | 10 | ||
55 | 11 | from charmhelpers.fetch import ( | ||
56 | 12 | apt_install, apt_update, add_source | ||
57 | 13 | ) | ||
58 | 14 | |||
59 | 11 | 15 | ||
60 | 12 | def log_start(service_name): | 16 | def log_start(service_name): |
61 | 13 | hookenv.log('thruk-agent starting') | 17 | hookenv.log('thruk-agent starting') |
62 | @@ -81,3 +85,16 @@ | |||
63 | 81 | 85 | ||
64 | 82 | for rel_id in hookenv.relation_ids('thruk-agent'): | 86 | for rel_id in hookenv.relation_ids('thruk-agent'): |
65 | 83 | hookenv.relation_set(relation_id=rel_id, relation_settings=thruk_data) | 87 | hookenv.relation_set(relation_id=rel_id, relation_settings=thruk_data) |
66 | 88 | |||
67 | 89 | |||
68 | 90 | def update_ppa(service_name): | ||
69 | 91 | config = hookenv.config() | ||
70 | 92 | new_source = config.get('source') | ||
71 | 93 | prev_source = config.previous('source') | ||
72 | 94 | if prev_source is not None and prev_source != new_source: | ||
73 | 95 | subprocess.check_call(['add-apt-repository', | ||
74 | 96 | '--yes', '--remove', prev_source]) | ||
75 | 97 | add_source(config.get('source'), config.get('key', None)) | ||
76 | 98 | apt_update(fatal=True) | ||
77 | 99 | package_list = ["thruk", "pwgen", "apache2-utils"] | ||
78 | 100 | apt_install(packages=package_list, fatal=True) | ||
79 | 84 | 101 | ||
80 | === modified file 'hooks/charmhelpers/__init__.py' | |||
81 | --- hooks/charmhelpers/__init__.py 2015-04-08 05:57:06 +0000 | |||
82 | +++ hooks/charmhelpers/__init__.py 2017-07-18 06:08:45 +0000 | |||
83 | @@ -1,21 +1,24 @@ | |||
84 | 1 | # Copyright 2014-2015 Canonical Limited. | 1 | # Copyright 2014-2015 Canonical Limited. |
85 | 2 | # | 2 | # |
99 | 3 | # This file is part of charm-helpers. | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
100 | 4 | # | 4 | # you may not use this file except in compliance with the License. |
101 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | 5 | # You may obtain a copy of the License at |
102 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | 6 | # |
103 | 7 | # published by the Free Software Foundation. | 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
104 | 8 | # | 8 | # |
105 | 9 | # charm-helpers is distributed in the hope that it will be useful, | 9 | # Unless required by applicable law or agreed to in writing, software |
106 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
107 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
108 | 12 | # GNU Lesser General Public License for more details. | 12 | # See the License for the specific language governing permissions and |
109 | 13 | # | 13 | # limitations under the License. |
97 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
98 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
110 | 16 | 14 | ||
111 | 17 | # Bootstrap charm-helpers, installing its dependencies if necessary using | 15 | # Bootstrap charm-helpers, installing its dependencies if necessary using |
112 | 18 | # only standard libraries. | 16 | # only standard libraries. |
113 | 17 | from __future__ import print_function | ||
114 | 18 | from __future__ import absolute_import | ||
115 | 19 | |||
116 | 20 | import functools | ||
117 | 21 | import inspect | ||
118 | 19 | import subprocess | 22 | import subprocess |
119 | 20 | import sys | 23 | import sys |
120 | 21 | 24 | ||
121 | @@ -36,3 +39,59 @@ | |||
122 | 36 | else: | 39 | else: |
123 | 37 | subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml']) | 40 | subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml']) |
124 | 38 | import yaml # flake8: noqa | 41 | import yaml # flake8: noqa |
125 | 42 | |||
126 | 43 | |||
127 | 44 | # Holds a list of mapping of mangled function names that have been deprecated | ||
128 | 45 | # using the @deprecate decorator below. This is so that the warning is only | ||
129 | 46 | # printed once for each usage of the function. | ||
130 | 47 | __deprecated_functions = {} | ||
131 | 48 | |||
132 | 49 | |||
133 | 50 | def deprecate(warning, date=None, log=None): | ||
134 | 51 | """Add a deprecation warning the first time the function is used. | ||
135 | 52 | The date, which is a string in semi-ISO8660 format indicate the year-month | ||
136 | 53 | that the function is officially going to be removed. | ||
137 | 54 | |||
138 | 55 | usage: | ||
139 | 56 | |||
140 | 57 | @deprecate('use core/fetch/add_source() instead', '2017-04') | ||
141 | 58 | def contributed_add_source_thing(...): | ||
142 | 59 | ... | ||
143 | 60 | |||
144 | 61 | And it then prints to the log ONCE that the function is deprecated. | ||
145 | 62 | The reason for passing the logging function (log) is so that hookenv.log | ||
146 | 63 | can be used for a charm if needed. | ||
147 | 64 | |||
148 | 65 | :param warning: String to indicat where it has moved ot. | ||
149 | 66 | :param date: optional sting, in YYYY-MM format to indicate when the | ||
150 | 67 | function will definitely (probably) be removed. | ||
151 | 68 | :param log: The log function to call to log. If not, logs to stdout | ||
152 | 69 | """ | ||
153 | 70 | def wrap(f): | ||
154 | 71 | |||
155 | 72 | @functools.wraps(f) | ||
156 | 73 | def wrapped_f(*args, **kwargs): | ||
157 | 74 | try: | ||
158 | 75 | module = inspect.getmodule(f) | ||
159 | 76 | file = inspect.getsourcefile(f) | ||
160 | 77 | lines = inspect.getsourcelines(f) | ||
161 | 78 | f_name = "{}-{}-{}..{}-{}".format( | ||
162 | 79 | module.__name__, file, lines[0], lines[-1], f.__name__) | ||
163 | 80 | except (IOError, TypeError): | ||
164 | 81 | # assume it was local, so just use the name of the function | ||
165 | 82 | f_name = f.__name__ | ||
166 | 83 | if f_name not in __deprecated_functions: | ||
167 | 84 | __deprecated_functions[f_name] = True | ||
168 | 85 | s = "DEPRECATION WARNING: Function {} is being removed".format( | ||
169 | 86 | f.__name__) | ||
170 | 87 | if date: | ||
171 | 88 | s = "{} on/around {}".format(s, date) | ||
172 | 89 | if warning: | ||
173 | 90 | s = "{} : {}".format(s, warning) | ||
174 | 91 | if log: | ||
175 | 92 | log(s) | ||
176 | 93 | else: | ||
177 | 94 | print(s) | ||
178 | 95 | return f(*args, **kwargs) | ||
179 | 96 | return wrapped_f | ||
180 | 97 | return wrap | ||
181 | 39 | 98 | ||
182 | === modified file 'hooks/charmhelpers/core/__init__.py' | |||
183 | --- hooks/charmhelpers/core/__init__.py 2015-04-08 05:57:06 +0000 | |||
184 | +++ hooks/charmhelpers/core/__init__.py 2017-07-18 06:08:45 +0000 | |||
185 | @@ -1,15 +1,13 @@ | |||
186 | 1 | # Copyright 2014-2015 Canonical Limited. | 1 | # Copyright 2014-2015 Canonical Limited. |
187 | 2 | # | 2 | # |
201 | 3 | # This file is part of charm-helpers. | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
202 | 4 | # | 4 | # you may not use this file except in compliance with the License. |
203 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | 5 | # You may obtain a copy of the License at |
204 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | 6 | # |
205 | 7 | # published by the Free Software Foundation. | 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
206 | 8 | # | 8 | # |
207 | 9 | # charm-helpers is distributed in the hope that it will be useful, | 9 | # Unless required by applicable law or agreed to in writing, software |
208 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
209 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
210 | 12 | # GNU Lesser General Public License for more details. | 12 | # See the License for the specific language governing permissions and |
211 | 13 | # | 13 | # limitations under the License. |
199 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
200 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
212 | 16 | 14 | ||
213 | === modified file 'hooks/charmhelpers/core/decorators.py' | |||
214 | --- hooks/charmhelpers/core/decorators.py 2015-04-08 05:57:06 +0000 | |||
215 | +++ hooks/charmhelpers/core/decorators.py 2017-07-18 06:08:45 +0000 | |||
216 | @@ -1,18 +1,16 @@ | |||
217 | 1 | # Copyright 2014-2015 Canonical Limited. | 1 | # Copyright 2014-2015 Canonical Limited. |
218 | 2 | # | 2 | # |
232 | 3 | # This file is part of charm-helpers. | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
233 | 4 | # | 4 | # you may not use this file except in compliance with the License. |
234 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | 5 | # You may obtain a copy of the License at |
235 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | 6 | # |
236 | 7 | # published by the Free Software Foundation. | 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
237 | 8 | # | 8 | # |
238 | 9 | # charm-helpers is distributed in the hope that it will be useful, | 9 | # Unless required by applicable law or agreed to in writing, software |
239 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
240 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
241 | 12 | # GNU Lesser General Public License for more details. | 12 | # See the License for the specific language governing permissions and |
242 | 13 | # | 13 | # limitations under the License. |
230 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
231 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
243 | 16 | 14 | ||
244 | 17 | # | 15 | # |
245 | 18 | # Copyright 2014 Canonical Ltd. | 16 | # Copyright 2014 Canonical Ltd. |
246 | 19 | 17 | ||
247 | === added file 'hooks/charmhelpers/core/files.py' | |||
248 | --- hooks/charmhelpers/core/files.py 1970-01-01 00:00:00 +0000 | |||
249 | +++ hooks/charmhelpers/core/files.py 2017-07-18 06:08:45 +0000 | |||
250 | @@ -0,0 +1,43 @@ | |||
251 | 1 | #!/usr/bin/env python | ||
252 | 2 | # -*- coding: utf-8 -*- | ||
253 | 3 | |||
254 | 4 | # Copyright 2014-2015 Canonical Limited. | ||
255 | 5 | # | ||
256 | 6 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
257 | 7 | # you may not use this file except in compliance with the License. | ||
258 | 8 | # You may obtain a copy of the License at | ||
259 | 9 | # | ||
260 | 10 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
261 | 11 | # | ||
262 | 12 | # Unless required by applicable law or agreed to in writing, software | ||
263 | 13 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
264 | 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
265 | 15 | # See the License for the specific language governing permissions and | ||
266 | 16 | # limitations under the License. | ||
267 | 17 | |||
268 | 18 | __author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>' | ||
269 | 19 | |||
270 | 20 | import os | ||
271 | 21 | import subprocess | ||
272 | 22 | |||
273 | 23 | |||
274 | 24 | def sed(filename, before, after, flags='g'): | ||
275 | 25 | """ | ||
276 | 26 | Search and replaces the given pattern on filename. | ||
277 | 27 | |||
278 | 28 | :param filename: relative or absolute file path. | ||
279 | 29 | :param before: expression to be replaced (see 'man sed') | ||
280 | 30 | :param after: expression to replace with (see 'man sed') | ||
281 | 31 | :param flags: sed-compatible regex flags in example, to make | ||
282 | 32 | the search and replace case insensitive, specify ``flags="i"``. | ||
283 | 33 | The ``g`` flag is always specified regardless, so you do not | ||
284 | 34 | need to remember to include it when overriding this parameter. | ||
285 | 35 | :returns: If the sed command exit code was zero then return, | ||
286 | 36 | otherwise raise CalledProcessError. | ||
287 | 37 | """ | ||
288 | 38 | expression = r's/{0}/{1}/{2}'.format(before, | ||
289 | 39 | after, flags) | ||
290 | 40 | |||
291 | 41 | return subprocess.check_call(["sed", "-i", "-r", "-e", | ||
292 | 42 | expression, | ||
293 | 43 | os.path.expanduser(filename)]) | ||
294 | 0 | 44 | ||
295 | === modified file 'hooks/charmhelpers/core/fstab.py' | |||
296 | --- hooks/charmhelpers/core/fstab.py 2015-04-08 05:57:06 +0000 | |||
297 | +++ hooks/charmhelpers/core/fstab.py 2017-07-18 06:08:45 +0000 | |||
298 | @@ -3,19 +3,17 @@ | |||
299 | 3 | 3 | ||
300 | 4 | # Copyright 2014-2015 Canonical Limited. | 4 | # Copyright 2014-2015 Canonical Limited. |
301 | 5 | # | 5 | # |
315 | 6 | # This file is part of charm-helpers. | 6 | # Licensed under the Apache License, Version 2.0 (the "License"); |
316 | 7 | # | 7 | # you may not use this file except in compliance with the License. |
317 | 8 | # charm-helpers is free software: you can redistribute it and/or modify | 8 | # You may obtain a copy of the License at |
318 | 9 | # it under the terms of the GNU Lesser General Public License version 3 as | 9 | # |
319 | 10 | # published by the Free Software Foundation. | 10 | # http://www.apache.org/licenses/LICENSE-2.0 |
320 | 11 | # | 11 | # |
321 | 12 | # charm-helpers is distributed in the hope that it will be useful, | 12 | # Unless required by applicable law or agreed to in writing, software |
322 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | 13 | # distributed under the License is distributed on an "AS IS" BASIS, |
323 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
324 | 15 | # GNU Lesser General Public License for more details. | 15 | # See the License for the specific language governing permissions and |
325 | 16 | # | 16 | # limitations under the License. |
313 | 17 | # You should have received a copy of the GNU Lesser General Public License | ||
314 | 18 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
326 | 19 | 17 | ||
327 | 20 | import io | 18 | import io |
328 | 21 | import os | 19 | import os |
329 | 22 | 20 | ||
330 | === modified file 'hooks/charmhelpers/core/hookenv.py' | |||
331 | --- hooks/charmhelpers/core/hookenv.py 2015-04-08 05:57:06 +0000 | |||
332 | +++ hooks/charmhelpers/core/hookenv.py 2017-07-18 06:08:45 +0000 | |||
333 | @@ -1,18 +1,16 @@ | |||
334 | 1 | # Copyright 2014-2015 Canonical Limited. | 1 | # Copyright 2014-2015 Canonical Limited. |
335 | 2 | # | 2 | # |
349 | 3 | # This file is part of charm-helpers. | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
350 | 4 | # | 4 | # you may not use this file except in compliance with the License. |
351 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | 5 | # You may obtain a copy of the License at |
352 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | 6 | # |
353 | 7 | # published by the Free Software Foundation. | 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
354 | 8 | # | 8 | # |
355 | 9 | # charm-helpers is distributed in the hope that it will be useful, | 9 | # Unless required by applicable law or agreed to in writing, software |
356 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
357 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
358 | 12 | # GNU Lesser General Public License for more details. | 12 | # See the License for the specific language governing permissions and |
359 | 13 | # | 13 | # limitations under the License. |
347 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
348 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
360 | 16 | 14 | ||
361 | 17 | "Interactions with the Juju environment" | 15 | "Interactions with the Juju environment" |
362 | 18 | # Copyright 2013 Canonical Ltd. | 16 | # Copyright 2013 Canonical Ltd. |
363 | @@ -20,11 +18,18 @@ | |||
364 | 20 | # Authors: | 18 | # Authors: |
365 | 21 | # Charm Helpers Developers <juju@lists.ubuntu.com> | 19 | # Charm Helpers Developers <juju@lists.ubuntu.com> |
366 | 22 | 20 | ||
367 | 21 | from __future__ import print_function | ||
368 | 22 | import copy | ||
369 | 23 | from distutils.version import LooseVersion | ||
370 | 24 | from functools import wraps | ||
371 | 25 | import glob | ||
372 | 23 | import os | 26 | import os |
373 | 24 | import json | 27 | import json |
374 | 25 | import yaml | 28 | import yaml |
375 | 26 | import subprocess | 29 | import subprocess |
376 | 27 | import sys | 30 | import sys |
377 | 31 | import errno | ||
378 | 32 | import tempfile | ||
379 | 28 | from subprocess import CalledProcessError | 33 | from subprocess import CalledProcessError |
380 | 29 | 34 | ||
381 | 30 | import six | 35 | import six |
382 | @@ -56,15 +61,18 @@ | |||
383 | 56 | 61 | ||
384 | 57 | will cache the result of unit_get + 'test' for future calls. | 62 | will cache the result of unit_get + 'test' for future calls. |
385 | 58 | """ | 63 | """ |
386 | 64 | @wraps(func) | ||
387 | 59 | def wrapper(*args, **kwargs): | 65 | def wrapper(*args, **kwargs): |
388 | 60 | global cache | 66 | global cache |
389 | 61 | key = str((func, args, kwargs)) | 67 | key = str((func, args, kwargs)) |
390 | 62 | try: | 68 | try: |
391 | 63 | return cache[key] | 69 | return cache[key] |
392 | 64 | except KeyError: | 70 | except KeyError: |
396 | 65 | res = func(*args, **kwargs) | 71 | pass # Drop out of the exception handler scope. |
397 | 66 | cache[key] = res | 72 | res = func(*args, **kwargs) |
398 | 67 | return res | 73 | cache[key] = res |
399 | 74 | return res | ||
400 | 75 | wrapper._wrapped = func | ||
401 | 68 | return wrapper | 76 | return wrapper |
402 | 69 | 77 | ||
403 | 70 | 78 | ||
404 | @@ -87,7 +95,18 @@ | |||
405 | 87 | if not isinstance(message, six.string_types): | 95 | if not isinstance(message, six.string_types): |
406 | 88 | message = repr(message) | 96 | message = repr(message) |
407 | 89 | command += [message] | 97 | command += [message] |
409 | 90 | subprocess.call(command) | 98 | # Missing juju-log should not cause failures in unit tests |
410 | 99 | # Send log output to stderr | ||
411 | 100 | try: | ||
412 | 101 | subprocess.call(command) | ||
413 | 102 | except OSError as e: | ||
414 | 103 | if e.errno == errno.ENOENT: | ||
415 | 104 | if level: | ||
416 | 105 | message = "{}: {}".format(level, message) | ||
417 | 106 | message = "juju-log: {}".format(message) | ||
418 | 107 | print(message, file=sys.stderr) | ||
419 | 108 | else: | ||
420 | 109 | raise | ||
421 | 91 | 110 | ||
422 | 92 | 111 | ||
423 | 93 | class Serializable(UserDict): | 112 | class Serializable(UserDict): |
424 | @@ -153,9 +172,19 @@ | |||
425 | 153 | return os.environ.get('JUJU_RELATION', None) | 172 | return os.environ.get('JUJU_RELATION', None) |
426 | 154 | 173 | ||
427 | 155 | 174 | ||
431 | 156 | def relation_id(): | 175 | @cached |
432 | 157 | """The relation ID for the current relation hook""" | 176 | def relation_id(relation_name=None, service_or_unit=None): |
433 | 158 | return os.environ.get('JUJU_RELATION_ID', None) | 177 | """The relation ID for the current or a specified relation""" |
434 | 178 | if not relation_name and not service_or_unit: | ||
435 | 179 | return os.environ.get('JUJU_RELATION_ID', None) | ||
436 | 180 | elif relation_name and service_or_unit: | ||
437 | 181 | service_name = service_or_unit.split('/')[0] | ||
438 | 182 | for relid in relation_ids(relation_name): | ||
439 | 183 | remote_service = remote_service_name(relid) | ||
440 | 184 | if remote_service == service_name: | ||
441 | 185 | return relid | ||
442 | 186 | else: | ||
443 | 187 | raise ValueError('Must specify neither or both of relation_name and service_or_unit') | ||
444 | 159 | 188 | ||
445 | 160 | 189 | ||
446 | 161 | def local_unit(): | 190 | def local_unit(): |
447 | @@ -165,7 +194,7 @@ | |||
448 | 165 | 194 | ||
449 | 166 | def remote_unit(): | 195 | def remote_unit(): |
450 | 167 | """The remote unit for the current relation hook""" | 196 | """The remote unit for the current relation hook""" |
452 | 168 | return os.environ['JUJU_REMOTE_UNIT'] | 197 | return os.environ.get('JUJU_REMOTE_UNIT', None) |
453 | 169 | 198 | ||
454 | 170 | 199 | ||
455 | 171 | def service_name(): | 200 | def service_name(): |
456 | @@ -173,9 +202,20 @@ | |||
457 | 173 | return local_unit().split('/')[0] | 202 | return local_unit().split('/')[0] |
458 | 174 | 203 | ||
459 | 175 | 204 | ||
460 | 205 | @cached | ||
461 | 206 | def remote_service_name(relid=None): | ||
462 | 207 | """The remote service name for a given relation-id (or the current relation)""" | ||
463 | 208 | if relid is None: | ||
464 | 209 | unit = remote_unit() | ||
465 | 210 | else: | ||
466 | 211 | units = related_units(relid) | ||
467 | 212 | unit = units[0] if units else None | ||
468 | 213 | return unit.split('/')[0] if unit else None | ||
469 | 214 | |||
470 | 215 | |||
471 | 176 | def hook_name(): | 216 | def hook_name(): |
472 | 177 | """The name of the currently executing hook""" | 217 | """The name of the currently executing hook""" |
474 | 178 | return os.path.basename(sys.argv[0]) | 218 | return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0])) |
475 | 179 | 219 | ||
476 | 180 | 220 | ||
477 | 181 | class Config(dict): | 221 | class Config(dict): |
478 | @@ -225,23 +265,7 @@ | |||
479 | 225 | self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME) | 265 | self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME) |
480 | 226 | if os.path.exists(self.path): | 266 | if os.path.exists(self.path): |
481 | 227 | self.load_previous() | 267 | self.load_previous() |
499 | 228 | 268 | atexit(self._implicit_save) | |
483 | 229 | def __getitem__(self, key): | ||
484 | 230 | """For regular dict lookups, check the current juju config first, | ||
485 | 231 | then the previous (saved) copy. This ensures that user-saved values | ||
486 | 232 | will be returned by a dict lookup. | ||
487 | 233 | |||
488 | 234 | """ | ||
489 | 235 | try: | ||
490 | 236 | return dict.__getitem__(self, key) | ||
491 | 237 | except KeyError: | ||
492 | 238 | return (self._prev_dict or {})[key] | ||
493 | 239 | |||
494 | 240 | def keys(self): | ||
495 | 241 | prev_keys = [] | ||
496 | 242 | if self._prev_dict is not None: | ||
497 | 243 | prev_keys = self._prev_dict.keys() | ||
498 | 244 | return list(set(prev_keys + list(dict.keys(self)))) | ||
500 | 245 | 269 | ||
501 | 246 | def load_previous(self, path=None): | 270 | def load_previous(self, path=None): |
502 | 247 | """Load previous copy of config from disk. | 271 | """Load previous copy of config from disk. |
503 | @@ -260,6 +284,9 @@ | |||
504 | 260 | self.path = path or self.path | 284 | self.path = path or self.path |
505 | 261 | with open(self.path) as f: | 285 | with open(self.path) as f: |
506 | 262 | self._prev_dict = json.load(f) | 286 | self._prev_dict = json.load(f) |
507 | 287 | for k, v in copy.deepcopy(self._prev_dict).items(): | ||
508 | 288 | if k not in self: | ||
509 | 289 | self[k] = v | ||
510 | 263 | 290 | ||
511 | 264 | def changed(self, key): | 291 | def changed(self, key): |
512 | 265 | """Return True if the current value for this key is different from | 292 | """Return True if the current value for this key is different from |
513 | @@ -291,13 +318,13 @@ | |||
514 | 291 | instance. | 318 | instance. |
515 | 292 | 319 | ||
516 | 293 | """ | 320 | """ |
517 | 294 | if self._prev_dict: | ||
518 | 295 | for k, v in six.iteritems(self._prev_dict): | ||
519 | 296 | if k not in self: | ||
520 | 297 | self[k] = v | ||
521 | 298 | with open(self.path, 'w') as f: | 321 | with open(self.path, 'w') as f: |
522 | 299 | json.dump(self, f) | 322 | json.dump(self, f) |
523 | 300 | 323 | ||
524 | 324 | def _implicit_save(self): | ||
525 | 325 | if self.implicit_save: | ||
526 | 326 | self.save() | ||
527 | 327 | |||
528 | 301 | 328 | ||
529 | 302 | @cached | 329 | @cached |
530 | 303 | def config(scope=None): | 330 | def config(scope=None): |
531 | @@ -305,6 +332,8 @@ | |||
532 | 305 | config_cmd_line = ['config-get'] | 332 | config_cmd_line = ['config-get'] |
533 | 306 | if scope is not None: | 333 | if scope is not None: |
534 | 307 | config_cmd_line.append(scope) | 334 | config_cmd_line.append(scope) |
535 | 335 | else: | ||
536 | 336 | config_cmd_line.append('--all') | ||
537 | 308 | config_cmd_line.append('--format=json') | 337 | config_cmd_line.append('--format=json') |
538 | 309 | try: | 338 | try: |
539 | 310 | config_data = json.loads( | 339 | config_data = json.loads( |
540 | @@ -340,18 +369,49 @@ | |||
541 | 340 | """Set relation information for the current unit""" | 369 | """Set relation information for the current unit""" |
542 | 341 | relation_settings = relation_settings if relation_settings else {} | 370 | relation_settings = relation_settings if relation_settings else {} |
543 | 342 | relation_cmd_line = ['relation-set'] | 371 | relation_cmd_line = ['relation-set'] |
544 | 372 | accepts_file = "--file" in subprocess.check_output( | ||
545 | 373 | relation_cmd_line + ["--help"], universal_newlines=True) | ||
546 | 343 | if relation_id is not None: | 374 | if relation_id is not None: |
547 | 344 | relation_cmd_line.extend(('-r', relation_id)) | 375 | relation_cmd_line.extend(('-r', relation_id)) |
554 | 345 | for k, v in (list(relation_settings.items()) + list(kwargs.items())): | 376 | settings = relation_settings.copy() |
555 | 346 | if v is None: | 377 | settings.update(kwargs) |
556 | 347 | relation_cmd_line.append('{}='.format(k)) | 378 | for key, value in settings.items(): |
557 | 348 | else: | 379 | # Force value to be a string: it always should, but some call |
558 | 349 | relation_cmd_line.append('{}={}'.format(k, v)) | 380 | # sites pass in things like dicts or numbers. |
559 | 350 | subprocess.check_call(relation_cmd_line) | 381 | if value is not None: |
560 | 382 | settings[key] = "{}".format(value) | ||
561 | 383 | if accepts_file: | ||
562 | 384 | # --file was introduced in Juju 1.23.2. Use it by default if | ||
563 | 385 | # available, since otherwise we'll break if the relation data is | ||
564 | 386 | # too big. Ideally we should tell relation-set to read the data from | ||
565 | 387 | # stdin, but that feature is broken in 1.23.2: Bug #1454678. | ||
566 | 388 | with tempfile.NamedTemporaryFile(delete=False) as settings_file: | ||
567 | 389 | settings_file.write(yaml.safe_dump(settings).encode("utf-8")) | ||
568 | 390 | subprocess.check_call( | ||
569 | 391 | relation_cmd_line + ["--file", settings_file.name]) | ||
570 | 392 | os.remove(settings_file.name) | ||
571 | 393 | else: | ||
572 | 394 | for key, value in settings.items(): | ||
573 | 395 | if value is None: | ||
574 | 396 | relation_cmd_line.append('{}='.format(key)) | ||
575 | 397 | else: | ||
576 | 398 | relation_cmd_line.append('{}={}'.format(key, value)) | ||
577 | 399 | subprocess.check_call(relation_cmd_line) | ||
578 | 351 | # Flush cache of any relation-gets for local unit | 400 | # Flush cache of any relation-gets for local unit |
579 | 352 | flush(local_unit()) | 401 | flush(local_unit()) |
580 | 353 | 402 | ||
581 | 354 | 403 | ||
582 | 404 | def relation_clear(r_id=None): | ||
583 | 405 | ''' Clears any relation data already set on relation r_id ''' | ||
584 | 406 | settings = relation_get(rid=r_id, | ||
585 | 407 | unit=local_unit()) | ||
586 | 408 | for setting in settings: | ||
587 | 409 | if setting not in ['public-address', 'private-address']: | ||
588 | 410 | settings[setting] = None | ||
589 | 411 | relation_set(relation_id=r_id, | ||
590 | 412 | **settings) | ||
591 | 413 | |||
592 | 414 | |||
593 | 355 | @cached | 415 | @cached |
594 | 356 | def relation_ids(reltype=None): | 416 | def relation_ids(reltype=None): |
595 | 357 | """A list of relation_ids""" | 417 | """A list of relation_ids""" |
596 | @@ -431,6 +491,76 @@ | |||
597 | 431 | 491 | ||
598 | 432 | 492 | ||
599 | 433 | @cached | 493 | @cached |
600 | 494 | def peer_relation_id(): | ||
601 | 495 | '''Get the peers relation id if a peers relation has been joined, else None.''' | ||
602 | 496 | md = metadata() | ||
603 | 497 | section = md.get('peers') | ||
604 | 498 | if section: | ||
605 | 499 | for key in section: | ||
606 | 500 | relids = relation_ids(key) | ||
607 | 501 | if relids: | ||
608 | 502 | return relids[0] | ||
609 | 503 | return None | ||
610 | 504 | |||
611 | 505 | |||
612 | 506 | @cached | ||
613 | 507 | def relation_to_interface(relation_name): | ||
614 | 508 | """ | ||
615 | 509 | Given the name of a relation, return the interface that relation uses. | ||
616 | 510 | |||
617 | 511 | :returns: The interface name, or ``None``. | ||
618 | 512 | """ | ||
619 | 513 | return relation_to_role_and_interface(relation_name)[1] | ||
620 | 514 | |||
621 | 515 | |||
622 | 516 | @cached | ||
623 | 517 | def relation_to_role_and_interface(relation_name): | ||
624 | 518 | """ | ||
625 | 519 | Given the name of a relation, return the role and the name of the interface | ||
626 | 520 | that relation uses (where role is one of ``provides``, ``requires``, or ``peers``). | ||
627 | 521 | |||
628 | 522 | :returns: A tuple containing ``(role, interface)``, or ``(None, None)``. | ||
629 | 523 | """ | ||
630 | 524 | _metadata = metadata() | ||
631 | 525 | for role in ('provides', 'requires', 'peers'): | ||
632 | 526 | interface = _metadata.get(role, {}).get(relation_name, {}).get('interface') | ||
633 | 527 | if interface: | ||
634 | 528 | return role, interface | ||
635 | 529 | return None, None | ||
636 | 530 | |||
637 | 531 | |||
638 | 532 | @cached | ||
639 | 533 | def role_and_interface_to_relations(role, interface_name): | ||
640 | 534 | """ | ||
641 | 535 | Given a role and interface name, return a list of relation names for the | ||
642 | 536 | current charm that use that interface under that role (where role is one | ||
643 | 537 | of ``provides``, ``requires``, or ``peers``). | ||
644 | 538 | |||
645 | 539 | :returns: A list of relation names. | ||
646 | 540 | """ | ||
647 | 541 | _metadata = metadata() | ||
648 | 542 | results = [] | ||
649 | 543 | for relation_name, relation in _metadata.get(role, {}).items(): | ||
650 | 544 | if relation['interface'] == interface_name: | ||
651 | 545 | results.append(relation_name) | ||
652 | 546 | return results | ||
653 | 547 | |||
654 | 548 | |||
655 | 549 | @cached | ||
656 | 550 | def interface_to_relations(interface_name): | ||
657 | 551 | """ | ||
658 | 552 | Given an interface, return a list of relation names for the current | ||
659 | 553 | charm that use that interface. | ||
660 | 554 | |||
661 | 555 | :returns: A list of relation names. | ||
662 | 556 | """ | ||
663 | 557 | results = [] | ||
664 | 558 | for role in ('provides', 'requires', 'peers'): | ||
665 | 559 | results.extend(role_and_interface_to_relations(role, interface_name)) | ||
666 | 560 | return results | ||
667 | 561 | |||
668 | 562 | |||
669 | 563 | @cached | ||
670 | 434 | def charm_name(): | 564 | def charm_name(): |
671 | 435 | """Get the name of the current charm as is specified on metadata.yaml""" | 565 | """Get the name of the current charm as is specified on metadata.yaml""" |
672 | 436 | return metadata().get('name') | 566 | return metadata().get('name') |
673 | @@ -486,6 +616,20 @@ | |||
674 | 486 | subprocess.check_call(_args) | 616 | subprocess.check_call(_args) |
675 | 487 | 617 | ||
676 | 488 | 618 | ||
677 | 619 | def open_ports(start, end, protocol="TCP"): | ||
678 | 620 | """Opens a range of service network ports""" | ||
679 | 621 | _args = ['open-port'] | ||
680 | 622 | _args.append('{}-{}/{}'.format(start, end, protocol)) | ||
681 | 623 | subprocess.check_call(_args) | ||
682 | 624 | |||
683 | 625 | |||
684 | 626 | def close_ports(start, end, protocol="TCP"): | ||
685 | 627 | """Close a range of service network ports""" | ||
686 | 628 | _args = ['close-port'] | ||
687 | 629 | _args.append('{}-{}/{}'.format(start, end, protocol)) | ||
688 | 630 | subprocess.check_call(_args) | ||
689 | 631 | |||
690 | 632 | |||
691 | 489 | @cached | 633 | @cached |
692 | 490 | def unit_get(attribute): | 634 | def unit_get(attribute): |
693 | 491 | """Get the unit ID for the remote unit""" | 635 | """Get the unit ID for the remote unit""" |
694 | @@ -496,11 +640,48 @@ | |||
695 | 496 | return None | 640 | return None |
696 | 497 | 641 | ||
697 | 498 | 642 | ||
698 | 643 | def unit_public_ip(): | ||
699 | 644 | """Get this unit's public IP address""" | ||
700 | 645 | return unit_get('public-address') | ||
701 | 646 | |||
702 | 647 | |||
703 | 499 | def unit_private_ip(): | 648 | def unit_private_ip(): |
704 | 500 | """Get this unit's private IP address""" | 649 | """Get this unit's private IP address""" |
705 | 501 | return unit_get('private-address') | 650 | return unit_get('private-address') |
706 | 502 | 651 | ||
707 | 503 | 652 | ||
708 | 653 | @cached | ||
709 | 654 | def storage_get(attribute=None, storage_id=None): | ||
710 | 655 | """Get storage attributes""" | ||
711 | 656 | _args = ['storage-get', '--format=json'] | ||
712 | 657 | if storage_id: | ||
713 | 658 | _args.extend(('-s', storage_id)) | ||
714 | 659 | if attribute: | ||
715 | 660 | _args.append(attribute) | ||
716 | 661 | try: | ||
717 | 662 | return json.loads(subprocess.check_output(_args).decode('UTF-8')) | ||
718 | 663 | except ValueError: | ||
719 | 664 | return None | ||
720 | 665 | |||
721 | 666 | |||
722 | 667 | @cached | ||
723 | 668 | def storage_list(storage_name=None): | ||
724 | 669 | """List the storage IDs for the unit""" | ||
725 | 670 | _args = ['storage-list', '--format=json'] | ||
726 | 671 | if storage_name: | ||
727 | 672 | _args.append(storage_name) | ||
728 | 673 | try: | ||
729 | 674 | return json.loads(subprocess.check_output(_args).decode('UTF-8')) | ||
730 | 675 | except ValueError: | ||
731 | 676 | return None | ||
732 | 677 | except OSError as e: | ||
733 | 678 | import errno | ||
734 | 679 | if e.errno == errno.ENOENT: | ||
735 | 680 | # storage-list does not exist | ||
736 | 681 | return [] | ||
737 | 682 | raise | ||
738 | 683 | |||
739 | 684 | |||
740 | 504 | class UnregisteredHookError(Exception): | 685 | class UnregisteredHookError(Exception): |
741 | 505 | """Raised when an undefined hook is called""" | 686 | """Raised when an undefined hook is called""" |
742 | 506 | pass | 687 | pass |
743 | @@ -528,10 +709,14 @@ | |||
744 | 528 | hooks.execute(sys.argv) | 709 | hooks.execute(sys.argv) |
745 | 529 | """ | 710 | """ |
746 | 530 | 711 | ||
748 | 531 | def __init__(self, config_save=True): | 712 | def __init__(self, config_save=None): |
749 | 532 | super(Hooks, self).__init__() | 713 | super(Hooks, self).__init__() |
750 | 533 | self._hooks = {} | 714 | self._hooks = {} |
752 | 534 | self._config_save = config_save | 715 | |
753 | 716 | # For unknown reasons, we allow the Hooks constructor to override | ||
754 | 717 | # config().implicit_save. | ||
755 | 718 | if config_save is not None: | ||
756 | 719 | config().implicit_save = config_save | ||
757 | 535 | 720 | ||
758 | 536 | def register(self, name, function): | 721 | def register(self, name, function): |
759 | 537 | """Register a hook""" | 722 | """Register a hook""" |
760 | @@ -539,13 +724,16 @@ | |||
761 | 539 | 724 | ||
762 | 540 | def execute(self, args): | 725 | def execute(self, args): |
763 | 541 | """Execute a registered hook based on args[0]""" | 726 | """Execute a registered hook based on args[0]""" |
764 | 727 | _run_atstart() | ||
765 | 542 | hook_name = os.path.basename(args[0]) | 728 | hook_name = os.path.basename(args[0]) |
766 | 543 | if hook_name in self._hooks: | 729 | if hook_name in self._hooks: |
772 | 544 | self._hooks[hook_name]() | 730 | try: |
773 | 545 | if self._config_save: | 731 | self._hooks[hook_name]() |
774 | 546 | cfg = config() | 732 | except SystemExit as x: |
775 | 547 | if cfg.implicit_save: | 733 | if x.code is None or x.code == 0: |
776 | 548 | cfg.save() | 734 | _run_atexit() |
777 | 735 | raise | ||
778 | 736 | _run_atexit() | ||
779 | 549 | else: | 737 | else: |
780 | 550 | raise UnregisteredHookError(hook_name) | 738 | raise UnregisteredHookError(hook_name) |
781 | 551 | 739 | ||
782 | @@ -592,3 +780,289 @@ | |||
783 | 592 | 780 | ||
784 | 593 | The results set by action_set are preserved.""" | 781 | The results set by action_set are preserved.""" |
785 | 594 | subprocess.check_call(['action-fail', message]) | 782 | subprocess.check_call(['action-fail', message]) |
786 | 783 | |||
787 | 784 | |||
788 | 785 | def action_name(): | ||
789 | 786 | """Get the name of the currently executing action.""" | ||
790 | 787 | return os.environ.get('JUJU_ACTION_NAME') | ||
791 | 788 | |||
792 | 789 | |||
793 | 790 | def action_uuid(): | ||
794 | 791 | """Get the UUID of the currently executing action.""" | ||
795 | 792 | return os.environ.get('JUJU_ACTION_UUID') | ||
796 | 793 | |||
797 | 794 | |||
798 | 795 | def action_tag(): | ||
799 | 796 | """Get the tag for the currently executing action.""" | ||
800 | 797 | return os.environ.get('JUJU_ACTION_TAG') | ||
801 | 798 | |||
802 | 799 | |||
803 | 800 | def status_set(workload_state, message): | ||
804 | 801 | """Set the workload state with a message | ||
805 | 802 | |||
806 | 803 | Use status-set to set the workload state with a message which is visible | ||
807 | 804 | to the user via juju status. If the status-set command is not found then | ||
808 | 805 | assume this is juju < 1.23 and juju-log the message unstead. | ||
809 | 806 | |||
810 | 807 | workload_state -- valid juju workload state. | ||
811 | 808 | message -- status update message | ||
812 | 809 | """ | ||
813 | 810 | valid_states = ['maintenance', 'blocked', 'waiting', 'active'] | ||
814 | 811 | if workload_state not in valid_states: | ||
815 | 812 | raise ValueError( | ||
816 | 813 | '{!r} is not a valid workload state'.format(workload_state) | ||
817 | 814 | ) | ||
818 | 815 | cmd = ['status-set', workload_state, message] | ||
819 | 816 | try: | ||
820 | 817 | ret = subprocess.call(cmd) | ||
821 | 818 | if ret == 0: | ||
822 | 819 | return | ||
823 | 820 | except OSError as e: | ||
824 | 821 | if e.errno != errno.ENOENT: | ||
825 | 822 | raise | ||
826 | 823 | log_message = 'status-set failed: {} {}'.format(workload_state, | ||
827 | 824 | message) | ||
828 | 825 | log(log_message, level='INFO') | ||
829 | 826 | |||
830 | 827 | |||
831 | 828 | def status_get(): | ||
832 | 829 | """Retrieve the previously set juju workload state and message | ||
833 | 830 | |||
834 | 831 | If the status-get command is not found then assume this is juju < 1.23 and | ||
835 | 832 | return 'unknown', "" | ||
836 | 833 | |||
837 | 834 | """ | ||
838 | 835 | cmd = ['status-get', "--format=json", "--include-data"] | ||
839 | 836 | try: | ||
840 | 837 | raw_status = subprocess.check_output(cmd) | ||
841 | 838 | except OSError as e: | ||
842 | 839 | if e.errno == errno.ENOENT: | ||
843 | 840 | return ('unknown', "") | ||
844 | 841 | else: | ||
845 | 842 | raise | ||
846 | 843 | else: | ||
847 | 844 | status = json.loads(raw_status.decode("UTF-8")) | ||
848 | 845 | return (status["status"], status["message"]) | ||
849 | 846 | |||
850 | 847 | |||
851 | 848 | def translate_exc(from_exc, to_exc): | ||
852 | 849 | def inner_translate_exc1(f): | ||
853 | 850 | @wraps(f) | ||
854 | 851 | def inner_translate_exc2(*args, **kwargs): | ||
855 | 852 | try: | ||
856 | 853 | return f(*args, **kwargs) | ||
857 | 854 | except from_exc: | ||
858 | 855 | raise to_exc | ||
859 | 856 | |||
860 | 857 | return inner_translate_exc2 | ||
861 | 858 | |||
862 | 859 | return inner_translate_exc1 | ||
863 | 860 | |||
864 | 861 | |||
865 | 862 | def application_version_set(version): | ||
866 | 863 | """Charm authors may trigger this command from any hook to output what | ||
867 | 864 | version of the application is running. This could be a package version, | ||
868 | 865 | for instance postgres version 9.5. It could also be a build number or | ||
869 | 866 | version control revision identifier, for instance git sha 6fb7ba68. """ | ||
870 | 867 | |||
871 | 868 | cmd = ['application-version-set'] | ||
872 | 869 | cmd.append(version) | ||
873 | 870 | try: | ||
874 | 871 | subprocess.check_call(cmd) | ||
875 | 872 | except OSError: | ||
876 | 873 | log("Application Version: {}".format(version)) | ||
877 | 874 | |||
878 | 875 | |||
879 | 876 | @translate_exc(from_exc=OSError, to_exc=NotImplementedError) | ||
880 | 877 | def is_leader(): | ||
881 | 878 | """Does the current unit hold the juju leadership | ||
882 | 879 | |||
883 | 880 | Uses juju to determine whether the current unit is the leader of its peers | ||
884 | 881 | """ | ||
885 | 882 | cmd = ['is-leader', '--format=json'] | ||
886 | 883 | return json.loads(subprocess.check_output(cmd).decode('UTF-8')) | ||
887 | 884 | |||
888 | 885 | |||
889 | 886 | @translate_exc(from_exc=OSError, to_exc=NotImplementedError) | ||
890 | 887 | def leader_get(attribute=None): | ||
891 | 888 | """Juju leader get value(s)""" | ||
892 | 889 | cmd = ['leader-get', '--format=json'] + [attribute or '-'] | ||
893 | 890 | return json.loads(subprocess.check_output(cmd).decode('UTF-8')) | ||
894 | 891 | |||
895 | 892 | |||
896 | 893 | @translate_exc(from_exc=OSError, to_exc=NotImplementedError) | ||
897 | 894 | def leader_set(settings=None, **kwargs): | ||
898 | 895 | """Juju leader set value(s)""" | ||
899 | 896 | # Don't log secrets. | ||
900 | 897 | # log("Juju leader-set '%s'" % (settings), level=DEBUG) | ||
901 | 898 | cmd = ['leader-set'] | ||
902 | 899 | settings = settings or {} | ||
903 | 900 | settings.update(kwargs) | ||
904 | 901 | for k, v in settings.items(): | ||
905 | 902 | if v is None: | ||
906 | 903 | cmd.append('{}='.format(k)) | ||
907 | 904 | else: | ||
908 | 905 | cmd.append('{}={}'.format(k, v)) | ||
909 | 906 | subprocess.check_call(cmd) | ||
910 | 907 | |||
911 | 908 | |||
912 | 909 | @translate_exc(from_exc=OSError, to_exc=NotImplementedError) | ||
913 | 910 | def payload_register(ptype, klass, pid): | ||
914 | 911 | """ is used while a hook is running to let Juju know that a | ||
915 | 912 | payload has been started.""" | ||
916 | 913 | cmd = ['payload-register'] | ||
917 | 914 | for x in [ptype, klass, pid]: | ||
918 | 915 | cmd.append(x) | ||
919 | 916 | subprocess.check_call(cmd) | ||
920 | 917 | |||
921 | 918 | |||
922 | 919 | @translate_exc(from_exc=OSError, to_exc=NotImplementedError) | ||
923 | 920 | def payload_unregister(klass, pid): | ||
924 | 921 | """ is used while a hook is running to let Juju know | ||
925 | 922 | that a payload has been manually stopped. The <class> and <id> provided | ||
926 | 923 | must match a payload that has been previously registered with juju using | ||
927 | 924 | payload-register.""" | ||
928 | 925 | cmd = ['payload-unregister'] | ||
929 | 926 | for x in [klass, pid]: | ||
930 | 927 | cmd.append(x) | ||
931 | 928 | subprocess.check_call(cmd) | ||
932 | 929 | |||
933 | 930 | |||
934 | 931 | @translate_exc(from_exc=OSError, to_exc=NotImplementedError) | ||
935 | 932 | def payload_status_set(klass, pid, status): | ||
936 | 933 | """is used to update the current status of a registered payload. | ||
937 | 934 | The <class> and <id> provided must match a payload that has been previously | ||
938 | 935 | registered with juju using payload-register. The <status> must be one of the | ||
939 | 936 | follow: starting, started, stopping, stopped""" | ||
940 | 937 | cmd = ['payload-status-set'] | ||
941 | 938 | for x in [klass, pid, status]: | ||
942 | 939 | cmd.append(x) | ||
943 | 940 | subprocess.check_call(cmd) | ||
944 | 941 | |||
945 | 942 | |||
946 | 943 | @translate_exc(from_exc=OSError, to_exc=NotImplementedError) | ||
947 | 944 | def resource_get(name): | ||
948 | 945 | """used to fetch the resource path of the given name. | ||
949 | 946 | |||
950 | 947 | <name> must match a name of defined resource in metadata.yaml | ||
951 | 948 | |||
952 | 949 | returns either a path or False if resource not available | ||
953 | 950 | """ | ||
954 | 951 | if not name: | ||
955 | 952 | return False | ||
956 | 953 | |||
957 | 954 | cmd = ['resource-get', name] | ||
958 | 955 | try: | ||
959 | 956 | return subprocess.check_output(cmd).decode('UTF-8') | ||
960 | 957 | except subprocess.CalledProcessError: | ||
961 | 958 | return False | ||
962 | 959 | |||
963 | 960 | |||
964 | 961 | @cached | ||
965 | 962 | def juju_version(): | ||
966 | 963 | """Full version string (eg. '1.23.3.1-trusty-amd64')""" | ||
967 | 964 | # Per https://bugs.launchpad.net/juju-core/+bug/1455368/comments/1 | ||
968 | 965 | jujud = glob.glob('/var/lib/juju/tools/machine-*/jujud')[0] | ||
969 | 966 | return subprocess.check_output([jujud, 'version'], | ||
970 | 967 | universal_newlines=True).strip() | ||
971 | 968 | |||
972 | 969 | |||
973 | 970 | @cached | ||
974 | 971 | def has_juju_version(minimum_version): | ||
975 | 972 | """Return True if the Juju version is at least the provided version""" | ||
976 | 973 | return LooseVersion(juju_version()) >= LooseVersion(minimum_version) | ||
977 | 974 | |||
978 | 975 | |||
979 | 976 | _atexit = [] | ||
980 | 977 | _atstart = [] | ||
981 | 978 | |||
982 | 979 | |||
983 | 980 | def atstart(callback, *args, **kwargs): | ||
984 | 981 | '''Schedule a callback to run before the main hook. | ||
985 | 982 | |||
986 | 983 | Callbacks are run in the order they were added. | ||
987 | 984 | |||
988 | 985 | This is useful for modules and classes to perform initialization | ||
989 | 986 | and inject behavior. In particular: | ||
990 | 987 | |||
991 | 988 | - Run common code before all of your hooks, such as logging | ||
992 | 989 | the hook name or interesting relation data. | ||
993 | 990 | - Defer object or module initialization that requires a hook | ||
994 | 991 | context until we know there actually is a hook context, | ||
995 | 992 | making testing easier. | ||
996 | 993 | - Rather than requiring charm authors to include boilerplate to | ||
997 | 994 | invoke your helper's behavior, have it run automatically if | ||
998 | 995 | your object is instantiated or module imported. | ||
999 | 996 | |||
1000 | 997 | This is not at all useful after your hook framework as been launched. | ||
1001 | 998 | ''' | ||
1002 | 999 | global _atstart | ||
1003 | 1000 | _atstart.append((callback, args, kwargs)) | ||
1004 | 1001 | |||
1005 | 1002 | |||
1006 | 1003 | def atexit(callback, *args, **kwargs): | ||
1007 | 1004 | '''Schedule a callback to run on successful hook completion. | ||
1008 | 1005 | |||
1009 | 1006 | Callbacks are run in the reverse order that they were added.''' | ||
1010 | 1007 | _atexit.append((callback, args, kwargs)) | ||
1011 | 1008 | |||
1012 | 1009 | |||
1013 | 1010 | def _run_atstart(): | ||
1014 | 1011 | '''Hook frameworks must invoke this before running the main hook body.''' | ||
1015 | 1012 | global _atstart | ||
1016 | 1013 | for callback, args, kwargs in _atstart: | ||
1017 | 1014 | callback(*args, **kwargs) | ||
1018 | 1015 | del _atstart[:] | ||
1019 | 1016 | |||
1020 | 1017 | |||
1021 | 1018 | def _run_atexit(): | ||
1022 | 1019 | '''Hook frameworks must invoke this after the main hook body has | ||
1023 | 1020 | successfully completed. Do not invoke it if the hook fails.''' | ||
1024 | 1021 | global _atexit | ||
1025 | 1022 | for callback, args, kwargs in reversed(_atexit): | ||
1026 | 1023 | callback(*args, **kwargs) | ||
1027 | 1024 | del _atexit[:] | ||
1028 | 1025 | |||
1029 | 1026 | |||
1030 | 1027 | @translate_exc(from_exc=OSError, to_exc=NotImplementedError) | ||
1031 | 1028 | def network_get_primary_address(binding): | ||
1032 | 1029 | ''' | ||
1033 | 1030 | Retrieve the primary network address for a named binding | ||
1034 | 1031 | |||
1035 | 1032 | :param binding: string. The name of a relation of extra-binding | ||
1036 | 1033 | :return: string. The primary IP address for the named binding | ||
1037 | 1034 | :raise: NotImplementedError if run on Juju < 2.0 | ||
1038 | 1035 | ''' | ||
1039 | 1036 | cmd = ['network-get', '--primary-address', binding] | ||
1040 | 1037 | return subprocess.check_output(cmd).decode('UTF-8').strip() | ||
1041 | 1038 | |||
1042 | 1039 | |||
1043 | 1040 | def add_metric(*args, **kwargs): | ||
1044 | 1041 | """Add metric values. Values may be expressed with keyword arguments. For | ||
1045 | 1042 | metric names containing dashes, these may be expressed as one or more | ||
1046 | 1043 | 'key=value' positional arguments. May only be called from the collect-metrics | ||
1047 | 1044 | hook.""" | ||
1048 | 1045 | _args = ['add-metric'] | ||
1049 | 1046 | _kvpairs = [] | ||
1050 | 1047 | _kvpairs.extend(args) | ||
1051 | 1048 | _kvpairs.extend(['{}={}'.format(k, v) for k, v in kwargs.items()]) | ||
1052 | 1049 | _args.extend(sorted(_kvpairs)) | ||
1053 | 1050 | try: | ||
1054 | 1051 | subprocess.check_call(_args) | ||
1055 | 1052 | return | ||
1056 | 1053 | except EnvironmentError as e: | ||
1057 | 1054 | if e.errno != errno.ENOENT: | ||
1058 | 1055 | raise | ||
1059 | 1056 | log_message = 'add-metric failed: {}'.format(' '.join(_kvpairs)) | ||
1060 | 1057 | log(log_message, level='INFO') | ||
1061 | 1058 | |||
1062 | 1059 | |||
1063 | 1060 | def meter_status(): | ||
1064 | 1061 | """Get the meter status, if running in the meter-status-changed hook.""" | ||
1065 | 1062 | return os.environ.get('JUJU_METER_STATUS') | ||
1066 | 1063 | |||
1067 | 1064 | |||
1068 | 1065 | def meter_info(): | ||
1069 | 1066 | """Get the meter status information, if running in the meter-status-changed | ||
1070 | 1067 | hook.""" | ||
1071 | 1068 | return os.environ.get('JUJU_METER_INFO') | ||
1072 | 595 | 1069 | ||
1073 | === modified file 'hooks/charmhelpers/core/host.py' | |||
1074 | --- hooks/charmhelpers/core/host.py 2015-04-08 05:57:06 +0000 | |||
1075 | +++ hooks/charmhelpers/core/host.py 2017-07-18 06:08:45 +0000 | |||
1076 | @@ -1,18 +1,16 @@ | |||
1077 | 1 | # Copyright 2014-2015 Canonical Limited. | 1 | # Copyright 2014-2015 Canonical Limited. |
1078 | 2 | # | 2 | # |
1092 | 3 | # This file is part of charm-helpers. | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
1093 | 4 | # | 4 | # you may not use this file except in compliance with the License. |
1094 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | 5 | # You may obtain a copy of the License at |
1095 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | 6 | # |
1096 | 7 | # published by the Free Software Foundation. | 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
1097 | 8 | # | 8 | # |
1098 | 9 | # charm-helpers is distributed in the hope that it will be useful, | 9 | # Unless required by applicable law or agreed to in writing, software |
1099 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
1100 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
1101 | 12 | # GNU Lesser General Public License for more details. | 12 | # See the License for the specific language governing permissions and |
1102 | 13 | # | 13 | # limitations under the License. |
1090 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
1091 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
1103 | 16 | 14 | ||
1104 | 17 | """Tools for working with the host system""" | 15 | """Tools for working with the host system""" |
1105 | 18 | # Copyright 2012 Canonical Ltd. | 16 | # Copyright 2012 Canonical Ltd. |
1106 | @@ -24,85 +22,330 @@ | |||
1107 | 24 | import os | 22 | import os |
1108 | 25 | import re | 23 | import re |
1109 | 26 | import pwd | 24 | import pwd |
1110 | 25 | import glob | ||
1111 | 27 | import grp | 26 | import grp |
1112 | 28 | import random | 27 | import random |
1113 | 29 | import string | 28 | import string |
1114 | 30 | import subprocess | 29 | import subprocess |
1115 | 31 | import hashlib | 30 | import hashlib |
1116 | 31 | import functools | ||
1117 | 32 | import itertools | ||
1118 | 33 | import six | ||
1119 | 34 | |||
1120 | 32 | from contextlib import contextmanager | 35 | from contextlib import contextmanager |
1121 | 33 | from collections import OrderedDict | 36 | from collections import OrderedDict |
1122 | 34 | |||
1123 | 35 | import six | ||
1124 | 36 | |||
1125 | 37 | from .hookenv import log | 37 | from .hookenv import log |
1126 | 38 | from .fstab import Fstab | 38 | from .fstab import Fstab |
1141 | 39 | 39 | from charmhelpers.osplatform import get_platform | |
1142 | 40 | 40 | ||
1143 | 41 | def service_start(service_name): | 41 | __platform__ = get_platform() |
1144 | 42 | """Start a system service""" | 42 | if __platform__ == "ubuntu": |
1145 | 43 | return service('start', service_name) | 43 | from charmhelpers.core.host_factory.ubuntu import ( |
1146 | 44 | 44 | service_available, | |
1147 | 45 | 45 | add_new_group, | |
1148 | 46 | def service_stop(service_name): | 46 | lsb_release, |
1149 | 47 | """Stop a system service""" | 47 | cmp_pkgrevno, |
1150 | 48 | return service('stop', service_name) | 48 | CompareHostReleases, |
1151 | 49 | 49 | ) # flake8: noqa -- ignore F401 for this import | |
1152 | 50 | 50 | elif __platform__ == "centos": | |
1153 | 51 | def service_restart(service_name): | 51 | from charmhelpers.core.host_factory.centos import ( |
1154 | 52 | """Restart a system service""" | 52 | service_available, |
1155 | 53 | add_new_group, | ||
1156 | 54 | lsb_release, | ||
1157 | 55 | cmp_pkgrevno, | ||
1158 | 56 | CompareHostReleases, | ||
1159 | 57 | ) # flake8: noqa -- ignore F401 for this import | ||
1160 | 58 | |||
1161 | 59 | UPDATEDB_PATH = '/etc/updatedb.conf' | ||
1162 | 60 | |||
1163 | 61 | def service_start(service_name, **kwargs): | ||
1164 | 62 | """Start a system service. | ||
1165 | 63 | |||
1166 | 64 | The specified service name is managed via the system level init system. | ||
1167 | 65 | Some init systems (e.g. upstart) require that additional arguments be | ||
1168 | 66 | provided in order to directly control service instances whereas other init | ||
1169 | 67 | systems allow for addressing instances of a service directly by name (e.g. | ||
1170 | 68 | systemd). | ||
1171 | 69 | |||
1172 | 70 | The kwargs allow for the additional parameters to be passed to underlying | ||
1173 | 71 | init systems for those systems which require/allow for them. For example, | ||
1174 | 72 | the ceph-osd upstart script requires the id parameter to be passed along | ||
1175 | 73 | in order to identify which running daemon should be reloaded. The follow- | ||
1176 | 74 | ing example stops the ceph-osd service for instance id=4: | ||
1177 | 75 | |||
1178 | 76 | service_stop('ceph-osd', id=4) | ||
1179 | 77 | |||
1180 | 78 | :param service_name: the name of the service to stop | ||
1181 | 79 | :param **kwargs: additional parameters to pass to the init system when | ||
1182 | 80 | managing services. These will be passed as key=value | ||
1183 | 81 | parameters to the init system's commandline. kwargs | ||
1184 | 82 | are ignored for systemd enabled systems. | ||
1185 | 83 | """ | ||
1186 | 84 | return service('start', service_name, **kwargs) | ||
1187 | 85 | |||
1188 | 86 | |||
1189 | 87 | def service_stop(service_name, **kwargs): | ||
1190 | 88 | """Stop a system service. | ||
1191 | 89 | |||
1192 | 90 | The specified service name is managed via the system level init system. | ||
1193 | 91 | Some init systems (e.g. upstart) require that additional arguments be | ||
1194 | 92 | provided in order to directly control service instances whereas other init | ||
1195 | 93 | systems allow for addressing instances of a service directly by name (e.g. | ||
1196 | 94 | systemd). | ||
1197 | 95 | |||
1198 | 96 | The kwargs allow for the additional parameters to be passed to underlying | ||
1199 | 97 | init systems for those systems which require/allow for them. For example, | ||
1200 | 98 | the ceph-osd upstart script requires the id parameter to be passed along | ||
1201 | 99 | in order to identify which running daemon should be reloaded. The follow- | ||
1202 | 100 | ing example stops the ceph-osd service for instance id=4: | ||
1203 | 101 | |||
1204 | 102 | service_stop('ceph-osd', id=4) | ||
1205 | 103 | |||
1206 | 104 | :param service_name: the name of the service to stop | ||
1207 | 105 | :param **kwargs: additional parameters to pass to the init system when | ||
1208 | 106 | managing services. These will be passed as key=value | ||
1209 | 107 | parameters to the init system's commandline. kwargs | ||
1210 | 108 | are ignored for systemd enabled systems. | ||
1211 | 109 | """ | ||
1212 | 110 | return service('stop', service_name, **kwargs) | ||
1213 | 111 | |||
1214 | 112 | |||
1215 | 113 | def service_restart(service_name, **kwargs): | ||
1216 | 114 | """Restart a system service. | ||
1217 | 115 | |||
1218 | 116 | The specified service name is managed via the system level init system. | ||
1219 | 117 | Some init systems (e.g. upstart) require that additional arguments be | ||
1220 | 118 | provided in order to directly control service instances whereas other init | ||
1221 | 119 | systems allow for addressing instances of a service directly by name (e.g. | ||
1222 | 120 | systemd). | ||
1223 | 121 | |||
1224 | 122 | The kwargs allow for the additional parameters to be passed to underlying | ||
1225 | 123 | init systems for those systems which require/allow for them. For example, | ||
1226 | 124 | the ceph-osd upstart script requires the id parameter to be passed along | ||
1227 | 125 | in order to identify which running daemon should be restarted. The follow- | ||
1228 | 126 | ing example restarts the ceph-osd service for instance id=4: | ||
1229 | 127 | |||
1230 | 128 | service_restart('ceph-osd', id=4) | ||
1231 | 129 | |||
1232 | 130 | :param service_name: the name of the service to restart | ||
1233 | 131 | :param **kwargs: additional parameters to pass to the init system when | ||
1234 | 132 | managing services. These will be passed as key=value | ||
1235 | 133 | parameters to the init system's commandline. kwargs | ||
1236 | 134 | are ignored for init systems not allowing additional | ||
1237 | 135 | parameters via the commandline (systemd). | ||
1238 | 136 | """ | ||
1239 | 53 | return service('restart', service_name) | 137 | return service('restart', service_name) |
1240 | 54 | 138 | ||
1241 | 55 | 139 | ||
1243 | 56 | def service_reload(service_name, restart_on_failure=False): | 140 | def service_reload(service_name, restart_on_failure=False, **kwargs): |
1244 | 57 | """Reload a system service, optionally falling back to restart if | 141 | """Reload a system service, optionally falling back to restart if |
1247 | 58 | reload fails""" | 142 | reload fails. |
1248 | 59 | service_result = service('reload', service_name) | 143 | |
1249 | 144 | The specified service name is managed via the system level init system. | ||
1250 | 145 | Some init systems (e.g. upstart) require that additional arguments be | ||
1251 | 146 | provided in order to directly control service instances whereas other init | ||
1252 | 147 | systems allow for addressing instances of a service directly by name (e.g. | ||
1253 | 148 | systemd). | ||
1254 | 149 | |||
1255 | 150 | The kwargs allow for the additional parameters to be passed to underlying | ||
1256 | 151 | init systems for those systems which require/allow for them. For example, | ||
1257 | 152 | the ceph-osd upstart script requires the id parameter to be passed along | ||
1258 | 153 | in order to identify which running daemon should be reloaded. The follow- | ||
1259 | 154 | ing example restarts the ceph-osd service for instance id=4: | ||
1260 | 155 | |||
1261 | 156 | service_reload('ceph-osd', id=4) | ||
1262 | 157 | |||
1263 | 158 | :param service_name: the name of the service to reload | ||
1264 | 159 | :param restart_on_failure: boolean indicating whether to fallback to a | ||
1265 | 160 | restart if the reload fails. | ||
1266 | 161 | :param **kwargs: additional parameters to pass to the init system when | ||
1267 | 162 | managing services. These will be passed as key=value | ||
1268 | 163 | parameters to the init system's commandline. kwargs | ||
1269 | 164 | are ignored for init systems not allowing additional | ||
1270 | 165 | parameters via the commandline (systemd). | ||
1271 | 166 | """ | ||
1272 | 167 | service_result = service('reload', service_name, **kwargs) | ||
1273 | 60 | if not service_result and restart_on_failure: | 168 | if not service_result and restart_on_failure: |
1275 | 61 | service_result = service('restart', service_name) | 169 | service_result = service('restart', service_name, **kwargs) |
1276 | 62 | return service_result | 170 | return service_result |
1277 | 63 | 171 | ||
1278 | 64 | 172 | ||
1282 | 65 | def service(action, service_name): | 173 | def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d", |
1283 | 66 | """Control a system service""" | 174 | **kwargs): |
1284 | 67 | cmd = ['service', service_name, action] | 175 | """Pause a system service. |
1285 | 176 | |||
1286 | 177 | Stop it, and prevent it from starting again at boot. | ||
1287 | 178 | |||
1288 | 179 | :param service_name: the name of the service to pause | ||
1289 | 180 | :param init_dir: path to the upstart init directory | ||
1290 | 181 | :param initd_dir: path to the sysv init directory | ||
1291 | 182 | :param **kwargs: additional parameters to pass to the init system when | ||
1292 | 183 | managing services. These will be passed as key=value | ||
1293 | 184 | parameters to the init system's commandline. kwargs | ||
1294 | 185 | are ignored for init systems which do not support | ||
1295 | 186 | key=value arguments via the commandline. | ||
1296 | 187 | """ | ||
1297 | 188 | stopped = True | ||
1298 | 189 | if service_running(service_name, **kwargs): | ||
1299 | 190 | stopped = service_stop(service_name, **kwargs) | ||
1300 | 191 | upstart_file = os.path.join(init_dir, "{}.conf".format(service_name)) | ||
1301 | 192 | sysv_file = os.path.join(initd_dir, service_name) | ||
1302 | 193 | if init_is_systemd(): | ||
1303 | 194 | service('disable', service_name) | ||
1304 | 195 | service('mask', service_name) | ||
1305 | 196 | elif os.path.exists(upstart_file): | ||
1306 | 197 | override_path = os.path.join( | ||
1307 | 198 | init_dir, '{}.override'.format(service_name)) | ||
1308 | 199 | with open(override_path, 'w') as fh: | ||
1309 | 200 | fh.write("manual\n") | ||
1310 | 201 | elif os.path.exists(sysv_file): | ||
1311 | 202 | subprocess.check_call(["update-rc.d", service_name, "disable"]) | ||
1312 | 203 | else: | ||
1313 | 204 | raise ValueError( | ||
1314 | 205 | "Unable to detect {0} as SystemD, Upstart {1} or" | ||
1315 | 206 | " SysV {2}".format( | ||
1316 | 207 | service_name, upstart_file, sysv_file)) | ||
1317 | 208 | return stopped | ||
1318 | 209 | |||
1319 | 210 | |||
1320 | 211 | def service_resume(service_name, init_dir="/etc/init", | ||
1321 | 212 | initd_dir="/etc/init.d", **kwargs): | ||
1322 | 213 | """Resume a system service. | ||
1323 | 214 | |||
1324 | 215 | Reenable starting again at boot. Start the service. | ||
1325 | 216 | |||
1326 | 217 | :param service_name: the name of the service to resume | ||
1327 | 218 | :param init_dir: the path to the init dir | ||
1328 | 219 | :param initd dir: the path to the initd dir | ||
1329 | 220 | :param **kwargs: additional parameters to pass to the init system when | ||
1330 | 221 | managing services. These will be passed as key=value | ||
1331 | 222 | parameters to the init system's commandline. kwargs | ||
1332 | 223 | are ignored for systemd enabled systems. | ||
1333 | 224 | """ | ||
1334 | 225 | upstart_file = os.path.join(init_dir, "{}.conf".format(service_name)) | ||
1335 | 226 | sysv_file = os.path.join(initd_dir, service_name) | ||
1336 | 227 | if init_is_systemd(): | ||
1337 | 228 | service('unmask', service_name) | ||
1338 | 229 | service('enable', service_name) | ||
1339 | 230 | elif os.path.exists(upstart_file): | ||
1340 | 231 | override_path = os.path.join( | ||
1341 | 232 | init_dir, '{}.override'.format(service_name)) | ||
1342 | 233 | if os.path.exists(override_path): | ||
1343 | 234 | os.unlink(override_path) | ||
1344 | 235 | elif os.path.exists(sysv_file): | ||
1345 | 236 | subprocess.check_call(["update-rc.d", service_name, "enable"]) | ||
1346 | 237 | else: | ||
1347 | 238 | raise ValueError( | ||
1348 | 239 | "Unable to detect {0} as SystemD, Upstart {1} or" | ||
1349 | 240 | " SysV {2}".format( | ||
1350 | 241 | service_name, upstart_file, sysv_file)) | ||
1351 | 242 | started = service_running(service_name, **kwargs) | ||
1352 | 243 | |||
1353 | 244 | if not started: | ||
1354 | 245 | started = service_start(service_name, **kwargs) | ||
1355 | 246 | return started | ||
1356 | 247 | |||
1357 | 248 | |||
1358 | 249 | def service(action, service_name, **kwargs): | ||
1359 | 250 | """Control a system service. | ||
1360 | 251 | |||
1361 | 252 | :param action: the action to take on the service | ||
1362 | 253 | :param service_name: the name of the service to perform th action on | ||
1363 | 254 | :param **kwargs: additional params to be passed to the service command in | ||
1364 | 255 | the form of key=value. | ||
1365 | 256 | """ | ||
1366 | 257 | if init_is_systemd(): | ||
1367 | 258 | cmd = ['systemctl', action, service_name] | ||
1368 | 259 | else: | ||
1369 | 260 | cmd = ['service', service_name, action] | ||
1370 | 261 | for key, value in six.iteritems(kwargs): | ||
1371 | 262 | parameter = '%s=%s' % (key, value) | ||
1372 | 263 | cmd.append(parameter) | ||
1373 | 68 | return subprocess.call(cmd) == 0 | 264 | return subprocess.call(cmd) == 0 |
1374 | 69 | 265 | ||
1375 | 70 | 266 | ||
1405 | 71 | def service_running(service): | 267 | _UPSTART_CONF = "/etc/init/{}.conf" |
1406 | 72 | """Determine whether a system service is running""" | 268 | _INIT_D_CONF = "/etc/init.d/{}" |
1407 | 73 | try: | 269 | |
1408 | 74 | output = subprocess.check_output( | 270 | |
1409 | 75 | ['service', service, 'status'], | 271 | def service_running(service_name, **kwargs): |
1410 | 76 | stderr=subprocess.STDOUT).decode('UTF-8') | 272 | """Determine whether a system service is running. |
1411 | 77 | except subprocess.CalledProcessError: | 273 | |
1412 | 78 | return False | 274 | :param service_name: the name of the service |
1413 | 79 | else: | 275 | :param **kwargs: additional args to pass to the service command. This is |
1414 | 80 | if ("start/running" in output or "is running" in output): | 276 | used to pass additional key=value arguments to the |
1415 | 81 | return True | 277 | service command line for managing specific instance |
1416 | 82 | else: | 278 | units (e.g. service ceph-osd status id=2). The kwargs |
1417 | 83 | return False | 279 | are ignored in systemd services. |
1418 | 84 | 280 | """ | |
1419 | 85 | 281 | if init_is_systemd(): | |
1420 | 86 | def service_available(service_name): | 282 | return service('is-active', service_name) |
1421 | 87 | """Determine whether a system service is available""" | 283 | else: |
1422 | 88 | try: | 284 | if os.path.exists(_UPSTART_CONF.format(service_name)): |
1423 | 89 | subprocess.check_output( | 285 | try: |
1424 | 90 | ['service', service_name, 'status'], | 286 | cmd = ['status', service_name] |
1425 | 91 | stderr=subprocess.STDOUT).decode('UTF-8') | 287 | for key, value in six.iteritems(kwargs): |
1426 | 92 | except subprocess.CalledProcessError as e: | 288 | parameter = '%s=%s' % (key, value) |
1427 | 93 | return 'unrecognized service' not in e.output | 289 | cmd.append(parameter) |
1428 | 94 | else: | 290 | output = subprocess.check_output(cmd, |
1429 | 95 | return True | 291 | stderr=subprocess.STDOUT).decode('UTF-8') |
1430 | 96 | 292 | except subprocess.CalledProcessError: | |
1431 | 97 | 293 | return False | |
1432 | 98 | def adduser(username, password=None, shell='/bin/bash', system_user=False): | 294 | else: |
1433 | 99 | """Add a user to the system""" | 295 | # This works for upstart scripts where the 'service' command |
1434 | 296 | # returns a consistent string to represent running | ||
1435 | 297 | # 'start/running' | ||
1436 | 298 | if ("start/running" in output or | ||
1437 | 299 | "is running" in output or | ||
1438 | 300 | "up and running" in output): | ||
1439 | 301 | return True | ||
1440 | 302 | elif os.path.exists(_INIT_D_CONF.format(service_name)): | ||
1441 | 303 | # Check System V scripts init script return codes | ||
1442 | 304 | return service('status', service_name) | ||
1443 | 305 | return False | ||
1444 | 306 | |||
1445 | 307 | |||
1446 | 308 | SYSTEMD_SYSTEM = '/run/systemd/system' | ||
1447 | 309 | |||
1448 | 310 | |||
1449 | 311 | def init_is_systemd(): | ||
1450 | 312 | """Return True if the host system uses systemd, False otherwise.""" | ||
1451 | 313 | if lsb_release()['DISTRIB_CODENAME'] == 'trusty': | ||
1452 | 314 | return False | ||
1453 | 315 | return os.path.isdir(SYSTEMD_SYSTEM) | ||
1454 | 316 | |||
1455 | 317 | |||
1456 | 318 | def adduser(username, password=None, shell='/bin/bash', | ||
1457 | 319 | system_user=False, primary_group=None, | ||
1458 | 320 | secondary_groups=None, uid=None, home_dir=None): | ||
1459 | 321 | """Add a user to the system. | ||
1460 | 322 | |||
1461 | 323 | Will log but otherwise succeed if the user already exists. | ||
1462 | 324 | |||
1463 | 325 | :param str username: Username to create | ||
1464 | 326 | :param str password: Password for user; if ``None``, create a system user | ||
1465 | 327 | :param str shell: The default shell for the user | ||
1466 | 328 | :param bool system_user: Whether to create a login or system user | ||
1467 | 329 | :param str primary_group: Primary group for user; defaults to username | ||
1468 | 330 | :param list secondary_groups: Optional list of additional groups | ||
1469 | 331 | :param int uid: UID for user being created | ||
1470 | 332 | :param str home_dir: Home directory for user | ||
1471 | 333 | |||
1472 | 334 | :returns: The password database entry struct, as returned by `pwd.getpwnam` | ||
1473 | 335 | """ | ||
1474 | 100 | try: | 336 | try: |
1475 | 101 | user_info = pwd.getpwnam(username) | 337 | user_info = pwd.getpwnam(username) |
1476 | 102 | log('user {0} already exists!'.format(username)) | 338 | log('user {0} already exists!'.format(username)) |
1477 | 339 | if uid: | ||
1478 | 340 | user_info = pwd.getpwuid(int(uid)) | ||
1479 | 341 | log('user with uid {0} already exists!'.format(uid)) | ||
1480 | 103 | except KeyError: | 342 | except KeyError: |
1481 | 104 | log('creating user {0}'.format(username)) | 343 | log('creating user {0}'.format(username)) |
1482 | 105 | cmd = ['useradd'] | 344 | cmd = ['useradd'] |
1483 | 345 | if uid: | ||
1484 | 346 | cmd.extend(['--uid', str(uid)]) | ||
1485 | 347 | if home_dir: | ||
1486 | 348 | cmd.extend(['--home', str(home_dir)]) | ||
1487 | 106 | if system_user or password is None: | 349 | if system_user or password is None: |
1488 | 107 | cmd.append('--system') | 350 | cmd.append('--system') |
1489 | 108 | else: | 351 | else: |
1490 | @@ -111,52 +354,104 @@ | |||
1491 | 111 | '--shell', shell, | 354 | '--shell', shell, |
1492 | 112 | '--password', password, | 355 | '--password', password, |
1493 | 113 | ]) | 356 | ]) |
1494 | 357 | if not primary_group: | ||
1495 | 358 | try: | ||
1496 | 359 | grp.getgrnam(username) | ||
1497 | 360 | primary_group = username # avoid "group exists" error | ||
1498 | 361 | except KeyError: | ||
1499 | 362 | pass | ||
1500 | 363 | if primary_group: | ||
1501 | 364 | cmd.extend(['-g', primary_group]) | ||
1502 | 365 | if secondary_groups: | ||
1503 | 366 | cmd.extend(['-G', ','.join(secondary_groups)]) | ||
1504 | 114 | cmd.append(username) | 367 | cmd.append(username) |
1505 | 115 | subprocess.check_call(cmd) | 368 | subprocess.check_call(cmd) |
1506 | 116 | user_info = pwd.getpwnam(username) | 369 | user_info = pwd.getpwnam(username) |
1507 | 117 | return user_info | 370 | return user_info |
1508 | 118 | 371 | ||
1509 | 119 | 372 | ||
1512 | 120 | def add_group(group_name, system_group=False): | 373 | def user_exists(username): |
1513 | 121 | """Add a group to the system""" | 374 | """Check if a user exists""" |
1514 | 375 | try: | ||
1515 | 376 | pwd.getpwnam(username) | ||
1516 | 377 | user_exists = True | ||
1517 | 378 | except KeyError: | ||
1518 | 379 | user_exists = False | ||
1519 | 380 | return user_exists | ||
1520 | 381 | |||
1521 | 382 | |||
1522 | 383 | def uid_exists(uid): | ||
1523 | 384 | """Check if a uid exists""" | ||
1524 | 385 | try: | ||
1525 | 386 | pwd.getpwuid(uid) | ||
1526 | 387 | uid_exists = True | ||
1527 | 388 | except KeyError: | ||
1528 | 389 | uid_exists = False | ||
1529 | 390 | return uid_exists | ||
1530 | 391 | |||
1531 | 392 | |||
1532 | 393 | def group_exists(groupname): | ||
1533 | 394 | """Check if a group exists""" | ||
1534 | 395 | try: | ||
1535 | 396 | grp.getgrnam(groupname) | ||
1536 | 397 | group_exists = True | ||
1537 | 398 | except KeyError: | ||
1538 | 399 | group_exists = False | ||
1539 | 400 | return group_exists | ||
1540 | 401 | |||
1541 | 402 | |||
1542 | 403 | def gid_exists(gid): | ||
1543 | 404 | """Check if a gid exists""" | ||
1544 | 405 | try: | ||
1545 | 406 | grp.getgrgid(gid) | ||
1546 | 407 | gid_exists = True | ||
1547 | 408 | except KeyError: | ||
1548 | 409 | gid_exists = False | ||
1549 | 410 | return gid_exists | ||
1550 | 411 | |||
1551 | 412 | |||
1552 | 413 | def add_group(group_name, system_group=False, gid=None): | ||
1553 | 414 | """Add a group to the system | ||
1554 | 415 | |||
1555 | 416 | Will log but otherwise succeed if the group already exists. | ||
1556 | 417 | |||
1557 | 418 | :param str group_name: group to create | ||
1558 | 419 | :param bool system_group: Create system group | ||
1559 | 420 | :param int gid: GID for user being created | ||
1560 | 421 | |||
1561 | 422 | :returns: The password database entry struct, as returned by `grp.getgrnam` | ||
1562 | 423 | """ | ||
1563 | 122 | try: | 424 | try: |
1564 | 123 | group_info = grp.getgrnam(group_name) | 425 | group_info = grp.getgrnam(group_name) |
1565 | 124 | log('group {0} already exists!'.format(group_name)) | 426 | log('group {0} already exists!'.format(group_name)) |
1566 | 427 | if gid: | ||
1567 | 428 | group_info = grp.getgrgid(gid) | ||
1568 | 429 | log('group with gid {0} already exists!'.format(gid)) | ||
1569 | 125 | except KeyError: | 430 | except KeyError: |
1570 | 126 | log('creating group {0}'.format(group_name)) | 431 | log('creating group {0}'.format(group_name)) |
1580 | 127 | cmd = ['addgroup'] | 432 | add_new_group(group_name, system_group, gid) |
1572 | 128 | if system_group: | ||
1573 | 129 | cmd.append('--system') | ||
1574 | 130 | else: | ||
1575 | 131 | cmd.extend([ | ||
1576 | 132 | '--group', | ||
1577 | 133 | ]) | ||
1578 | 134 | cmd.append(group_name) | ||
1579 | 135 | subprocess.check_call(cmd) | ||
1581 | 136 | group_info = grp.getgrnam(group_name) | 433 | group_info = grp.getgrnam(group_name) |
1582 | 137 | return group_info | 434 | return group_info |
1583 | 138 | 435 | ||
1584 | 139 | 436 | ||
1585 | 140 | def add_user_to_group(username, group): | 437 | def add_user_to_group(username, group): |
1586 | 141 | """Add a user to a group""" | 438 | """Add a user to a group""" |
1592 | 142 | cmd = [ | 439 | cmd = ['gpasswd', '-a', username, group] |
1588 | 143 | 'gpasswd', '-a', | ||
1589 | 144 | username, | ||
1590 | 145 | group | ||
1591 | 146 | ] | ||
1593 | 147 | log("Adding user {} to group {}".format(username, group)) | 440 | log("Adding user {} to group {}".format(username, group)) |
1594 | 148 | subprocess.check_call(cmd) | 441 | subprocess.check_call(cmd) |
1595 | 149 | 442 | ||
1596 | 150 | 443 | ||
1598 | 151 | def rsync(from_path, to_path, flags='-r', options=None): | 444 | def rsync(from_path, to_path, flags='-r', options=None, timeout=None): |
1599 | 152 | """Replicate the contents of a path""" | 445 | """Replicate the contents of a path""" |
1600 | 153 | options = options or ['--delete', '--executability'] | 446 | options = options or ['--delete', '--executability'] |
1601 | 154 | cmd = ['/usr/bin/rsync', flags] | 447 | cmd = ['/usr/bin/rsync', flags] |
1602 | 448 | if timeout: | ||
1603 | 449 | cmd = ['timeout', str(timeout)] + cmd | ||
1604 | 155 | cmd.extend(options) | 450 | cmd.extend(options) |
1605 | 156 | cmd.append(from_path) | 451 | cmd.append(from_path) |
1606 | 157 | cmd.append(to_path) | 452 | cmd.append(to_path) |
1607 | 158 | log(" ".join(cmd)) | 453 | log(" ".join(cmd)) |
1609 | 159 | return subprocess.check_output(cmd).decode('UTF-8').strip() | 454 | return subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode('UTF-8').strip() |
1610 | 160 | 455 | ||
1611 | 161 | 456 | ||
1612 | 162 | def symlink(source, destination): | 457 | def symlink(source, destination): |
1613 | @@ -202,14 +497,12 @@ | |||
1614 | 202 | 497 | ||
1615 | 203 | 498 | ||
1616 | 204 | def fstab_remove(mp): | 499 | def fstab_remove(mp): |
1619 | 205 | """Remove the given mountpoint entry from /etc/fstab | 500 | """Remove the given mountpoint entry from /etc/fstab""" |
1618 | 206 | """ | ||
1620 | 207 | return Fstab.remove_by_mountpoint(mp) | 501 | return Fstab.remove_by_mountpoint(mp) |
1621 | 208 | 502 | ||
1622 | 209 | 503 | ||
1623 | 210 | def fstab_add(dev, mp, fs, options=None): | 504 | def fstab_add(dev, mp, fs, options=None): |
1626 | 211 | """Adds the given device entry to the /etc/fstab file | 505 | """Adds the given device entry to the /etc/fstab file""" |
1625 | 212 | """ | ||
1627 | 213 | return Fstab.add(dev, mp, fs, options=options) | 506 | return Fstab.add(dev, mp, fs, options=options) |
1628 | 214 | 507 | ||
1629 | 215 | 508 | ||
1630 | @@ -253,9 +546,19 @@ | |||
1631 | 253 | return system_mounts | 546 | return system_mounts |
1632 | 254 | 547 | ||
1633 | 255 | 548 | ||
1634 | 549 | def fstab_mount(mountpoint): | ||
1635 | 550 | """Mount filesystem using fstab""" | ||
1636 | 551 | cmd_args = ['mount', mountpoint] | ||
1637 | 552 | try: | ||
1638 | 553 | subprocess.check_output(cmd_args) | ||
1639 | 554 | except subprocess.CalledProcessError as e: | ||
1640 | 555 | log('Error unmounting {}\n{}'.format(mountpoint, e.output)) | ||
1641 | 556 | return False | ||
1642 | 557 | return True | ||
1643 | 558 | |||
1644 | 559 | |||
1645 | 256 | def file_hash(path, hash_type='md5'): | 560 | def file_hash(path, hash_type='md5'): |
1648 | 257 | """ | 561 | """Generate a hash checksum of the contents of 'path' or None if not found. |
1647 | 258 | Generate a hash checksum of the contents of 'path' or None if not found. | ||
1649 | 259 | 562 | ||
1650 | 260 | :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`, | 563 | :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`, |
1651 | 261 | such as md5, sha1, sha256, sha512, etc. | 564 | such as md5, sha1, sha256, sha512, etc. |
1652 | @@ -269,9 +572,22 @@ | |||
1653 | 269 | return None | 572 | return None |
1654 | 270 | 573 | ||
1655 | 271 | 574 | ||
1656 | 575 | def path_hash(path): | ||
1657 | 576 | """Generate a hash checksum of all files matching 'path'. Standard | ||
1658 | 577 | wildcards like '*' and '?' are supported, see documentation for the 'glob' | ||
1659 | 578 | module for more information. | ||
1660 | 579 | |||
1661 | 580 | :return: dict: A { filename: hash } dictionary for all matched files. | ||
1662 | 581 | Empty if none found. | ||
1663 | 582 | """ | ||
1664 | 583 | return { | ||
1665 | 584 | filename: file_hash(filename) | ||
1666 | 585 | for filename in glob.iglob(path) | ||
1667 | 586 | } | ||
1668 | 587 | |||
1669 | 588 | |||
1670 | 272 | def check_hash(path, checksum, hash_type='md5'): | 589 | def check_hash(path, checksum, hash_type='md5'): |
1673 | 273 | """ | 590 | """Validate a file using a cryptographic checksum. |
1672 | 274 | Validate a file using a cryptographic checksum. | ||
1674 | 275 | 591 | ||
1675 | 276 | :param str checksum: Value of the checksum used to validate the file. | 592 | :param str checksum: Value of the checksum used to validate the file. |
1676 | 277 | :param str hash_type: Hash algorithm used to generate `checksum`. | 593 | :param str hash_type: Hash algorithm used to generate `checksum`. |
1677 | @@ -286,54 +602,78 @@ | |||
1678 | 286 | 602 | ||
1679 | 287 | 603 | ||
1680 | 288 | class ChecksumError(ValueError): | 604 | class ChecksumError(ValueError): |
1681 | 605 | """A class derived from Value error to indicate the checksum failed.""" | ||
1682 | 289 | pass | 606 | pass |
1683 | 290 | 607 | ||
1684 | 291 | 608 | ||
1686 | 292 | def restart_on_change(restart_map, stopstart=False): | 609 | def restart_on_change(restart_map, stopstart=False, restart_functions=None): |
1687 | 293 | """Restart services based on configuration files changing | 610 | """Restart services based on configuration files changing |
1688 | 294 | 611 | ||
1689 | 295 | This function is used a decorator, for example:: | 612 | This function is used a decorator, for example:: |
1690 | 296 | 613 | ||
1691 | 297 | @restart_on_change({ | 614 | @restart_on_change({ |
1692 | 298 | '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ] | 615 | '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ] |
1693 | 616 | '/etc/apache/sites-enabled/*': [ 'apache2' ] | ||
1694 | 299 | }) | 617 | }) |
1696 | 300 | def ceph_client_changed(): | 618 | def config_changed(): |
1697 | 301 | pass # your code here | 619 | pass # your code here |
1698 | 302 | 620 | ||
1699 | 303 | In this example, the cinder-api and cinder-volume services | 621 | In this example, the cinder-api and cinder-volume services |
1700 | 304 | would be restarted if /etc/ceph/ceph.conf is changed by the | 622 | would be restarted if /etc/ceph/ceph.conf is changed by the |
1702 | 305 | ceph_client_changed function. | 623 | ceph_client_changed function. The apache2 service would be |
1703 | 624 | restarted if any file matching the pattern got changed, created | ||
1704 | 625 | or removed. Standard wildcards are supported, see documentation | ||
1705 | 626 | for the 'glob' module for more information. | ||
1706 | 627 | |||
1707 | 628 | @param restart_map: {path_file_name: [service_name, ...] | ||
1708 | 629 | @param stopstart: DEFAULT false; whether to stop, start OR restart | ||
1709 | 630 | @param restart_functions: nonstandard functions to use to restart services | ||
1710 | 631 | {svc: func, ...} | ||
1711 | 632 | @returns result from decorated function | ||
1712 | 306 | """ | 633 | """ |
1713 | 307 | def wrap(f): | 634 | def wrap(f): |
1714 | 635 | @functools.wraps(f) | ||
1715 | 308 | def wrapped_f(*args, **kwargs): | 636 | def wrapped_f(*args, **kwargs): |
1732 | 309 | checksums = {} | 637 | return restart_on_change_helper( |
1733 | 310 | for path in restart_map: | 638 | (lambda: f(*args, **kwargs)), restart_map, stopstart, |
1734 | 311 | checksums[path] = file_hash(path) | 639 | restart_functions) |
1719 | 312 | f(*args, **kwargs) | ||
1720 | 313 | restarts = [] | ||
1721 | 314 | for path in restart_map: | ||
1722 | 315 | if checksums[path] != file_hash(path): | ||
1723 | 316 | restarts += restart_map[path] | ||
1724 | 317 | services_list = list(OrderedDict.fromkeys(restarts)) | ||
1725 | 318 | if not stopstart: | ||
1726 | 319 | for service_name in services_list: | ||
1727 | 320 | service('restart', service_name) | ||
1728 | 321 | else: | ||
1729 | 322 | for action in ['stop', 'start']: | ||
1730 | 323 | for service_name in services_list: | ||
1731 | 324 | service(action, service_name) | ||
1735 | 325 | return wrapped_f | 640 | return wrapped_f |
1736 | 326 | return wrap | 641 | return wrap |
1737 | 327 | 642 | ||
1738 | 328 | 643 | ||
1747 | 329 | def lsb_release(): | 644 | def restart_on_change_helper(lambda_f, restart_map, stopstart=False, |
1748 | 330 | """Return /etc/lsb-release in a dict""" | 645 | restart_functions=None): |
1749 | 331 | d = {} | 646 | """Helper function to perform the restart_on_change function. |
1750 | 332 | with open('/etc/lsb-release', 'r') as lsb: | 647 | |
1751 | 333 | for l in lsb: | 648 | This is provided for decorators to restart services if files described |
1752 | 334 | k, v = l.split('=') | 649 | in the restart_map have changed after an invocation of lambda_f(). |
1753 | 335 | d[k.strip()] = v.strip() | 650 | |
1754 | 336 | return d | 651 | @param lambda_f: function to call. |
1755 | 652 | @param restart_map: {file: [service, ...]} | ||
1756 | 653 | @param stopstart: whether to stop, start or restart a service | ||
1757 | 654 | @param restart_functions: nonstandard functions to use to restart services | ||
1758 | 655 | {svc: func, ...} | ||
1759 | 656 | @returns result of lambda_f() | ||
1760 | 657 | """ | ||
1761 | 658 | if restart_functions is None: | ||
1762 | 659 | restart_functions = {} | ||
1763 | 660 | checksums = {path: path_hash(path) for path in restart_map} | ||
1764 | 661 | r = lambda_f() | ||
1765 | 662 | # create a list of lists of the services to restart | ||
1766 | 663 | restarts = [restart_map[path] | ||
1767 | 664 | for path in restart_map | ||
1768 | 665 | if path_hash(path) != checksums[path]] | ||
1769 | 666 | # create a flat list of ordered services without duplicates from lists | ||
1770 | 667 | services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts))) | ||
1771 | 668 | if services_list: | ||
1772 | 669 | actions = ('stop', 'start') if stopstart else ('restart',) | ||
1773 | 670 | for service_name in services_list: | ||
1774 | 671 | if service_name in restart_functions: | ||
1775 | 672 | restart_functions[service_name](service_name) | ||
1776 | 673 | else: | ||
1777 | 674 | for action in actions: | ||
1778 | 675 | service(action, service_name) | ||
1779 | 676 | return r | ||
1780 | 337 | 677 | ||
1781 | 338 | 678 | ||
1782 | 339 | def pwgen(length=None): | 679 | def pwgen(length=None): |
1783 | @@ -352,36 +692,92 @@ | |||
1784 | 352 | return(''.join(random_chars)) | 692 | return(''.join(random_chars)) |
1785 | 353 | 693 | ||
1786 | 354 | 694 | ||
1789 | 355 | def list_nics(nic_type): | 695 | def is_phy_iface(interface): |
1790 | 356 | '''Return a list of nics of given type(s)''' | 696 | """Returns True if interface is not virtual, otherwise False.""" |
1791 | 697 | if interface: | ||
1792 | 698 | sys_net = '/sys/class/net' | ||
1793 | 699 | if os.path.isdir(sys_net): | ||
1794 | 700 | for iface in glob.glob(os.path.join(sys_net, '*')): | ||
1795 | 701 | if '/virtual/' in os.path.realpath(iface): | ||
1796 | 702 | continue | ||
1797 | 703 | |||
1798 | 704 | if interface == os.path.basename(iface): | ||
1799 | 705 | return True | ||
1800 | 706 | |||
1801 | 707 | return False | ||
1802 | 708 | |||
1803 | 709 | |||
1804 | 710 | def get_bond_master(interface): | ||
1805 | 711 | """Returns bond master if interface is bond slave otherwise None. | ||
1806 | 712 | |||
1807 | 713 | NOTE: the provided interface is expected to be physical | ||
1808 | 714 | """ | ||
1809 | 715 | if interface: | ||
1810 | 716 | iface_path = '/sys/class/net/%s' % (interface) | ||
1811 | 717 | if os.path.exists(iface_path): | ||
1812 | 718 | if '/virtual/' in os.path.realpath(iface_path): | ||
1813 | 719 | return None | ||
1814 | 720 | |||
1815 | 721 | master = os.path.join(iface_path, 'master') | ||
1816 | 722 | if os.path.exists(master): | ||
1817 | 723 | master = os.path.realpath(master) | ||
1818 | 724 | # make sure it is a bond master | ||
1819 | 725 | if os.path.exists(os.path.join(master, 'bonding')): | ||
1820 | 726 | return os.path.basename(master) | ||
1821 | 727 | |||
1822 | 728 | return None | ||
1823 | 729 | |||
1824 | 730 | |||
1825 | 731 | def list_nics(nic_type=None): | ||
1826 | 732 | """Return a list of nics of given type(s)""" | ||
1827 | 357 | if isinstance(nic_type, six.string_types): | 733 | if isinstance(nic_type, six.string_types): |
1828 | 358 | int_types = [nic_type] | 734 | int_types = [nic_type] |
1829 | 359 | else: | 735 | else: |
1830 | 360 | int_types = nic_type | 736 | int_types = nic_type |
1831 | 737 | |||
1832 | 361 | interfaces = [] | 738 | interfaces = [] |
1835 | 362 | for int_type in int_types: | 739 | if nic_type: |
1836 | 363 | cmd = ['ip', 'addr', 'show', 'label', int_type + '*'] | 740 | for int_type in int_types: |
1837 | 741 | cmd = ['ip', 'addr', 'show', 'label', int_type + '*'] | ||
1838 | 742 | ip_output = subprocess.check_output(cmd).decode('UTF-8') | ||
1839 | 743 | ip_output = ip_output.split('\n') | ||
1840 | 744 | ip_output = (line for line in ip_output if line) | ||
1841 | 745 | for line in ip_output: | ||
1842 | 746 | if line.split()[1].startswith(int_type): | ||
1843 | 747 | matched = re.search('.*: (' + int_type + | ||
1844 | 748 | r'[0-9]+\.[0-9]+)@.*', line) | ||
1845 | 749 | if matched: | ||
1846 | 750 | iface = matched.groups()[0] | ||
1847 | 751 | else: | ||
1848 | 752 | iface = line.split()[1].replace(":", "") | ||
1849 | 753 | |||
1850 | 754 | if iface not in interfaces: | ||
1851 | 755 | interfaces.append(iface) | ||
1852 | 756 | else: | ||
1853 | 757 | cmd = ['ip', 'a'] | ||
1854 | 364 | ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n') | 758 | ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n') |
1856 | 365 | ip_output = (line for line in ip_output if line) | 759 | ip_output = (line.strip() for line in ip_output if line) |
1857 | 760 | |||
1858 | 761 | key = re.compile('^[0-9]+:\s+(.+):') | ||
1859 | 366 | for line in ip_output: | 762 | for line in ip_output: |
1867 | 367 | if line.split()[1].startswith(int_type): | 763 | matched = re.search(key, line) |
1868 | 368 | matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line) | 764 | if matched: |
1869 | 369 | if matched: | 765 | iface = matched.group(1) |
1870 | 370 | interface = matched.groups()[0] | 766 | iface = iface.partition("@")[0] |
1871 | 371 | else: | 767 | if iface not in interfaces: |
1872 | 372 | interface = line.split()[1].replace(":", "") | 768 | interfaces.append(iface) |
1866 | 373 | interfaces.append(interface) | ||
1873 | 374 | 769 | ||
1874 | 375 | return interfaces | 770 | return interfaces |
1875 | 376 | 771 | ||
1876 | 377 | 772 | ||
1877 | 378 | def set_nic_mtu(nic, mtu): | 773 | def set_nic_mtu(nic, mtu): |
1879 | 379 | '''Set MTU on a network interface''' | 774 | """Set the Maximum Transmission Unit (MTU) on a network interface.""" |
1880 | 380 | cmd = ['ip', 'link', 'set', nic, 'mtu', mtu] | 775 | cmd = ['ip', 'link', 'set', nic, 'mtu', mtu] |
1881 | 381 | subprocess.check_call(cmd) | 776 | subprocess.check_call(cmd) |
1882 | 382 | 777 | ||
1883 | 383 | 778 | ||
1884 | 384 | def get_nic_mtu(nic): | 779 | def get_nic_mtu(nic): |
1885 | 780 | """Return the Maximum Transmission Unit (MTU) for a network interface.""" | ||
1886 | 385 | cmd = ['ip', 'addr', 'show', nic] | 781 | cmd = ['ip', 'addr', 'show', nic] |
1887 | 386 | ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n') | 782 | ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n') |
1888 | 387 | mtu = "" | 783 | mtu = "" |
1889 | @@ -393,6 +789,7 @@ | |||
1890 | 393 | 789 | ||
1891 | 394 | 790 | ||
1892 | 395 | def get_nic_hwaddr(nic): | 791 | def get_nic_hwaddr(nic): |
1893 | 792 | """Return the Media Access Control (MAC) for a network interface.""" | ||
1894 | 396 | cmd = ['ip', '-o', '-0', 'addr', 'show', nic] | 793 | cmd = ['ip', '-o', '-0', 'addr', 'show', nic] |
1895 | 397 | ip_output = subprocess.check_output(cmd).decode('UTF-8') | 794 | ip_output = subprocess.check_output(cmd).decode('UTF-8') |
1896 | 398 | hwaddr = "" | 795 | hwaddr = "" |
1897 | @@ -402,35 +799,31 @@ | |||
1898 | 402 | return hwaddr | 799 | return hwaddr |
1899 | 403 | 800 | ||
1900 | 404 | 801 | ||
1901 | 405 | def cmp_pkgrevno(package, revno, pkgcache=None): | ||
1902 | 406 | '''Compare supplied revno with the revno of the installed package | ||
1903 | 407 | |||
1904 | 408 | * 1 => Installed revno is greater than supplied arg | ||
1905 | 409 | * 0 => Installed revno is the same as supplied arg | ||
1906 | 410 | * -1 => Installed revno is less than supplied arg | ||
1907 | 411 | |||
1908 | 412 | This function imports apt_cache function from charmhelpers.fetch if | ||
1909 | 413 | the pkgcache argument is None. Be sure to add charmhelpers.fetch if | ||
1910 | 414 | you call this function, or pass an apt_pkg.Cache() instance. | ||
1911 | 415 | ''' | ||
1912 | 416 | import apt_pkg | ||
1913 | 417 | if not pkgcache: | ||
1914 | 418 | from charmhelpers.fetch import apt_cache | ||
1915 | 419 | pkgcache = apt_cache() | ||
1916 | 420 | pkg = pkgcache[package] | ||
1917 | 421 | return apt_pkg.version_compare(pkg.current_ver.ver_str, revno) | ||
1918 | 422 | |||
1919 | 423 | |||
1920 | 424 | @contextmanager | 802 | @contextmanager |
1922 | 425 | def chdir(d): | 803 | def chdir(directory): |
1923 | 804 | """Change the current working directory to a different directory for a code | ||
1924 | 805 | block and return the previous directory after the block exits. Useful to | ||
1925 | 806 | run commands from a specificed directory. | ||
1926 | 807 | |||
1927 | 808 | :param str directory: The directory path to change to for this context. | ||
1928 | 809 | """ | ||
1929 | 426 | cur = os.getcwd() | 810 | cur = os.getcwd() |
1930 | 427 | try: | 811 | try: |
1932 | 428 | yield os.chdir(d) | 812 | yield os.chdir(directory) |
1933 | 429 | finally: | 813 | finally: |
1934 | 430 | os.chdir(cur) | 814 | os.chdir(cur) |
1935 | 431 | 815 | ||
1936 | 432 | 816 | ||
1938 | 433 | def chownr(path, owner, group, follow_links=True): | 817 | def chownr(path, owner, group, follow_links=True, chowntopdir=False): |
1939 | 818 | """Recursively change user and group ownership of files and directories | ||
1940 | 819 | in given path. Doesn't chown path itself by default, only its children. | ||
1941 | 820 | |||
1942 | 821 | :param str path: The string path to start changing ownership. | ||
1943 | 822 | :param str owner: The owner string to use when looking up the uid. | ||
1944 | 823 | :param str group: The group string to use when looking up the gid. | ||
1945 | 824 | :param bool follow_links: Also follow and chown links if True | ||
1946 | 825 | :param bool chowntopdir: Also chown path itself if True | ||
1947 | 826 | """ | ||
1948 | 434 | uid = pwd.getpwnam(owner).pw_uid | 827 | uid = pwd.getpwnam(owner).pw_uid |
1949 | 435 | gid = grp.getgrnam(group).gr_gid | 828 | gid = grp.getgrnam(group).gr_gid |
1950 | 436 | if follow_links: | 829 | if follow_links: |
1951 | @@ -438,7 +831,11 @@ | |||
1952 | 438 | else: | 831 | else: |
1953 | 439 | chown = os.lchown | 832 | chown = os.lchown |
1954 | 440 | 833 | ||
1956 | 441 | for root, dirs, files in os.walk(path): | 834 | if chowntopdir: |
1957 | 835 | broken_symlink = os.path.lexists(path) and not os.path.exists(path) | ||
1958 | 836 | if not broken_symlink: | ||
1959 | 837 | chown(path, uid, gid) | ||
1960 | 838 | for root, dirs, files in os.walk(path, followlinks=follow_links): | ||
1961 | 442 | for name in dirs + files: | 839 | for name in dirs + files: |
1962 | 443 | full = os.path.join(root, name) | 840 | full = os.path.join(root, name) |
1963 | 444 | broken_symlink = os.path.lexists(full) and not os.path.exists(full) | 841 | broken_symlink = os.path.lexists(full) and not os.path.exists(full) |
1964 | @@ -447,4 +844,81 @@ | |||
1965 | 447 | 844 | ||
1966 | 448 | 845 | ||
1967 | 449 | def lchownr(path, owner, group): | 846 | def lchownr(path, owner, group): |
1968 | 847 | """Recursively change user and group ownership of files and directories | ||
1969 | 848 | in a given path, not following symbolic links. See the documentation for | ||
1970 | 849 | 'os.lchown' for more information. | ||
1971 | 850 | |||
1972 | 851 | :param str path: The string path to start changing ownership. | ||
1973 | 852 | :param str owner: The owner string to use when looking up the uid. | ||
1974 | 853 | :param str group: The group string to use when looking up the gid. | ||
1975 | 854 | """ | ||
1976 | 450 | chownr(path, owner, group, follow_links=False) | 855 | chownr(path, owner, group, follow_links=False) |
1977 | 856 | |||
1978 | 857 | |||
1979 | 858 | def owner(path): | ||
1980 | 859 | """Returns a tuple containing the username & groupname owning the path. | ||
1981 | 860 | |||
1982 | 861 | :param str path: the string path to retrieve the ownership | ||
1983 | 862 | :return tuple(str, str): A (username, groupname) tuple containing the | ||
1984 | 863 | name of the user and group owning the path. | ||
1985 | 864 | :raises OSError: if the specified path does not exist | ||
1986 | 865 | """ | ||
1987 | 866 | stat = os.stat(path) | ||
1988 | 867 | username = pwd.getpwuid(stat.st_uid)[0] | ||
1989 | 868 | groupname = grp.getgrgid(stat.st_gid)[0] | ||
1990 | 869 | return username, groupname | ||
1991 | 870 | |||
1992 | 871 | |||
1993 | 872 | def get_total_ram(): | ||
1994 | 873 | """The total amount of system RAM in bytes. | ||
1995 | 874 | |||
1996 | 875 | This is what is reported by the OS, and may be overcommitted when | ||
1997 | 876 | there are multiple containers hosted on the same machine. | ||
1998 | 877 | """ | ||
1999 | 878 | with open('/proc/meminfo', 'r') as f: | ||
2000 | 879 | for line in f.readlines(): | ||
2001 | 880 | if line: | ||
2002 | 881 | key, value, unit = line.split() | ||
2003 | 882 | if key == 'MemTotal:': | ||
2004 | 883 | assert unit == 'kB', 'Unknown unit' | ||
2005 | 884 | return int(value) * 1024 # Classic, not KiB. | ||
2006 | 885 | raise NotImplementedError() | ||
2007 | 886 | |||
2008 | 887 | |||
2009 | 888 | UPSTART_CONTAINER_TYPE = '/run/container_type' | ||
2010 | 889 | |||
2011 | 890 | |||
2012 | 891 | def is_container(): | ||
2013 | 892 | """Determine whether unit is running in a container | ||
2014 | 893 | |||
2015 | 894 | @return: boolean indicating if unit is in a container | ||
2016 | 895 | """ | ||
2017 | 896 | if init_is_systemd(): | ||
2018 | 897 | # Detect using systemd-detect-virt | ||
2019 | 898 | return subprocess.call(['systemd-detect-virt', | ||
2020 | 899 | '--container']) == 0 | ||
2021 | 900 | else: | ||
2022 | 901 | # Detect using upstart container file marker | ||
2023 | 902 | return os.path.exists(UPSTART_CONTAINER_TYPE) | ||
2024 | 903 | |||
2025 | 904 | |||
2026 | 905 | def add_to_updatedb_prunepath(path, updatedb_path=UPDATEDB_PATH): | ||
2027 | 906 | with open(updatedb_path, 'r+') as f_id: | ||
2028 | 907 | updatedb_text = f_id.read() | ||
2029 | 908 | output = updatedb(updatedb_text, path) | ||
2030 | 909 | f_id.seek(0) | ||
2031 | 910 | f_id.write(output) | ||
2032 | 911 | f_id.truncate() | ||
2033 | 912 | |||
2034 | 913 | |||
2035 | 914 | def updatedb(updatedb_text, new_path): | ||
2036 | 915 | lines = [line for line in updatedb_text.split("\n")] | ||
2037 | 916 | for i, line in enumerate(lines): | ||
2038 | 917 | if line.startswith("PRUNEPATHS="): | ||
2039 | 918 | paths_line = line.split("=")[1].replace('"', '') | ||
2040 | 919 | paths = paths_line.split(" ") | ||
2041 | 920 | if new_path not in paths: | ||
2042 | 921 | paths.append(new_path) | ||
2043 | 922 | lines[i] = 'PRUNEPATHS="{}"'.format(' '.join(paths)) | ||
2044 | 923 | output = "\n".join(lines) | ||
2045 | 924 | return output | ||
2046 | 451 | 925 | ||
2047 | === added directory 'hooks/charmhelpers/core/host_factory' | |||
2048 | === added file 'hooks/charmhelpers/core/host_factory/__init__.py' | |||
2049 | === added file 'hooks/charmhelpers/core/host_factory/centos.py' | |||
2050 | --- hooks/charmhelpers/core/host_factory/centos.py 1970-01-01 00:00:00 +0000 | |||
2051 | +++ hooks/charmhelpers/core/host_factory/centos.py 2017-07-18 06:08:45 +0000 | |||
2052 | @@ -0,0 +1,72 @@ | |||
2053 | 1 | import subprocess | ||
2054 | 2 | import yum | ||
2055 | 3 | import os | ||
2056 | 4 | |||
2057 | 5 | from charmhelpers.core.strutils import BasicStringComparator | ||
2058 | 6 | |||
2059 | 7 | |||
2060 | 8 | class CompareHostReleases(BasicStringComparator): | ||
2061 | 9 | """Provide comparisons of Host releases. | ||
2062 | 10 | |||
2063 | 11 | Use in the form of | ||
2064 | 12 | |||
2065 | 13 | if CompareHostReleases(release) > 'trusty': | ||
2066 | 14 | # do something with mitaka | ||
2067 | 15 | """ | ||
2068 | 16 | |||
2069 | 17 | def __init__(self, item): | ||
2070 | 18 | raise NotImplementedError( | ||
2071 | 19 | "CompareHostReleases() is not implemented for CentOS") | ||
2072 | 20 | |||
2073 | 21 | |||
2074 | 22 | def service_available(service_name): | ||
2075 | 23 | # """Determine whether a system service is available.""" | ||
2076 | 24 | if os.path.isdir('/run/systemd/system'): | ||
2077 | 25 | cmd = ['systemctl', 'is-enabled', service_name] | ||
2078 | 26 | else: | ||
2079 | 27 | cmd = ['service', service_name, 'is-enabled'] | ||
2080 | 28 | return subprocess.call(cmd) == 0 | ||
2081 | 29 | |||
2082 | 30 | |||
2083 | 31 | def add_new_group(group_name, system_group=False, gid=None): | ||
2084 | 32 | cmd = ['groupadd'] | ||
2085 | 33 | if gid: | ||
2086 | 34 | cmd.extend(['--gid', str(gid)]) | ||
2087 | 35 | if system_group: | ||
2088 | 36 | cmd.append('-r') | ||
2089 | 37 | cmd.append(group_name) | ||
2090 | 38 | subprocess.check_call(cmd) | ||
2091 | 39 | |||
2092 | 40 | |||
2093 | 41 | def lsb_release(): | ||
2094 | 42 | """Return /etc/os-release in a dict.""" | ||
2095 | 43 | d = {} | ||
2096 | 44 | with open('/etc/os-release', 'r') as lsb: | ||
2097 | 45 | for l in lsb: | ||
2098 | 46 | s = l.split('=') | ||
2099 | 47 | if len(s) != 2: | ||
2100 | 48 | continue | ||
2101 | 49 | d[s[0].strip()] = s[1].strip() | ||
2102 | 50 | return d | ||
2103 | 51 | |||
2104 | 52 | |||
2105 | 53 | def cmp_pkgrevno(package, revno, pkgcache=None): | ||
2106 | 54 | """Compare supplied revno with the revno of the installed package. | ||
2107 | 55 | |||
2108 | 56 | * 1 => Installed revno is greater than supplied arg | ||
2109 | 57 | * 0 => Installed revno is the same as supplied arg | ||
2110 | 58 | * -1 => Installed revno is less than supplied arg | ||
2111 | 59 | |||
2112 | 60 | This function imports YumBase function if the pkgcache argument | ||
2113 | 61 | is None. | ||
2114 | 62 | """ | ||
2115 | 63 | if not pkgcache: | ||
2116 | 64 | y = yum.YumBase() | ||
2117 | 65 | packages = y.doPackageLists() | ||
2118 | 66 | pkgcache = {i.Name: i.version for i in packages['installed']} | ||
2119 | 67 | pkg = pkgcache[package] | ||
2120 | 68 | if pkg > revno: | ||
2121 | 69 | return 1 | ||
2122 | 70 | if pkg < revno: | ||
2123 | 71 | return -1 | ||
2124 | 72 | return 0 | ||
2125 | 0 | 73 | ||
2126 | === added file 'hooks/charmhelpers/core/host_factory/ubuntu.py' | |||
2127 | --- hooks/charmhelpers/core/host_factory/ubuntu.py 1970-01-01 00:00:00 +0000 | |||
2128 | +++ hooks/charmhelpers/core/host_factory/ubuntu.py 2017-07-18 06:08:45 +0000 | |||
2129 | @@ -0,0 +1,89 @@ | |||
2130 | 1 | import subprocess | ||
2131 | 2 | |||
2132 | 3 | from charmhelpers.core.strutils import BasicStringComparator | ||
2133 | 4 | |||
2134 | 5 | |||
2135 | 6 | UBUNTU_RELEASES = ( | ||
2136 | 7 | 'lucid', | ||
2137 | 8 | 'maverick', | ||
2138 | 9 | 'natty', | ||
2139 | 10 | 'oneiric', | ||
2140 | 11 | 'precise', | ||
2141 | 12 | 'quantal', | ||
2142 | 13 | 'raring', | ||
2143 | 14 | 'saucy', | ||
2144 | 15 | 'trusty', | ||
2145 | 16 | 'utopic', | ||
2146 | 17 | 'vivid', | ||
2147 | 18 | 'wily', | ||
2148 | 19 | 'xenial', | ||
2149 | 20 | 'yakkety', | ||
2150 | 21 | 'zesty', | ||
2151 | 22 | 'artful', | ||
2152 | 23 | ) | ||
2153 | 24 | |||
2154 | 25 | |||
2155 | 26 | class CompareHostReleases(BasicStringComparator): | ||
2156 | 27 | """Provide comparisons of Ubuntu releases. | ||
2157 | 28 | |||
2158 | 29 | Use in the form of | ||
2159 | 30 | |||
2160 | 31 | if CompareHostReleases(release) > 'trusty': | ||
2161 | 32 | # do something with mitaka | ||
2162 | 33 | """ | ||
2163 | 34 | _list = UBUNTU_RELEASES | ||
2164 | 35 | |||
2165 | 36 | |||
2166 | 37 | def service_available(service_name): | ||
2167 | 38 | """Determine whether a system service is available""" | ||
2168 | 39 | try: | ||
2169 | 40 | subprocess.check_output( | ||
2170 | 41 | ['service', service_name, 'status'], | ||
2171 | 42 | stderr=subprocess.STDOUT).decode('UTF-8') | ||
2172 | 43 | except subprocess.CalledProcessError as e: | ||
2173 | 44 | return b'unrecognized service' not in e.output | ||
2174 | 45 | else: | ||
2175 | 46 | return True | ||
2176 | 47 | |||
2177 | 48 | |||
2178 | 49 | def add_new_group(group_name, system_group=False, gid=None): | ||
2179 | 50 | cmd = ['addgroup'] | ||
2180 | 51 | if gid: | ||
2181 | 52 | cmd.extend(['--gid', str(gid)]) | ||
2182 | 53 | if system_group: | ||
2183 | 54 | cmd.append('--system') | ||
2184 | 55 | else: | ||
2185 | 56 | cmd.extend([ | ||
2186 | 57 | '--group', | ||
2187 | 58 | ]) | ||
2188 | 59 | cmd.append(group_name) | ||
2189 | 60 | subprocess.check_call(cmd) | ||
2190 | 61 | |||
2191 | 62 | |||
2192 | 63 | def lsb_release(): | ||
2193 | 64 | """Return /etc/lsb-release in a dict""" | ||
2194 | 65 | d = {} | ||
2195 | 66 | with open('/etc/lsb-release', 'r') as lsb: | ||
2196 | 67 | for l in lsb: | ||
2197 | 68 | k, v = l.split('=') | ||
2198 | 69 | d[k.strip()] = v.strip() | ||
2199 | 70 | return d | ||
2200 | 71 | |||
2201 | 72 | |||
2202 | 73 | def cmp_pkgrevno(package, revno, pkgcache=None): | ||
2203 | 74 | """Compare supplied revno with the revno of the installed package. | ||
2204 | 75 | |||
2205 | 76 | * 1 => Installed revno is greater than supplied arg | ||
2206 | 77 | * 0 => Installed revno is the same as supplied arg | ||
2207 | 78 | * -1 => Installed revno is less than supplied arg | ||
2208 | 79 | |||
2209 | 80 | This function imports apt_cache function from charmhelpers.fetch if | ||
2210 | 81 | the pkgcache argument is None. Be sure to add charmhelpers.fetch if | ||
2211 | 82 | you call this function, or pass an apt_pkg.Cache() instance. | ||
2212 | 83 | """ | ||
2213 | 84 | import apt_pkg | ||
2214 | 85 | if not pkgcache: | ||
2215 | 86 | from charmhelpers.fetch import apt_cache | ||
2216 | 87 | pkgcache = apt_cache() | ||
2217 | 88 | pkg = pkgcache[package] | ||
2218 | 89 | return apt_pkg.version_compare(pkg.current_ver.ver_str, revno) | ||
2219 | 0 | 90 | ||
2220 | === added file 'hooks/charmhelpers/core/hugepage.py' | |||
2221 | --- hooks/charmhelpers/core/hugepage.py 1970-01-01 00:00:00 +0000 | |||
2222 | +++ hooks/charmhelpers/core/hugepage.py 2017-07-18 06:08:45 +0000 | |||
2223 | @@ -0,0 +1,69 @@ | |||
2224 | 1 | # -*- coding: utf-8 -*- | ||
2225 | 2 | |||
2226 | 3 | # Copyright 2014-2015 Canonical Limited. | ||
2227 | 4 | # | ||
2228 | 5 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
2229 | 6 | # you may not use this file except in compliance with the License. | ||
2230 | 7 | # You may obtain a copy of the License at | ||
2231 | 8 | # | ||
2232 | 9 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
2233 | 10 | # | ||
2234 | 11 | # Unless required by applicable law or agreed to in writing, software | ||
2235 | 12 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
2236 | 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
2237 | 14 | # See the License for the specific language governing permissions and | ||
2238 | 15 | # limitations under the License. | ||
2239 | 16 | |||
2240 | 17 | import yaml | ||
2241 | 18 | from charmhelpers.core import fstab | ||
2242 | 19 | from charmhelpers.core import sysctl | ||
2243 | 20 | from charmhelpers.core.host import ( | ||
2244 | 21 | add_group, | ||
2245 | 22 | add_user_to_group, | ||
2246 | 23 | fstab_mount, | ||
2247 | 24 | mkdir, | ||
2248 | 25 | ) | ||
2249 | 26 | from charmhelpers.core.strutils import bytes_from_string | ||
2250 | 27 | from subprocess import check_output | ||
2251 | 28 | |||
2252 | 29 | |||
2253 | 30 | def hugepage_support(user, group='hugetlb', nr_hugepages=256, | ||
2254 | 31 | max_map_count=65536, mnt_point='/run/hugepages/kvm', | ||
2255 | 32 | pagesize='2MB', mount=True, set_shmmax=False): | ||
2256 | 33 | """Enable hugepages on system. | ||
2257 | 34 | |||
2258 | 35 | Args: | ||
2259 | 36 | user (str) -- Username to allow access to hugepages to | ||
2260 | 37 | group (str) -- Group name to own hugepages | ||
2261 | 38 | nr_hugepages (int) -- Number of pages to reserve | ||
2262 | 39 | max_map_count (int) -- Number of Virtual Memory Areas a process can own | ||
2263 | 40 | mnt_point (str) -- Directory to mount hugepages on | ||
2264 | 41 | pagesize (str) -- Size of hugepages | ||
2265 | 42 | mount (bool) -- Whether to Mount hugepages | ||
2266 | 43 | """ | ||
2267 | 44 | group_info = add_group(group) | ||
2268 | 45 | gid = group_info.gr_gid | ||
2269 | 46 | add_user_to_group(user, group) | ||
2270 | 47 | if max_map_count < 2 * nr_hugepages: | ||
2271 | 48 | max_map_count = 2 * nr_hugepages | ||
2272 | 49 | sysctl_settings = { | ||
2273 | 50 | 'vm.nr_hugepages': nr_hugepages, | ||
2274 | 51 | 'vm.max_map_count': max_map_count, | ||
2275 | 52 | 'vm.hugetlb_shm_group': gid, | ||
2276 | 53 | } | ||
2277 | 54 | if set_shmmax: | ||
2278 | 55 | shmmax_current = int(check_output(['sysctl', '-n', 'kernel.shmmax'])) | ||
2279 | 56 | shmmax_minsize = bytes_from_string(pagesize) * nr_hugepages | ||
2280 | 57 | if shmmax_minsize > shmmax_current: | ||
2281 | 58 | sysctl_settings['kernel.shmmax'] = shmmax_minsize | ||
2282 | 59 | sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf') | ||
2283 | 60 | mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False) | ||
2284 | 61 | lfstab = fstab.Fstab() | ||
2285 | 62 | fstab_entry = lfstab.get_entry_by_attr('mountpoint', mnt_point) | ||
2286 | 63 | if fstab_entry: | ||
2287 | 64 | lfstab.remove_entry(fstab_entry) | ||
2288 | 65 | entry = lfstab.Entry('nodev', mnt_point, 'hugetlbfs', | ||
2289 | 66 | 'mode=1770,gid={},pagesize={}'.format(gid, pagesize), 0, 0) | ||
2290 | 67 | lfstab.add_entry(entry) | ||
2291 | 68 | if mount: | ||
2292 | 69 | fstab_mount(mnt_point) | ||
2293 | 0 | 70 | ||
2294 | === added file 'hooks/charmhelpers/core/kernel.py' | |||
2295 | --- hooks/charmhelpers/core/kernel.py 1970-01-01 00:00:00 +0000 | |||
2296 | +++ hooks/charmhelpers/core/kernel.py 2017-07-18 06:08:45 +0000 | |||
2297 | @@ -0,0 +1,72 @@ | |||
2298 | 1 | #!/usr/bin/env python | ||
2299 | 2 | # -*- coding: utf-8 -*- | ||
2300 | 3 | |||
2301 | 4 | # Copyright 2014-2015 Canonical Limited. | ||
2302 | 5 | # | ||
2303 | 6 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
2304 | 7 | # you may not use this file except in compliance with the License. | ||
2305 | 8 | # You may obtain a copy of the License at | ||
2306 | 9 | # | ||
2307 | 10 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
2308 | 11 | # | ||
2309 | 12 | # Unless required by applicable law or agreed to in writing, software | ||
2310 | 13 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
2311 | 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
2312 | 15 | # See the License for the specific language governing permissions and | ||
2313 | 16 | # limitations under the License. | ||
2314 | 17 | |||
2315 | 18 | import re | ||
2316 | 19 | import subprocess | ||
2317 | 20 | |||
2318 | 21 | from charmhelpers.osplatform import get_platform | ||
2319 | 22 | from charmhelpers.core.hookenv import ( | ||
2320 | 23 | log, | ||
2321 | 24 | INFO | ||
2322 | 25 | ) | ||
2323 | 26 | |||
2324 | 27 | __platform__ = get_platform() | ||
2325 | 28 | if __platform__ == "ubuntu": | ||
2326 | 29 | from charmhelpers.core.kernel_factory.ubuntu import ( | ||
2327 | 30 | persistent_modprobe, | ||
2328 | 31 | update_initramfs, | ||
2329 | 32 | ) # flake8: noqa -- ignore F401 for this import | ||
2330 | 33 | elif __platform__ == "centos": | ||
2331 | 34 | from charmhelpers.core.kernel_factory.centos import ( | ||
2332 | 35 | persistent_modprobe, | ||
2333 | 36 | update_initramfs, | ||
2334 | 37 | ) # flake8: noqa -- ignore F401 for this import | ||
2335 | 38 | |||
2336 | 39 | __author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>" | ||
2337 | 40 | |||
2338 | 41 | |||
2339 | 42 | def modprobe(module, persist=True): | ||
2340 | 43 | """Load a kernel module and configure for auto-load on reboot.""" | ||
2341 | 44 | cmd = ['modprobe', module] | ||
2342 | 45 | |||
2343 | 46 | log('Loading kernel module %s' % module, level=INFO) | ||
2344 | 47 | |||
2345 | 48 | subprocess.check_call(cmd) | ||
2346 | 49 | if persist: | ||
2347 | 50 | persistent_modprobe(module) | ||
2348 | 51 | |||
2349 | 52 | |||
2350 | 53 | def rmmod(module, force=False): | ||
2351 | 54 | """Remove a module from the linux kernel""" | ||
2352 | 55 | cmd = ['rmmod'] | ||
2353 | 56 | if force: | ||
2354 | 57 | cmd.append('-f') | ||
2355 | 58 | cmd.append(module) | ||
2356 | 59 | log('Removing kernel module %s' % module, level=INFO) | ||
2357 | 60 | return subprocess.check_call(cmd) | ||
2358 | 61 | |||
2359 | 62 | |||
2360 | 63 | def lsmod(): | ||
2361 | 64 | """Shows what kernel modules are currently loaded""" | ||
2362 | 65 | return subprocess.check_output(['lsmod'], | ||
2363 | 66 | universal_newlines=True) | ||
2364 | 67 | |||
2365 | 68 | |||
2366 | 69 | def is_module_loaded(module): | ||
2367 | 70 | """Checks if a kernel module is already loaded""" | ||
2368 | 71 | matches = re.findall('^%s[ ]+' % module, lsmod(), re.M) | ||
2369 | 72 | return len(matches) > 0 | ||
2370 | 0 | 73 | ||
2371 | === added directory 'hooks/charmhelpers/core/kernel_factory' | |||
2372 | === added file 'hooks/charmhelpers/core/kernel_factory/__init__.py' | |||
2373 | === added file 'hooks/charmhelpers/core/kernel_factory/centos.py' | |||
2374 | --- hooks/charmhelpers/core/kernel_factory/centos.py 1970-01-01 00:00:00 +0000 | |||
2375 | +++ hooks/charmhelpers/core/kernel_factory/centos.py 2017-07-18 06:08:45 +0000 | |||
2376 | @@ -0,0 +1,17 @@ | |||
2377 | 1 | import subprocess | ||
2378 | 2 | import os | ||
2379 | 3 | |||
2380 | 4 | |||
2381 | 5 | def persistent_modprobe(module): | ||
2382 | 6 | """Load a kernel module and configure for auto-load on reboot.""" | ||
2383 | 7 | if not os.path.exists('/etc/rc.modules'): | ||
2384 | 8 | open('/etc/rc.modules', 'a') | ||
2385 | 9 | os.chmod('/etc/rc.modules', 111) | ||
2386 | 10 | with open('/etc/rc.modules', 'r+') as modules: | ||
2387 | 11 | if module not in modules.read(): | ||
2388 | 12 | modules.write('modprobe %s\n' % module) | ||
2389 | 13 | |||
2390 | 14 | |||
2391 | 15 | def update_initramfs(version='all'): | ||
2392 | 16 | """Updates an initramfs image.""" | ||
2393 | 17 | return subprocess.check_call(["dracut", "-f", version]) | ||
2394 | 0 | 18 | ||
2395 | === added file 'hooks/charmhelpers/core/kernel_factory/ubuntu.py' | |||
2396 | --- hooks/charmhelpers/core/kernel_factory/ubuntu.py 1970-01-01 00:00:00 +0000 | |||
2397 | +++ hooks/charmhelpers/core/kernel_factory/ubuntu.py 2017-07-18 06:08:45 +0000 | |||
2398 | @@ -0,0 +1,13 @@ | |||
2399 | 1 | import subprocess | ||
2400 | 2 | |||
2401 | 3 | |||
2402 | 4 | def persistent_modprobe(module): | ||
2403 | 5 | """Load a kernel module and configure for auto-load on reboot.""" | ||
2404 | 6 | with open('/etc/modules', 'r+') as modules: | ||
2405 | 7 | if module not in modules.read(): | ||
2406 | 8 | modules.write(module + "\n") | ||
2407 | 9 | |||
2408 | 10 | |||
2409 | 11 | def update_initramfs(version='all'): | ||
2410 | 12 | """Updates an initramfs image.""" | ||
2411 | 13 | return subprocess.check_call(["update-initramfs", "-k", version, "-u"]) | ||
2412 | 0 | 14 | ||
2413 | === modified file 'hooks/charmhelpers/core/services/__init__.py' | |||
2414 | --- hooks/charmhelpers/core/services/__init__.py 2015-04-08 05:57:06 +0000 | |||
2415 | +++ hooks/charmhelpers/core/services/__init__.py 2017-07-18 06:08:45 +0000 | |||
2416 | @@ -1,18 +1,16 @@ | |||
2417 | 1 | # Copyright 2014-2015 Canonical Limited. | 1 | # Copyright 2014-2015 Canonical Limited. |
2418 | 2 | # | 2 | # |
2432 | 3 | # This file is part of charm-helpers. | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
2433 | 4 | # | 4 | # you may not use this file except in compliance with the License. |
2434 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | 5 | # You may obtain a copy of the License at |
2435 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | 6 | # |
2436 | 7 | # published by the Free Software Foundation. | 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
2437 | 8 | # | 8 | # |
2438 | 9 | # charm-helpers is distributed in the hope that it will be useful, | 9 | # Unless required by applicable law or agreed to in writing, software |
2439 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
2440 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
2441 | 12 | # GNU Lesser General Public License for more details. | 12 | # See the License for the specific language governing permissions and |
2442 | 13 | # | 13 | # limitations under the License. |
2430 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
2431 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2443 | 16 | 14 | ||
2444 | 17 | from .base import * # NOQA | 15 | from .base import * # NOQA |
2445 | 18 | from .helpers import * # NOQA | 16 | from .helpers import * # NOQA |
2446 | 19 | 17 | ||
2447 | === modified file 'hooks/charmhelpers/core/services/base.py' | |||
2448 | --- hooks/charmhelpers/core/services/base.py 2015-04-08 05:57:06 +0000 | |||
2449 | +++ hooks/charmhelpers/core/services/base.py 2017-07-18 06:08:45 +0000 | |||
2450 | @@ -1,23 +1,21 @@ | |||
2451 | 1 | # Copyright 2014-2015 Canonical Limited. | 1 | # Copyright 2014-2015 Canonical Limited. |
2452 | 2 | # | 2 | # |
2466 | 3 | # This file is part of charm-helpers. | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
2467 | 4 | # | 4 | # you may not use this file except in compliance with the License. |
2468 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | 5 | # You may obtain a copy of the License at |
2469 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | 6 | # |
2470 | 7 | # published by the Free Software Foundation. | 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
2471 | 8 | # | 8 | # |
2472 | 9 | # charm-helpers is distributed in the hope that it will be useful, | 9 | # Unless required by applicable law or agreed to in writing, software |
2473 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
2474 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
2475 | 12 | # GNU Lesser General Public License for more details. | 12 | # See the License for the specific language governing permissions and |
2476 | 13 | # | 13 | # limitations under the License. |
2464 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
2465 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2477 | 16 | 14 | ||
2478 | 17 | import os | 15 | import os |
2479 | 18 | import re | ||
2480 | 19 | import json | 16 | import json |
2482 | 20 | from collections import Iterable | 17 | from inspect import getargspec |
2483 | 18 | from collections import Iterable, OrderedDict | ||
2484 | 21 | 19 | ||
2485 | 22 | from charmhelpers.core import host | 20 | from charmhelpers.core import host |
2486 | 23 | from charmhelpers.core import hookenv | 21 | from charmhelpers.core import hookenv |
2487 | @@ -119,7 +117,7 @@ | |||
2488 | 119 | """ | 117 | """ |
2489 | 120 | self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json') | 118 | self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json') |
2490 | 121 | self._ready = None | 119 | self._ready = None |
2492 | 122 | self.services = {} | 120 | self.services = OrderedDict() |
2493 | 123 | for service in services or []: | 121 | for service in services or []: |
2494 | 124 | service_name = service['service'] | 122 | service_name = service['service'] |
2495 | 125 | self.services[service_name] = service | 123 | self.services[service_name] = service |
2496 | @@ -128,15 +126,18 @@ | |||
2497 | 128 | """ | 126 | """ |
2498 | 129 | Handle the current hook by doing The Right Thing with the registered services. | 127 | Handle the current hook by doing The Right Thing with the registered services. |
2499 | 130 | """ | 128 | """ |
2509 | 131 | hook_name = hookenv.hook_name() | 129 | hookenv._run_atstart() |
2510 | 132 | if hook_name == 'stop': | 130 | try: |
2511 | 133 | self.stop_services() | 131 | hook_name = hookenv.hook_name() |
2512 | 134 | else: | 132 | if hook_name == 'stop': |
2513 | 135 | self.provide_data() | 133 | self.stop_services() |
2514 | 136 | self.reconfigure_services() | 134 | else: |
2515 | 137 | cfg = hookenv.config() | 135 | self.reconfigure_services() |
2516 | 138 | if cfg.implicit_save: | 136 | self.provide_data() |
2517 | 139 | cfg.save() | 137 | except SystemExit as x: |
2518 | 138 | if x.code is None or x.code == 0: | ||
2519 | 139 | hookenv._run_atexit() | ||
2520 | 140 | hookenv._run_atexit() | ||
2521 | 140 | 141 | ||
2522 | 141 | def provide_data(self): | 142 | def provide_data(self): |
2523 | 142 | """ | 143 | """ |
2524 | @@ -145,15 +146,36 @@ | |||
2525 | 145 | A provider must have a `name` attribute, which indicates which relation | 146 | A provider must have a `name` attribute, which indicates which relation |
2526 | 146 | to set data on, and a `provide_data()` method, which returns a dict of | 147 | to set data on, and a `provide_data()` method, which returns a dict of |
2527 | 147 | data to set. | 148 | data to set. |
2528 | 149 | |||
2529 | 150 | The `provide_data()` method can optionally accept two parameters: | ||
2530 | 151 | |||
2531 | 152 | * ``remote_service`` The name of the remote service that the data will | ||
2532 | 153 | be provided to. The `provide_data()` method will be called once | ||
2533 | 154 | for each connected service (not unit). This allows the method to | ||
2534 | 155 | tailor its data to the given service. | ||
2535 | 156 | * ``service_ready`` Whether or not the service definition had all of | ||
2536 | 157 | its requirements met, and thus the ``data_ready`` callbacks run. | ||
2537 | 158 | |||
2538 | 159 | Note that the ``provided_data`` methods are now called **after** the | ||
2539 | 160 | ``data_ready`` callbacks are run. This gives the ``data_ready`` callbacks | ||
2540 | 161 | a chance to generate any data necessary for the providing to the remote | ||
2541 | 162 | services. | ||
2542 | 148 | """ | 163 | """ |
2545 | 149 | hook_name = hookenv.hook_name() | 164 | for service_name, service in self.services.items(): |
2546 | 150 | for service in self.services.values(): | 165 | service_ready = self.is_ready(service_name) |
2547 | 151 | for provider in service.get('provided_data', []): | 166 | for provider in service.get('provided_data', []): |
2553 | 152 | if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name): | 167 | for relid in hookenv.relation_ids(provider.name): |
2554 | 153 | data = provider.provide_data() | 168 | units = hookenv.related_units(relid) |
2555 | 154 | _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data | 169 | if not units: |
2556 | 155 | if _ready: | 170 | continue |
2557 | 156 | hookenv.relation_set(None, data) | 171 | remote_service = units[0].split('/')[0] |
2558 | 172 | argspec = getargspec(provider.provide_data) | ||
2559 | 173 | if len(argspec.args) > 1: | ||
2560 | 174 | data = provider.provide_data(remote_service, service_ready) | ||
2561 | 175 | else: | ||
2562 | 176 | data = provider.provide_data() | ||
2563 | 177 | if data: | ||
2564 | 178 | hookenv.relation_set(relid, data) | ||
2565 | 157 | 179 | ||
2566 | 158 | def reconfigure_services(self, *service_names): | 180 | def reconfigure_services(self, *service_names): |
2567 | 159 | """ | 181 | """ |
2568 | 160 | 182 | ||
2569 | === modified file 'hooks/charmhelpers/core/services/helpers.py' | |||
2570 | --- hooks/charmhelpers/core/services/helpers.py 2015-04-08 05:57:06 +0000 | |||
2571 | +++ hooks/charmhelpers/core/services/helpers.py 2017-07-18 06:08:45 +0000 | |||
2572 | @@ -1,22 +1,22 @@ | |||
2573 | 1 | # Copyright 2014-2015 Canonical Limited. | 1 | # Copyright 2014-2015 Canonical Limited. |
2574 | 2 | # | 2 | # |
2588 | 3 | # This file is part of charm-helpers. | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
2589 | 4 | # | 4 | # you may not use this file except in compliance with the License. |
2590 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | 5 | # You may obtain a copy of the License at |
2591 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | 6 | # |
2592 | 7 | # published by the Free Software Foundation. | 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
2593 | 8 | # | 8 | # |
2594 | 9 | # charm-helpers is distributed in the hope that it will be useful, | 9 | # Unless required by applicable law or agreed to in writing, software |
2595 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
2596 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
2597 | 12 | # GNU Lesser General Public License for more details. | 12 | # See the License for the specific language governing permissions and |
2598 | 13 | # | 13 | # limitations under the License. |
2586 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
2587 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2599 | 16 | 14 | ||
2600 | 17 | import os | 15 | import os |
2601 | 18 | import yaml | 16 | import yaml |
2602 | 17 | |||
2603 | 19 | from charmhelpers.core import hookenv | 18 | from charmhelpers.core import hookenv |
2604 | 19 | from charmhelpers.core import host | ||
2605 | 20 | from charmhelpers.core import templating | 20 | from charmhelpers.core import templating |
2606 | 21 | 21 | ||
2607 | 22 | from charmhelpers.core.services.base import ManagerCallback | 22 | from charmhelpers.core.services.base import ManagerCallback |
2608 | @@ -239,28 +239,51 @@ | |||
2609 | 239 | action. | 239 | action. |
2610 | 240 | 240 | ||
2611 | 241 | :param str source: The template source file, relative to | 241 | :param str source: The template source file, relative to |
2613 | 242 | `$CHARM_DIR/templates` | 242 | `$CHARM_DIR/templates` |
2614 | 243 | 243 | ||
2616 | 244 | :param str target: The target to write the rendered template to | 244 | :param str target: The target to write the rendered template to (or None) |
2617 | 245 | :param str owner: The owner of the rendered file | 245 | :param str owner: The owner of the rendered file |
2618 | 246 | :param str group: The group of the rendered file | 246 | :param str group: The group of the rendered file |
2619 | 247 | :param int perms: The permissions of the rendered file | 247 | :param int perms: The permissions of the rendered file |
2620 | 248 | :param partial on_change_action: functools partial to be executed when | ||
2621 | 249 | rendered file changes | ||
2622 | 250 | :param jinja2 loader template_loader: A jinja2 template loader | ||
2623 | 251 | |||
2624 | 252 | :return str: The rendered template | ||
2625 | 248 | """ | 253 | """ |
2626 | 249 | def __init__(self, source, target, | 254 | def __init__(self, source, target, |
2628 | 250 | owner='root', group='root', perms=0o444): | 255 | owner='root', group='root', perms=0o444, |
2629 | 256 | on_change_action=None, template_loader=None): | ||
2630 | 251 | self.source = source | 257 | self.source = source |
2631 | 252 | self.target = target | 258 | self.target = target |
2632 | 253 | self.owner = owner | 259 | self.owner = owner |
2633 | 254 | self.group = group | 260 | self.group = group |
2634 | 255 | self.perms = perms | 261 | self.perms = perms |
2635 | 262 | self.on_change_action = on_change_action | ||
2636 | 263 | self.template_loader = template_loader | ||
2637 | 256 | 264 | ||
2638 | 257 | def __call__(self, manager, service_name, event_name): | 265 | def __call__(self, manager, service_name, event_name): |
2639 | 266 | pre_checksum = '' | ||
2640 | 267 | if self.on_change_action and os.path.isfile(self.target): | ||
2641 | 268 | pre_checksum = host.file_hash(self.target) | ||
2642 | 258 | service = manager.get_service(service_name) | 269 | service = manager.get_service(service_name) |
2644 | 259 | context = {} | 270 | context = {'ctx': {}} |
2645 | 260 | for ctx in service.get('required_data', []): | 271 | for ctx in service.get('required_data', []): |
2646 | 261 | context.update(ctx) | 272 | context.update(ctx) |
2649 | 262 | templating.render(self.source, self.target, context, | 273 | context['ctx'].update(ctx) |
2650 | 263 | self.owner, self.group, self.perms) | 274 | |
2651 | 275 | result = templating.render(self.source, self.target, context, | ||
2652 | 276 | self.owner, self.group, self.perms, | ||
2653 | 277 | template_loader=self.template_loader) | ||
2654 | 278 | if self.on_change_action: | ||
2655 | 279 | if pre_checksum == host.file_hash(self.target): | ||
2656 | 280 | hookenv.log( | ||
2657 | 281 | 'No change detected: {}'.format(self.target), | ||
2658 | 282 | hookenv.DEBUG) | ||
2659 | 283 | else: | ||
2660 | 284 | self.on_change_action() | ||
2661 | 285 | |||
2662 | 286 | return result | ||
2663 | 264 | 287 | ||
2664 | 265 | 288 | ||
2665 | 266 | # Convenience aliases for templates | 289 | # Convenience aliases for templates |
2666 | 267 | 290 | ||
2667 | === modified file 'hooks/charmhelpers/core/strutils.py' | |||
2668 | --- hooks/charmhelpers/core/strutils.py 2015-04-08 05:57:06 +0000 | |||
2669 | +++ hooks/charmhelpers/core/strutils.py 2017-07-18 06:08:45 +0000 | |||
2670 | @@ -3,21 +3,20 @@ | |||
2671 | 3 | 3 | ||
2672 | 4 | # Copyright 2014-2015 Canonical Limited. | 4 | # Copyright 2014-2015 Canonical Limited. |
2673 | 5 | # | 5 | # |
2687 | 6 | # This file is part of charm-helpers. | 6 | # Licensed under the Apache License, Version 2.0 (the "License"); |
2688 | 7 | # | 7 | # you may not use this file except in compliance with the License. |
2689 | 8 | # charm-helpers is free software: you can redistribute it and/or modify | 8 | # You may obtain a copy of the License at |
2690 | 9 | # it under the terms of the GNU Lesser General Public License version 3 as | 9 | # |
2691 | 10 | # published by the Free Software Foundation. | 10 | # http://www.apache.org/licenses/LICENSE-2.0 |
2692 | 11 | # | 11 | # |
2693 | 12 | # charm-helpers is distributed in the hope that it will be useful, | 12 | # Unless required by applicable law or agreed to in writing, software |
2694 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | 13 | # distributed under the License is distributed on an "AS IS" BASIS, |
2695 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
2696 | 15 | # GNU Lesser General Public License for more details. | 15 | # See the License for the specific language governing permissions and |
2697 | 16 | # | 16 | # limitations under the License. |
2685 | 17 | # You should have received a copy of the GNU Lesser General Public License | ||
2686 | 18 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2698 | 19 | 17 | ||
2699 | 20 | import six | 18 | import six |
2700 | 19 | import re | ||
2701 | 21 | 20 | ||
2702 | 22 | 21 | ||
2703 | 23 | def bool_from_string(value): | 22 | def bool_from_string(value): |
2704 | @@ -33,10 +32,92 @@ | |||
2705 | 33 | 32 | ||
2706 | 34 | value = value.strip().lower() | 33 | value = value.strip().lower() |
2707 | 35 | 34 | ||
2709 | 36 | if value in ['y', 'yes', 'true', 't']: | 35 | if value in ['y', 'yes', 'true', 't', 'on']: |
2710 | 37 | return True | 36 | return True |
2712 | 38 | elif value in ['n', 'no', 'false', 'f']: | 37 | elif value in ['n', 'no', 'false', 'f', 'off']: |
2713 | 39 | return False | 38 | return False |
2714 | 40 | 39 | ||
2715 | 41 | msg = "Unable to interpret string value '%s' as boolean" % (value) | 40 | msg = "Unable to interpret string value '%s' as boolean" % (value) |
2716 | 42 | raise ValueError(msg) | 41 | raise ValueError(msg) |
2717 | 42 | |||
2718 | 43 | |||
2719 | 44 | def bytes_from_string(value): | ||
2720 | 45 | """Interpret human readable string value as bytes. | ||
2721 | 46 | |||
2722 | 47 | Returns int | ||
2723 | 48 | """ | ||
2724 | 49 | BYTE_POWER = { | ||
2725 | 50 | 'K': 1, | ||
2726 | 51 | 'KB': 1, | ||
2727 | 52 | 'M': 2, | ||
2728 | 53 | 'MB': 2, | ||
2729 | 54 | 'G': 3, | ||
2730 | 55 | 'GB': 3, | ||
2731 | 56 | 'T': 4, | ||
2732 | 57 | 'TB': 4, | ||
2733 | 58 | 'P': 5, | ||
2734 | 59 | 'PB': 5, | ||
2735 | 60 | } | ||
2736 | 61 | if isinstance(value, six.string_types): | ||
2737 | 62 | value = six.text_type(value) | ||
2738 | 63 | else: | ||
2739 | 64 | msg = "Unable to interpret non-string value '%s' as boolean" % (value) | ||
2740 | 65 | raise ValueError(msg) | ||
2741 | 66 | matches = re.match("([0-9]+)([a-zA-Z]+)", value) | ||
2742 | 67 | if not matches: | ||
2743 | 68 | msg = "Unable to interpret string value '%s' as bytes" % (value) | ||
2744 | 69 | raise ValueError(msg) | ||
2745 | 70 | return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)]) | ||
2746 | 71 | |||
2747 | 72 | |||
2748 | 73 | class BasicStringComparator(object): | ||
2749 | 74 | """Provides a class that will compare strings from an iterator type object. | ||
2750 | 75 | Used to provide > and < comparisons on strings that may not necessarily be | ||
2751 | 76 | alphanumerically ordered. e.g. OpenStack or Ubuntu releases AFTER the | ||
2752 | 77 | z-wrap. | ||
2753 | 78 | """ | ||
2754 | 79 | |||
2755 | 80 | _list = None | ||
2756 | 81 | |||
2757 | 82 | def __init__(self, item): | ||
2758 | 83 | if self._list is None: | ||
2759 | 84 | raise Exception("Must define the _list in the class definition!") | ||
2760 | 85 | try: | ||
2761 | 86 | self.index = self._list.index(item) | ||
2762 | 87 | except Exception: | ||
2763 | 88 | raise KeyError("Item '{}' is not in list '{}'" | ||
2764 | 89 | .format(item, self._list)) | ||
2765 | 90 | |||
2766 | 91 | def __eq__(self, other): | ||
2767 | 92 | assert isinstance(other, str) or isinstance(other, self.__class__) | ||
2768 | 93 | return self.index == self._list.index(other) | ||
2769 | 94 | |||
2770 | 95 | def __ne__(self, other): | ||
2771 | 96 | return not self.__eq__(other) | ||
2772 | 97 | |||
2773 | 98 | def __lt__(self, other): | ||
2774 | 99 | assert isinstance(other, str) or isinstance(other, self.__class__) | ||
2775 | 100 | return self.index < self._list.index(other) | ||
2776 | 101 | |||
2777 | 102 | def __ge__(self, other): | ||
2778 | 103 | return not self.__lt__(other) | ||
2779 | 104 | |||
2780 | 105 | def __gt__(self, other): | ||
2781 | 106 | assert isinstance(other, str) or isinstance(other, self.__class__) | ||
2782 | 107 | return self.index > self._list.index(other) | ||
2783 | 108 | |||
2784 | 109 | def __le__(self, other): | ||
2785 | 110 | return not self.__gt__(other) | ||
2786 | 111 | |||
2787 | 112 | def __str__(self): | ||
2788 | 113 | """Always give back the item at the index so it can be used in | ||
2789 | 114 | comparisons like: | ||
2790 | 115 | |||
2791 | 116 | s_mitaka = CompareOpenStack('mitaka') | ||
2792 | 117 | s_newton = CompareOpenstack('newton') | ||
2793 | 118 | |||
2794 | 119 | assert s_newton > s_mitaka | ||
2795 | 120 | |||
2796 | 121 | @returns: <string> | ||
2797 | 122 | """ | ||
2798 | 123 | return self._list[self.index] | ||
2799 | 43 | 124 | ||
2800 | === modified file 'hooks/charmhelpers/core/sysctl.py' | |||
2801 | --- hooks/charmhelpers/core/sysctl.py 2015-04-08 05:57:06 +0000 | |||
2802 | +++ hooks/charmhelpers/core/sysctl.py 2017-07-18 06:08:45 +0000 | |||
2803 | @@ -3,19 +3,17 @@ | |||
2804 | 3 | 3 | ||
2805 | 4 | # Copyright 2014-2015 Canonical Limited. | 4 | # Copyright 2014-2015 Canonical Limited. |
2806 | 5 | # | 5 | # |
2820 | 6 | # This file is part of charm-helpers. | 6 | # Licensed under the Apache License, Version 2.0 (the "License"); |
2821 | 7 | # | 7 | # you may not use this file except in compliance with the License. |
2822 | 8 | # charm-helpers is free software: you can redistribute it and/or modify | 8 | # You may obtain a copy of the License at |
2823 | 9 | # it under the terms of the GNU Lesser General Public License version 3 as | 9 | # |
2824 | 10 | # published by the Free Software Foundation. | 10 | # http://www.apache.org/licenses/LICENSE-2.0 |
2825 | 11 | # | 11 | # |
2826 | 12 | # charm-helpers is distributed in the hope that it will be useful, | 12 | # Unless required by applicable law or agreed to in writing, software |
2827 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | 13 | # distributed under the License is distributed on an "AS IS" BASIS, |
2828 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
2829 | 15 | # GNU Lesser General Public License for more details. | 15 | # See the License for the specific language governing permissions and |
2830 | 16 | # | 16 | # limitations under the License. |
2818 | 17 | # You should have received a copy of the GNU Lesser General Public License | ||
2819 | 18 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2831 | 19 | 17 | ||
2832 | 20 | import yaml | 18 | import yaml |
2833 | 21 | 19 | ||
2834 | 22 | 20 | ||
2835 | === modified file 'hooks/charmhelpers/core/templating.py' | |||
2836 | --- hooks/charmhelpers/core/templating.py 2015-04-08 05:57:06 +0000 | |||
2837 | +++ hooks/charmhelpers/core/templating.py 2017-07-18 06:08:45 +0000 | |||
2838 | @@ -1,33 +1,33 @@ | |||
2839 | 1 | # Copyright 2014-2015 Canonical Limited. | 1 | # Copyright 2014-2015 Canonical Limited. |
2840 | 2 | # | 2 | # |
2854 | 3 | # This file is part of charm-helpers. | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
2855 | 4 | # | 4 | # you may not use this file except in compliance with the License. |
2856 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | 5 | # You may obtain a copy of the License at |
2857 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | 6 | # |
2858 | 7 | # published by the Free Software Foundation. | 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
2859 | 8 | # | 8 | # |
2860 | 9 | # charm-helpers is distributed in the hope that it will be useful, | 9 | # Unless required by applicable law or agreed to in writing, software |
2861 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
2862 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
2863 | 12 | # GNU Lesser General Public License for more details. | 12 | # See the License for the specific language governing permissions and |
2864 | 13 | # | 13 | # limitations under the License. |
2852 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
2853 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2865 | 16 | 14 | ||
2866 | 17 | import os | 15 | import os |
2867 | 16 | import sys | ||
2868 | 18 | 17 | ||
2869 | 19 | from charmhelpers.core import host | 18 | from charmhelpers.core import host |
2870 | 20 | from charmhelpers.core import hookenv | 19 | from charmhelpers.core import hookenv |
2871 | 21 | 20 | ||
2872 | 22 | 21 | ||
2873 | 23 | def render(source, target, context, owner='root', group='root', | 22 | def render(source, target, context, owner='root', group='root', |
2875 | 24 | perms=0o444, templates_dir=None, encoding='UTF-8'): | 23 | perms=0o444, templates_dir=None, encoding='UTF-8', template_loader=None): |
2876 | 25 | """ | 24 | """ |
2877 | 26 | Render a template. | 25 | Render a template. |
2878 | 27 | 26 | ||
2879 | 28 | The `source` path, if not absolute, is relative to the `templates_dir`. | 27 | The `source` path, if not absolute, is relative to the `templates_dir`. |
2880 | 29 | 28 | ||
2882 | 30 | The `target` path should be absolute. | 29 | The `target` path should be absolute. It can also be `None`, in which |
2883 | 30 | case no file will be written. | ||
2884 | 31 | 31 | ||
2885 | 32 | The context should be a dict containing the values to be replaced in the | 32 | The context should be a dict containing the values to be replaced in the |
2886 | 33 | template. | 33 | template. |
2887 | @@ -36,8 +36,12 @@ | |||
2888 | 36 | 36 | ||
2889 | 37 | If omitted, `templates_dir` defaults to the `templates` folder in the charm. | 37 | If omitted, `templates_dir` defaults to the `templates` folder in the charm. |
2890 | 38 | 38 | ||
2893 | 39 | Note: Using this requires python-jinja2; if it is not installed, calling | 39 | The rendered template will be written to the file as well as being returned |
2894 | 40 | this will attempt to use charmhelpers.fetch.apt_install to install it. | 40 | as a string. |
2895 | 41 | |||
2896 | 42 | Note: Using this requires python-jinja2 or python3-jinja2; if it is not | ||
2897 | 43 | installed, calling this will attempt to use charmhelpers.fetch.apt_install | ||
2898 | 44 | to install it. | ||
2899 | 41 | """ | 45 | """ |
2900 | 42 | try: | 46 | try: |
2901 | 43 | from jinja2 import FileSystemLoader, Environment, exceptions | 47 | from jinja2 import FileSystemLoader, Environment, exceptions |
2902 | @@ -49,20 +53,32 @@ | |||
2903 | 49 | 'charmhelpers.fetch to install it', | 53 | 'charmhelpers.fetch to install it', |
2904 | 50 | level=hookenv.ERROR) | 54 | level=hookenv.ERROR) |
2905 | 51 | raise | 55 | raise |
2907 | 52 | apt_install('python-jinja2', fatal=True) | 56 | if sys.version_info.major == 2: |
2908 | 57 | apt_install('python-jinja2', fatal=True) | ||
2909 | 58 | else: | ||
2910 | 59 | apt_install('python3-jinja2', fatal=True) | ||
2911 | 53 | from jinja2 import FileSystemLoader, Environment, exceptions | 60 | from jinja2 import FileSystemLoader, Environment, exceptions |
2912 | 54 | 61 | ||
2916 | 55 | if templates_dir is None: | 62 | if template_loader: |
2917 | 56 | templates_dir = os.path.join(hookenv.charm_dir(), 'templates') | 63 | template_env = Environment(loader=template_loader) |
2918 | 57 | loader = Environment(loader=FileSystemLoader(templates_dir)) | 64 | else: |
2919 | 65 | if templates_dir is None: | ||
2920 | 66 | templates_dir = os.path.join(hookenv.charm_dir(), 'templates') | ||
2921 | 67 | template_env = Environment(loader=FileSystemLoader(templates_dir)) | ||
2922 | 58 | try: | 68 | try: |
2923 | 59 | source = source | 69 | source = source |
2925 | 60 | template = loader.get_template(source) | 70 | template = template_env.get_template(source) |
2926 | 61 | except exceptions.TemplateNotFound as e: | 71 | except exceptions.TemplateNotFound as e: |
2927 | 62 | hookenv.log('Could not load template %s from %s.' % | 72 | hookenv.log('Could not load template %s from %s.' % |
2928 | 63 | (source, templates_dir), | 73 | (source, templates_dir), |
2929 | 64 | level=hookenv.ERROR) | 74 | level=hookenv.ERROR) |
2930 | 65 | raise e | 75 | raise e |
2931 | 66 | content = template.render(context) | 76 | content = template.render(context) |
2934 | 67 | host.mkdir(os.path.dirname(target), owner, group, perms=0o755) | 77 | if target is not None: |
2935 | 68 | host.write_file(target, content.encode(encoding), owner, group, perms) | 78 | target_dir = os.path.dirname(target) |
2936 | 79 | if not os.path.exists(target_dir): | ||
2937 | 80 | # This is a terrible default directory permission, as the file | ||
2938 | 81 | # or its siblings will often contain secrets. | ||
2939 | 82 | host.mkdir(os.path.dirname(target), owner, group, perms=0o755) | ||
2940 | 83 | host.write_file(target, content.encode(encoding), owner, group, perms) | ||
2941 | 84 | return content | ||
2942 | 69 | 85 | ||
2943 | === modified file 'hooks/charmhelpers/core/unitdata.py' | |||
2944 | --- hooks/charmhelpers/core/unitdata.py 2015-04-08 05:57:06 +0000 | |||
2945 | +++ hooks/charmhelpers/core/unitdata.py 2017-07-18 06:08:45 +0000 | |||
2946 | @@ -3,20 +3,17 @@ | |||
2947 | 3 | # | 3 | # |
2948 | 4 | # Copyright 2014-2015 Canonical Limited. | 4 | # Copyright 2014-2015 Canonical Limited. |
2949 | 5 | # | 5 | # |
2964 | 6 | # This file is part of charm-helpers. | 6 | # Licensed under the Apache License, Version 2.0 (the "License"); |
2965 | 7 | # | 7 | # you may not use this file except in compliance with the License. |
2966 | 8 | # charm-helpers is free software: you can redistribute it and/or modify | 8 | # You may obtain a copy of the License at |
2967 | 9 | # it under the terms of the GNU Lesser General Public License version 3 as | 9 | # |
2968 | 10 | # published by the Free Software Foundation. | 10 | # http://www.apache.org/licenses/LICENSE-2.0 |
2969 | 11 | # | 11 | # |
2970 | 12 | # charm-helpers is distributed in the hope that it will be useful, | 12 | # Unless required by applicable law or agreed to in writing, software |
2971 | 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | 13 | # distributed under the License is distributed on an "AS IS" BASIS, |
2972 | 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
2973 | 15 | # GNU Lesser General Public License for more details. | 15 | # See the License for the specific language governing permissions and |
2974 | 16 | # | 16 | # limitations under the License. |
2961 | 17 | # You should have received a copy of the GNU Lesser General Public License | ||
2962 | 18 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
2963 | 19 | # | ||
2975 | 20 | # | 17 | # |
2976 | 21 | # Authors: | 18 | # Authors: |
2977 | 22 | # Kapil Thangavelu <kapil.foss@gmail.com> | 19 | # Kapil Thangavelu <kapil.foss@gmail.com> |
2978 | @@ -152,6 +149,7 @@ | |||
2979 | 152 | import collections | 149 | import collections |
2980 | 153 | import contextlib | 150 | import contextlib |
2981 | 154 | import datetime | 151 | import datetime |
2982 | 152 | import itertools | ||
2983 | 155 | import json | 153 | import json |
2984 | 156 | import os | 154 | import os |
2985 | 157 | import pprint | 155 | import pprint |
2986 | @@ -164,8 +162,7 @@ | |||
2987 | 164 | class Storage(object): | 162 | class Storage(object): |
2988 | 165 | """Simple key value database for local unit state within charms. | 163 | """Simple key value database for local unit state within charms. |
2989 | 166 | 164 | ||
2992 | 167 | Modifications are automatically committed at hook exit. That's | 165 | Modifications are not persisted unless :meth:`flush` is called. |
2991 | 168 | currently regardless of exit code. | ||
2993 | 169 | 166 | ||
2994 | 170 | To support dicts, lists, integer, floats, and booleans values | 167 | To support dicts, lists, integer, floats, and booleans values |
2995 | 171 | are automatically json encoded/decoded. | 168 | are automatically json encoded/decoded. |
2996 | @@ -173,8 +170,11 @@ | |||
2997 | 173 | def __init__(self, path=None): | 170 | def __init__(self, path=None): |
2998 | 174 | self.db_path = path | 171 | self.db_path = path |
2999 | 175 | if path is None: | 172 | if path is None: |
3002 | 176 | self.db_path = os.path.join( | 173 | if 'UNIT_STATE_DB' in os.environ: |
3003 | 177 | os.environ.get('CHARM_DIR', ''), '.unit-state.db') | 174 | self.db_path = os.environ['UNIT_STATE_DB'] |
3004 | 175 | else: | ||
3005 | 176 | self.db_path = os.path.join( | ||
3006 | 177 | os.environ.get('CHARM_DIR', ''), '.unit-state.db') | ||
3007 | 178 | self.conn = sqlite3.connect('%s' % self.db_path) | 178 | self.conn = sqlite3.connect('%s' % self.db_path) |
3008 | 179 | self.cursor = self.conn.cursor() | 179 | self.cursor = self.conn.cursor() |
3009 | 180 | self.revision = None | 180 | self.revision = None |
3010 | @@ -189,15 +189,8 @@ | |||
3011 | 189 | self.conn.close() | 189 | self.conn.close() |
3012 | 190 | self._closed = True | 190 | self._closed = True |
3013 | 191 | 191 | ||
3014 | 192 | def _scoped_query(self, stmt, params=None): | ||
3015 | 193 | if params is None: | ||
3016 | 194 | params = [] | ||
3017 | 195 | return stmt, params | ||
3018 | 196 | |||
3019 | 197 | def get(self, key, default=None, record=False): | 192 | def get(self, key, default=None, record=False): |
3023 | 198 | self.cursor.execute( | 193 | self.cursor.execute('select data from kv where key=?', [key]) |
3021 | 199 | *self._scoped_query( | ||
3022 | 200 | 'select data from kv where key=?', [key])) | ||
3024 | 201 | result = self.cursor.fetchone() | 194 | result = self.cursor.fetchone() |
3025 | 202 | if not result: | 195 | if not result: |
3026 | 203 | return default | 196 | return default |
3027 | @@ -206,33 +199,81 @@ | |||
3028 | 206 | return json.loads(result[0]) | 199 | return json.loads(result[0]) |
3029 | 207 | 200 | ||
3030 | 208 | def getrange(self, key_prefix, strip=False): | 201 | def getrange(self, key_prefix, strip=False): |
3033 | 209 | stmt = "select key, data from kv where key like '%s%%'" % key_prefix | 202 | """ |
3034 | 210 | self.cursor.execute(*self._scoped_query(stmt)) | 203 | Get a range of keys starting with a common prefix as a mapping of |
3035 | 204 | keys to values. | ||
3036 | 205 | |||
3037 | 206 | :param str key_prefix: Common prefix among all keys | ||
3038 | 207 | :param bool strip: Optionally strip the common prefix from the key | ||
3039 | 208 | names in the returned dict | ||
3040 | 209 | :return dict: A (possibly empty) dict of key-value mappings | ||
3041 | 210 | """ | ||
3042 | 211 | self.cursor.execute("select key, data from kv where key like ?", | ||
3043 | 212 | ['%s%%' % key_prefix]) | ||
3044 | 211 | result = self.cursor.fetchall() | 213 | result = self.cursor.fetchall() |
3045 | 212 | 214 | ||
3046 | 213 | if not result: | 215 | if not result: |
3048 | 214 | return None | 216 | return {} |
3049 | 215 | if not strip: | 217 | if not strip: |
3050 | 216 | key_prefix = '' | 218 | key_prefix = '' |
3051 | 217 | return dict([ | 219 | return dict([ |
3052 | 218 | (k[len(key_prefix):], json.loads(v)) for k, v in result]) | 220 | (k[len(key_prefix):], json.loads(v)) for k, v in result]) |
3053 | 219 | 221 | ||
3054 | 220 | def update(self, mapping, prefix=""): | 222 | def update(self, mapping, prefix=""): |
3055 | 223 | """ | ||
3056 | 224 | Set the values of multiple keys at once. | ||
3057 | 225 | |||
3058 | 226 | :param dict mapping: Mapping of keys to values | ||
3059 | 227 | :param str prefix: Optional prefix to apply to all keys in `mapping` | ||
3060 | 228 | before setting | ||
3061 | 229 | """ | ||
3062 | 221 | for k, v in mapping.items(): | 230 | for k, v in mapping.items(): |
3063 | 222 | self.set("%s%s" % (prefix, k), v) | 231 | self.set("%s%s" % (prefix, k), v) |
3064 | 223 | 232 | ||
3065 | 224 | def unset(self, key): | 233 | def unset(self, key): |
3066 | 234 | """ | ||
3067 | 235 | Remove a key from the database entirely. | ||
3068 | 236 | """ | ||
3069 | 225 | self.cursor.execute('delete from kv where key=?', [key]) | 237 | self.cursor.execute('delete from kv where key=?', [key]) |
3070 | 226 | if self.revision and self.cursor.rowcount: | 238 | if self.revision and self.cursor.rowcount: |
3071 | 227 | self.cursor.execute( | 239 | self.cursor.execute( |
3072 | 228 | 'insert into kv_revisions values (?, ?, ?)', | 240 | 'insert into kv_revisions values (?, ?, ?)', |
3073 | 229 | [key, self.revision, json.dumps('DELETED')]) | 241 | [key, self.revision, json.dumps('DELETED')]) |
3074 | 230 | 242 | ||
3075 | 243 | def unsetrange(self, keys=None, prefix=""): | ||
3076 | 244 | """ | ||
3077 | 245 | Remove a range of keys starting with a common prefix, from the database | ||
3078 | 246 | entirely. | ||
3079 | 247 | |||
3080 | 248 | :param list keys: List of keys to remove. | ||
3081 | 249 | :param str prefix: Optional prefix to apply to all keys in ``keys`` | ||
3082 | 250 | before removing. | ||
3083 | 251 | """ | ||
3084 | 252 | if keys is not None: | ||
3085 | 253 | keys = ['%s%s' % (prefix, key) for key in keys] | ||
3086 | 254 | self.cursor.execute('delete from kv where key in (%s)' % ','.join(['?'] * len(keys)), keys) | ||
3087 | 255 | if self.revision and self.cursor.rowcount: | ||
3088 | 256 | self.cursor.execute( | ||
3089 | 257 | 'insert into kv_revisions values %s' % ','.join(['(?, ?, ?)'] * len(keys)), | ||
3090 | 258 | list(itertools.chain.from_iterable((key, self.revision, json.dumps('DELETED')) for key in keys))) | ||
3091 | 259 | else: | ||
3092 | 260 | self.cursor.execute('delete from kv where key like ?', | ||
3093 | 261 | ['%s%%' % prefix]) | ||
3094 | 262 | if self.revision and self.cursor.rowcount: | ||
3095 | 263 | self.cursor.execute( | ||
3096 | 264 | 'insert into kv_revisions values (?, ?, ?)', | ||
3097 | 265 | ['%s%%' % prefix, self.revision, json.dumps('DELETED')]) | ||
3098 | 266 | |||
3099 | 231 | def set(self, key, value): | 267 | def set(self, key, value): |
3100 | 268 | """ | ||
3101 | 269 | Set a value in the database. | ||
3102 | 270 | |||
3103 | 271 | :param str key: Key to set the value for | ||
3104 | 272 | :param value: Any JSON-serializable value to be set | ||
3105 | 273 | """ | ||
3106 | 232 | serialized = json.dumps(value) | 274 | serialized = json.dumps(value) |
3107 | 233 | 275 | ||
3110 | 234 | self.cursor.execute( | 276 | self.cursor.execute('select data from kv where key=?', [key]) |
3109 | 235 | 'select data from kv where key=?', [key]) | ||
3111 | 236 | exists = self.cursor.fetchone() | 277 | exists = self.cursor.fetchone() |
3112 | 237 | 278 | ||
3113 | 238 | # Skip mutations to the same value | 279 | # Skip mutations to the same value |
3114 | 239 | 280 | ||
3115 | === modified file 'hooks/charmhelpers/fetch/__init__.py' | |||
3116 | --- hooks/charmhelpers/fetch/__init__.py 2015-04-08 05:57:06 +0000 | |||
3117 | +++ hooks/charmhelpers/fetch/__init__.py 2017-07-18 06:08:45 +0000 | |||
3118 | @@ -1,32 +1,24 @@ | |||
3119 | 1 | # Copyright 2014-2015 Canonical Limited. | 1 | # Copyright 2014-2015 Canonical Limited. |
3120 | 2 | # | 2 | # |
3134 | 3 | # This file is part of charm-helpers. | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
3135 | 4 | # | 4 | # you may not use this file except in compliance with the License. |
3136 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | 5 | # You may obtain a copy of the License at |
3137 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | 6 | # |
3138 | 7 | # published by the Free Software Foundation. | 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
3139 | 8 | # | 8 | # |
3140 | 9 | # charm-helpers is distributed in the hope that it will be useful, | 9 | # Unless required by applicable law or agreed to in writing, software |
3141 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
3142 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
3143 | 12 | # GNU Lesser General Public License for more details. | 12 | # See the License for the specific language governing permissions and |
3144 | 13 | # | 13 | # limitations under the License. |
3132 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
3133 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
3145 | 16 | 14 | ||
3146 | 17 | import importlib | 15 | import importlib |
3149 | 18 | from tempfile import NamedTemporaryFile | 16 | from charmhelpers.osplatform import get_platform |
3148 | 19 | import time | ||
3150 | 20 | from yaml import safe_load | 17 | from yaml import safe_load |
3151 | 21 | from charmhelpers.core.host import ( | ||
3152 | 22 | lsb_release | ||
3153 | 23 | ) | ||
3154 | 24 | import subprocess | ||
3155 | 25 | from charmhelpers.core.hookenv import ( | 18 | from charmhelpers.core.hookenv import ( |
3156 | 26 | config, | 19 | config, |
3157 | 27 | log, | 20 | log, |
3158 | 28 | ) | 21 | ) |
3159 | 29 | import os | ||
3160 | 30 | 22 | ||
3161 | 31 | import six | 23 | import six |
3162 | 32 | if six.PY3: | 24 | if six.PY3: |
3163 | @@ -35,63 +27,6 @@ | |||
3164 | 35 | from urlparse import urlparse, urlunparse | 27 | from urlparse import urlparse, urlunparse |
3165 | 36 | 28 | ||
3166 | 37 | 29 | ||
3167 | 38 | CLOUD_ARCHIVE = """# Ubuntu Cloud Archive | ||
3168 | 39 | deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main | ||
3169 | 40 | """ | ||
3170 | 41 | PROPOSED_POCKET = """# Proposed | ||
3171 | 42 | deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted | ||
3172 | 43 | """ | ||
3173 | 44 | CLOUD_ARCHIVE_POCKETS = { | ||
3174 | 45 | # Folsom | ||
3175 | 46 | 'folsom': 'precise-updates/folsom', | ||
3176 | 47 | 'precise-folsom': 'precise-updates/folsom', | ||
3177 | 48 | 'precise-folsom/updates': 'precise-updates/folsom', | ||
3178 | 49 | 'precise-updates/folsom': 'precise-updates/folsom', | ||
3179 | 50 | 'folsom/proposed': 'precise-proposed/folsom', | ||
3180 | 51 | 'precise-folsom/proposed': 'precise-proposed/folsom', | ||
3181 | 52 | 'precise-proposed/folsom': 'precise-proposed/folsom', | ||
3182 | 53 | # Grizzly | ||
3183 | 54 | 'grizzly': 'precise-updates/grizzly', | ||
3184 | 55 | 'precise-grizzly': 'precise-updates/grizzly', | ||
3185 | 56 | 'precise-grizzly/updates': 'precise-updates/grizzly', | ||
3186 | 57 | 'precise-updates/grizzly': 'precise-updates/grizzly', | ||
3187 | 58 | 'grizzly/proposed': 'precise-proposed/grizzly', | ||
3188 | 59 | 'precise-grizzly/proposed': 'precise-proposed/grizzly', | ||
3189 | 60 | 'precise-proposed/grizzly': 'precise-proposed/grizzly', | ||
3190 | 61 | # Havana | ||
3191 | 62 | 'havana': 'precise-updates/havana', | ||
3192 | 63 | 'precise-havana': 'precise-updates/havana', | ||
3193 | 64 | 'precise-havana/updates': 'precise-updates/havana', | ||
3194 | 65 | 'precise-updates/havana': 'precise-updates/havana', | ||
3195 | 66 | 'havana/proposed': 'precise-proposed/havana', | ||
3196 | 67 | 'precise-havana/proposed': 'precise-proposed/havana', | ||
3197 | 68 | 'precise-proposed/havana': 'precise-proposed/havana', | ||
3198 | 69 | # Icehouse | ||
3199 | 70 | 'icehouse': 'precise-updates/icehouse', | ||
3200 | 71 | 'precise-icehouse': 'precise-updates/icehouse', | ||
3201 | 72 | 'precise-icehouse/updates': 'precise-updates/icehouse', | ||
3202 | 73 | 'precise-updates/icehouse': 'precise-updates/icehouse', | ||
3203 | 74 | 'icehouse/proposed': 'precise-proposed/icehouse', | ||
3204 | 75 | 'precise-icehouse/proposed': 'precise-proposed/icehouse', | ||
3205 | 76 | 'precise-proposed/icehouse': 'precise-proposed/icehouse', | ||
3206 | 77 | # Juno | ||
3207 | 78 | 'juno': 'trusty-updates/juno', | ||
3208 | 79 | 'trusty-juno': 'trusty-updates/juno', | ||
3209 | 80 | 'trusty-juno/updates': 'trusty-updates/juno', | ||
3210 | 81 | 'trusty-updates/juno': 'trusty-updates/juno', | ||
3211 | 82 | 'juno/proposed': 'trusty-proposed/juno', | ||
3212 | 83 | 'trusty-juno/proposed': 'trusty-proposed/juno', | ||
3213 | 84 | 'trusty-proposed/juno': 'trusty-proposed/juno', | ||
3214 | 85 | # Kilo | ||
3215 | 86 | 'kilo': 'trusty-updates/kilo', | ||
3216 | 87 | 'trusty-kilo': 'trusty-updates/kilo', | ||
3217 | 88 | 'trusty-kilo/updates': 'trusty-updates/kilo', | ||
3218 | 89 | 'trusty-updates/kilo': 'trusty-updates/kilo', | ||
3219 | 90 | 'kilo/proposed': 'trusty-proposed/kilo', | ||
3220 | 91 | 'trusty-kilo/proposed': 'trusty-proposed/kilo', | ||
3221 | 92 | 'trusty-proposed/kilo': 'trusty-proposed/kilo', | ||
3222 | 93 | } | ||
3223 | 94 | |||
3224 | 95 | # The order of this list is very important. Handlers should be listed in from | 30 | # The order of this list is very important. Handlers should be listed in from |
3225 | 96 | # least- to most-specific URL matching. | 31 | # least- to most-specific URL matching. |
3226 | 97 | FETCH_HANDLERS = ( | 32 | FETCH_HANDLERS = ( |
3227 | @@ -100,10 +35,6 @@ | |||
3228 | 100 | 'charmhelpers.fetch.giturl.GitUrlFetchHandler', | 35 | 'charmhelpers.fetch.giturl.GitUrlFetchHandler', |
3229 | 101 | ) | 36 | ) |
3230 | 102 | 37 | ||
3231 | 103 | APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT. | ||
3232 | 104 | APT_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks. | ||
3233 | 105 | APT_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times. | ||
3234 | 106 | |||
3235 | 107 | 38 | ||
3236 | 108 | class SourceConfigError(Exception): | 39 | class SourceConfigError(Exception): |
3237 | 109 | pass | 40 | pass |
3238 | @@ -117,6 +48,13 @@ | |||
3239 | 117 | pass | 48 | pass |
3240 | 118 | 49 | ||
3241 | 119 | 50 | ||
3242 | 51 | class GPGKeyError(Exception): | ||
3243 | 52 | """Exception occurs when a GPG key cannot be fetched or used. The message | ||
3244 | 53 | indicates what the problem is. | ||
3245 | 54 | """ | ||
3246 | 55 | pass | ||
3247 | 56 | |||
3248 | 57 | |||
3249 | 120 | class BaseFetchHandler(object): | 58 | class BaseFetchHandler(object): |
3250 | 121 | 59 | ||
3251 | 122 | """Base class for FetchHandler implementations in fetch plugins""" | 60 | """Base class for FetchHandler implementations in fetch plugins""" |
3252 | @@ -141,172 +79,39 @@ | |||
3253 | 141 | return urlunparse(parts) | 79 | return urlunparse(parts) |
3254 | 142 | 80 | ||
3255 | 143 | 81 | ||
3412 | 144 | def filter_installed_packages(packages): | 82 | __platform__ = get_platform() |
3413 | 145 | """Returns a list of packages that require installation""" | 83 | module = "charmhelpers.fetch.%s" % __platform__ |
3414 | 146 | cache = apt_cache() | 84 | fetch = importlib.import_module(module) |
3415 | 147 | _pkgs = [] | 85 | |
3416 | 148 | for package in packages: | 86 | filter_installed_packages = fetch.filter_installed_packages |
3417 | 149 | try: | 87 | install = fetch.apt_install |
3418 | 150 | p = cache[package] | 88 | upgrade = fetch.apt_upgrade |
3419 | 151 | p.current_ver or _pkgs.append(package) | 89 | update = _fetch_update = fetch.apt_update |
3420 | 152 | except KeyError: | 90 | purge = fetch.apt_purge |
3421 | 153 | log('Package {} has no installation candidate.'.format(package), | 91 | add_source = fetch.add_source |
3422 | 154 | level='WARNING') | 92 | |
3423 | 155 | _pkgs.append(package) | 93 | if __platform__ == "ubuntu": |
3424 | 156 | return _pkgs | 94 | apt_cache = fetch.apt_cache |
3425 | 157 | 95 | apt_install = fetch.apt_install | |
3426 | 158 | 96 | apt_update = fetch.apt_update | |
3427 | 159 | def apt_cache(in_memory=True): | 97 | apt_upgrade = fetch.apt_upgrade |
3428 | 160 | """Build and return an apt cache""" | 98 | apt_purge = fetch.apt_purge |
3429 | 161 | import apt_pkg | 99 | apt_mark = fetch.apt_mark |
3430 | 162 | apt_pkg.init() | 100 | apt_hold = fetch.apt_hold |
3431 | 163 | if in_memory: | 101 | apt_unhold = fetch.apt_unhold |
3432 | 164 | apt_pkg.config.set("Dir::Cache::pkgcache", "") | 102 | import_key = fetch.import_key |
3433 | 165 | apt_pkg.config.set("Dir::Cache::srcpkgcache", "") | 103 | get_upstream_version = fetch.get_upstream_version |
3434 | 166 | return apt_pkg.Cache() | 104 | elif __platform__ == "centos": |
3435 | 167 | 105 | yum_search = fetch.yum_search | |
3280 | 168 | |||
3281 | 169 | def apt_install(packages, options=None, fatal=False): | ||
3282 | 170 | """Install one or more packages""" | ||
3283 | 171 | if options is None: | ||
3284 | 172 | options = ['--option=Dpkg::Options::=--force-confold'] | ||
3285 | 173 | |||
3286 | 174 | cmd = ['apt-get', '--assume-yes'] | ||
3287 | 175 | cmd.extend(options) | ||
3288 | 176 | cmd.append('install') | ||
3289 | 177 | if isinstance(packages, six.string_types): | ||
3290 | 178 | cmd.append(packages) | ||
3291 | 179 | else: | ||
3292 | 180 | cmd.extend(packages) | ||
3293 | 181 | log("Installing {} with options: {}".format(packages, | ||
3294 | 182 | options)) | ||
3295 | 183 | _run_apt_command(cmd, fatal) | ||
3296 | 184 | |||
3297 | 185 | |||
3298 | 186 | def apt_upgrade(options=None, fatal=False, dist=False): | ||
3299 | 187 | """Upgrade all packages""" | ||
3300 | 188 | if options is None: | ||
3301 | 189 | options = ['--option=Dpkg::Options::=--force-confold'] | ||
3302 | 190 | |||
3303 | 191 | cmd = ['apt-get', '--assume-yes'] | ||
3304 | 192 | cmd.extend(options) | ||
3305 | 193 | if dist: | ||
3306 | 194 | cmd.append('dist-upgrade') | ||
3307 | 195 | else: | ||
3308 | 196 | cmd.append('upgrade') | ||
3309 | 197 | log("Upgrading with options: {}".format(options)) | ||
3310 | 198 | _run_apt_command(cmd, fatal) | ||
3311 | 199 | |||
3312 | 200 | |||
3313 | 201 | def apt_update(fatal=False): | ||
3314 | 202 | """Update local apt cache""" | ||
3315 | 203 | cmd = ['apt-get', 'update'] | ||
3316 | 204 | _run_apt_command(cmd, fatal) | ||
3317 | 205 | |||
3318 | 206 | |||
3319 | 207 | def apt_purge(packages, fatal=False): | ||
3320 | 208 | """Purge one or more packages""" | ||
3321 | 209 | cmd = ['apt-get', '--assume-yes', 'purge'] | ||
3322 | 210 | if isinstance(packages, six.string_types): | ||
3323 | 211 | cmd.append(packages) | ||
3324 | 212 | else: | ||
3325 | 213 | cmd.extend(packages) | ||
3326 | 214 | log("Purging {}".format(packages)) | ||
3327 | 215 | _run_apt_command(cmd, fatal) | ||
3328 | 216 | |||
3329 | 217 | |||
3330 | 218 | def apt_hold(packages, fatal=False): | ||
3331 | 219 | """Hold one or more packages""" | ||
3332 | 220 | cmd = ['apt-mark', 'hold'] | ||
3333 | 221 | if isinstance(packages, six.string_types): | ||
3334 | 222 | cmd.append(packages) | ||
3335 | 223 | else: | ||
3336 | 224 | cmd.extend(packages) | ||
3337 | 225 | log("Holding {}".format(packages)) | ||
3338 | 226 | |||
3339 | 227 | if fatal: | ||
3340 | 228 | subprocess.check_call(cmd) | ||
3341 | 229 | else: | ||
3342 | 230 | subprocess.call(cmd) | ||
3343 | 231 | |||
3344 | 232 | |||
3345 | 233 | def add_source(source, key=None): | ||
3346 | 234 | """Add a package source to this system. | ||
3347 | 235 | |||
3348 | 236 | @param source: a URL or sources.list entry, as supported by | ||
3349 | 237 | add-apt-repository(1). Examples:: | ||
3350 | 238 | |||
3351 | 239 | ppa:charmers/example | ||
3352 | 240 | deb https://stub:key@private.example.com/ubuntu trusty main | ||
3353 | 241 | |||
3354 | 242 | In addition: | ||
3355 | 243 | 'proposed:' may be used to enable the standard 'proposed' | ||
3356 | 244 | pocket for the release. | ||
3357 | 245 | 'cloud:' may be used to activate official cloud archive pockets, | ||
3358 | 246 | such as 'cloud:icehouse' | ||
3359 | 247 | 'distro' may be used as a noop | ||
3360 | 248 | |||
3361 | 249 | @param key: A key to be added to the system's APT keyring and used | ||
3362 | 250 | to verify the signatures on packages. Ideally, this should be an | ||
3363 | 251 | ASCII format GPG public key including the block headers. A GPG key | ||
3364 | 252 | id may also be used, but be aware that only insecure protocols are | ||
3365 | 253 | available to retrieve the actual public key from a public keyserver | ||
3366 | 254 | placing your Juju environment at risk. ppa and cloud archive keys | ||
3367 | 255 | are securely added automtically, so sould not be provided. | ||
3368 | 256 | """ | ||
3369 | 257 | if source is None: | ||
3370 | 258 | log('Source is not present. Skipping') | ||
3371 | 259 | return | ||
3372 | 260 | |||
3373 | 261 | if (source.startswith('ppa:') or | ||
3374 | 262 | source.startswith('http') or | ||
3375 | 263 | source.startswith('deb ') or | ||
3376 | 264 | source.startswith('cloud-archive:')): | ||
3377 | 265 | subprocess.check_call(['add-apt-repository', '--yes', source]) | ||
3378 | 266 | elif source.startswith('cloud:'): | ||
3379 | 267 | apt_install(filter_installed_packages(['ubuntu-cloud-keyring']), | ||
3380 | 268 | fatal=True) | ||
3381 | 269 | pocket = source.split(':')[-1] | ||
3382 | 270 | if pocket not in CLOUD_ARCHIVE_POCKETS: | ||
3383 | 271 | raise SourceConfigError( | ||
3384 | 272 | 'Unsupported cloud: source option %s' % | ||
3385 | 273 | pocket) | ||
3386 | 274 | actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket] | ||
3387 | 275 | with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt: | ||
3388 | 276 | apt.write(CLOUD_ARCHIVE.format(actual_pocket)) | ||
3389 | 277 | elif source == 'proposed': | ||
3390 | 278 | release = lsb_release()['DISTRIB_CODENAME'] | ||
3391 | 279 | with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt: | ||
3392 | 280 | apt.write(PROPOSED_POCKET.format(release)) | ||
3393 | 281 | elif source == 'distro': | ||
3394 | 282 | pass | ||
3395 | 283 | else: | ||
3396 | 284 | log("Unknown source: {!r}".format(source)) | ||
3397 | 285 | |||
3398 | 286 | if key: | ||
3399 | 287 | if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key: | ||
3400 | 288 | with NamedTemporaryFile('w+') as key_file: | ||
3401 | 289 | key_file.write(key) | ||
3402 | 290 | key_file.flush() | ||
3403 | 291 | key_file.seek(0) | ||
3404 | 292 | subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file) | ||
3405 | 293 | else: | ||
3406 | 294 | # Note that hkp: is in no way a secure protocol. Using a | ||
3407 | 295 | # GPG key id is pointless from a security POV unless you | ||
3408 | 296 | # absolutely trust your network and DNS. | ||
3409 | 297 | subprocess.check_call(['apt-key', 'adv', '--keyserver', | ||
3410 | 298 | 'hkp://keyserver.ubuntu.com:80', '--recv', | ||
3411 | 299 | key]) | ||
3436 | 300 | 106 | ||
3437 | 301 | 107 | ||
3438 | 302 | def configure_sources(update=False, | 108 | def configure_sources(update=False, |
3439 | 303 | sources_var='install_sources', | 109 | sources_var='install_sources', |
3440 | 304 | keys_var='install_keys'): | 110 | keys_var='install_keys'): |
3443 | 305 | """ | 111 | """Configure multiple sources from charm configuration. |
3442 | 306 | Configure multiple sources from charm configuration. | ||
3444 | 307 | 112 | ||
3445 | 308 | The lists are encoded as yaml fragments in the configuration. | 113 | The lists are encoded as yaml fragments in the configuration. |
3447 | 309 | The frament needs to be included as a string. Sources and their | 114 | The fragment needs to be included as a string. Sources and their |
3448 | 310 | corresponding keys are of the types supported by add_source(). | 115 | corresponding keys are of the types supported by add_source(). |
3449 | 311 | 116 | ||
3450 | 312 | Example config: | 117 | Example config: |
3451 | @@ -338,12 +143,11 @@ | |||
3452 | 338 | for source, key in zip(sources, keys): | 143 | for source, key in zip(sources, keys): |
3453 | 339 | add_source(source, key) | 144 | add_source(source, key) |
3454 | 340 | if update: | 145 | if update: |
3456 | 341 | apt_update(fatal=True) | 146 | _fetch_update(fatal=True) |
3457 | 342 | 147 | ||
3458 | 343 | 148 | ||
3459 | 344 | def install_remote(source, *args, **kwargs): | 149 | def install_remote(source, *args, **kwargs): |
3462 | 345 | """ | 150 | """Install a file tree from a remote source. |
3461 | 346 | Install a file tree from a remote source | ||
3463 | 347 | 151 | ||
3464 | 348 | The specified source should be a url of the form: | 152 | The specified source should be a url of the form: |
3465 | 349 | scheme://[host]/path[#[option=value][&...]] | 153 | scheme://[host]/path[#[option=value][&...]] |
3466 | @@ -366,18 +170,17 @@ | |||
3467 | 366 | # We ONLY check for True here because can_handle may return a string | 170 | # We ONLY check for True here because can_handle may return a string |
3468 | 367 | # explaining why it can't handle a given source. | 171 | # explaining why it can't handle a given source. |
3469 | 368 | handlers = [h for h in plugins() if h.can_handle(source) is True] | 172 | handlers = [h for h in plugins() if h.can_handle(source) is True] |
3470 | 369 | installed_to = None | ||
3471 | 370 | for handler in handlers: | 173 | for handler in handlers: |
3472 | 371 | try: | 174 | try: |
3479 | 372 | installed_to = handler.install(source, *args, **kwargs) | 175 | return handler.install(source, *args, **kwargs) |
3480 | 373 | except UnhandledSource: | 176 | except UnhandledSource as e: |
3481 | 374 | pass | 177 | log('Install source attempt unsuccessful: {}'.format(e), |
3482 | 375 | if not installed_to: | 178 | level='WARNING') |
3483 | 376 | raise UnhandledSource("No handler found for source {}".format(source)) | 179 | raise UnhandledSource("No handler found for source {}".format(source)) |
3478 | 377 | return installed_to | ||
3484 | 378 | 180 | ||
3485 | 379 | 181 | ||
3486 | 380 | def install_from_config(config_var_name): | 182 | def install_from_config(config_var_name): |
3487 | 183 | """Install a file from config.""" | ||
3488 | 381 | charm_config = config() | 184 | charm_config = config() |
3489 | 382 | source = charm_config[config_var_name] | 185 | source = charm_config[config_var_name] |
3490 | 383 | return install_remote(source) | 186 | return install_remote(source) |
3491 | @@ -394,46 +197,9 @@ | |||
3492 | 394 | importlib.import_module(package), | 197 | importlib.import_module(package), |
3493 | 395 | classname) | 198 | classname) |
3494 | 396 | plugin_list.append(handler_class()) | 199 | plugin_list.append(handler_class()) |
3496 | 397 | except (ImportError, AttributeError): | 200 | except NotImplementedError: |
3497 | 398 | # Skip missing plugins so that they can be ommitted from | 201 | # Skip missing plugins so that they can be ommitted from |
3498 | 399 | # installation if desired | 202 | # installation if desired |
3499 | 400 | log("FetchHandler {} not found, skipping plugin".format( | 203 | log("FetchHandler {} not found, skipping plugin".format( |
3500 | 401 | handler_name)) | 204 | handler_name)) |
3501 | 402 | return plugin_list | 205 | return plugin_list |
3502 | 403 | |||
3503 | 404 | |||
3504 | 405 | def _run_apt_command(cmd, fatal=False): | ||
3505 | 406 | """ | ||
3506 | 407 | Run an APT command, checking output and retrying if the fatal flag is set | ||
3507 | 408 | to True. | ||
3508 | 409 | |||
3509 | 410 | :param: cmd: str: The apt command to run. | ||
3510 | 411 | :param: fatal: bool: Whether the command's output should be checked and | ||
3511 | 412 | retried. | ||
3512 | 413 | """ | ||
3513 | 414 | env = os.environ.copy() | ||
3514 | 415 | |||
3515 | 416 | if 'DEBIAN_FRONTEND' not in env: | ||
3516 | 417 | env['DEBIAN_FRONTEND'] = 'noninteractive' | ||
3517 | 418 | |||
3518 | 419 | if fatal: | ||
3519 | 420 | retry_count = 0 | ||
3520 | 421 | result = None | ||
3521 | 422 | |||
3522 | 423 | # If the command is considered "fatal", we need to retry if the apt | ||
3523 | 424 | # lock was not acquired. | ||
3524 | 425 | |||
3525 | 426 | while result is None or result == APT_NO_LOCK: | ||
3526 | 427 | try: | ||
3527 | 428 | result = subprocess.check_call(cmd, env=env) | ||
3528 | 429 | except subprocess.CalledProcessError as e: | ||
3529 | 430 | retry_count = retry_count + 1 | ||
3530 | 431 | if retry_count > APT_NO_LOCK_RETRY_COUNT: | ||
3531 | 432 | raise | ||
3532 | 433 | result = e.returncode | ||
3533 | 434 | log("Couldn't acquire DPKG lock. Will retry in {} seconds." | ||
3534 | 435 | "".format(APT_NO_LOCK_RETRY_DELAY)) | ||
3535 | 436 | time.sleep(APT_NO_LOCK_RETRY_DELAY) | ||
3536 | 437 | |||
3537 | 438 | else: | ||
3538 | 439 | subprocess.call(cmd, env=env) | ||
3539 | 440 | 206 | ||
3540 | === modified file 'hooks/charmhelpers/fetch/archiveurl.py' | |||
3541 | --- hooks/charmhelpers/fetch/archiveurl.py 2015-04-08 05:57:06 +0000 | |||
3542 | +++ hooks/charmhelpers/fetch/archiveurl.py 2017-07-18 06:08:45 +0000 | |||
3543 | @@ -1,18 +1,16 @@ | |||
3544 | 1 | # Copyright 2014-2015 Canonical Limited. | 1 | # Copyright 2014-2015 Canonical Limited. |
3545 | 2 | # | 2 | # |
3559 | 3 | # This file is part of charm-helpers. | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
3560 | 4 | # | 4 | # you may not use this file except in compliance with the License. |
3561 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | 5 | # You may obtain a copy of the License at |
3562 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | 6 | # |
3563 | 7 | # published by the Free Software Foundation. | 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
3564 | 8 | # | 8 | # |
3565 | 9 | # charm-helpers is distributed in the hope that it will be useful, | 9 | # Unless required by applicable law or agreed to in writing, software |
3566 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
3567 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
3568 | 12 | # GNU Lesser General Public License for more details. | 12 | # See the License for the specific language governing permissions and |
3569 | 13 | # | 13 | # limitations under the License. |
3557 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
3558 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
3570 | 16 | 14 | ||
3571 | 17 | import os | 15 | import os |
3572 | 18 | import hashlib | 16 | import hashlib |
3573 | @@ -77,6 +75,8 @@ | |||
3574 | 77 | def can_handle(self, source): | 75 | def can_handle(self, source): |
3575 | 78 | url_parts = self.parse_url(source) | 76 | url_parts = self.parse_url(source) |
3576 | 79 | if url_parts.scheme not in ('http', 'https', 'ftp', 'file'): | 77 | if url_parts.scheme not in ('http', 'https', 'ftp', 'file'): |
3577 | 78 | # XXX: Why is this returning a boolean and a string? It's | ||
3578 | 79 | # doomed to fail since "bool(can_handle('foo://'))" will be True. | ||
3579 | 80 | return "Wrong source type" | 80 | return "Wrong source type" |
3580 | 81 | if get_archive_handler(self.base_url(source)): | 81 | if get_archive_handler(self.base_url(source)): |
3581 | 82 | return True | 82 | return True |
3582 | @@ -106,7 +106,7 @@ | |||
3583 | 106 | install_opener(opener) | 106 | install_opener(opener) |
3584 | 107 | response = urlopen(source) | 107 | response = urlopen(source) |
3585 | 108 | try: | 108 | try: |
3587 | 109 | with open(dest, 'w') as dest_file: | 109 | with open(dest, 'wb') as dest_file: |
3588 | 110 | dest_file.write(response.read()) | 110 | dest_file.write(response.read()) |
3589 | 111 | except Exception as e: | 111 | except Exception as e: |
3590 | 112 | if os.path.isfile(dest): | 112 | if os.path.isfile(dest): |
3591 | @@ -155,7 +155,11 @@ | |||
3592 | 155 | else: | 155 | else: |
3593 | 156 | algorithms = hashlib.algorithms_available | 156 | algorithms = hashlib.algorithms_available |
3594 | 157 | if key in algorithms: | 157 | if key in algorithms: |
3596 | 158 | check_hash(dld_file, value, key) | 158 | if len(value) != 1: |
3597 | 159 | raise TypeError( | ||
3598 | 160 | "Expected 1 hash value, not %d" % len(value)) | ||
3599 | 161 | expected = value[0] | ||
3600 | 162 | check_hash(dld_file, expected, key) | ||
3601 | 159 | if checksum: | 163 | if checksum: |
3602 | 160 | check_hash(dld_file, checksum, hash_type) | 164 | check_hash(dld_file, checksum, hash_type) |
3603 | 161 | return extract(dld_file, dest) | 165 | return extract(dld_file, dest) |
3604 | 162 | 166 | ||
3605 | === modified file 'hooks/charmhelpers/fetch/bzrurl.py' | |||
3606 | --- hooks/charmhelpers/fetch/bzrurl.py 2015-04-08 05:57:06 +0000 | |||
3607 | +++ hooks/charmhelpers/fetch/bzrurl.py 2017-07-18 06:08:45 +0000 | |||
3608 | @@ -1,78 +1,76 @@ | |||
3609 | 1 | # Copyright 2014-2015 Canonical Limited. | 1 | # Copyright 2014-2015 Canonical Limited. |
3610 | 2 | # | 2 | # |
3624 | 3 | # This file is part of charm-helpers. | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
3625 | 4 | # | 4 | # you may not use this file except in compliance with the License. |
3626 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | 5 | # You may obtain a copy of the License at |
3627 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | 6 | # |
3628 | 7 | # published by the Free Software Foundation. | 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
3629 | 8 | # | 8 | # |
3630 | 9 | # charm-helpers is distributed in the hope that it will be useful, | 9 | # Unless required by applicable law or agreed to in writing, software |
3631 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
3632 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
3633 | 12 | # GNU Lesser General Public License for more details. | 12 | # See the License for the specific language governing permissions and |
3634 | 13 | # | 13 | # limitations under the License. |
3622 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
3623 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
3635 | 16 | 14 | ||
3636 | 17 | import os | 15 | import os |
3637 | 16 | from subprocess import check_call | ||
3638 | 18 | from charmhelpers.fetch import ( | 17 | from charmhelpers.fetch import ( |
3639 | 19 | BaseFetchHandler, | 18 | BaseFetchHandler, |
3641 | 20 | UnhandledSource | 19 | UnhandledSource, |
3642 | 20 | filter_installed_packages, | ||
3643 | 21 | install, | ||
3644 | 21 | ) | 22 | ) |
3645 | 22 | from charmhelpers.core.host import mkdir | 23 | from charmhelpers.core.host import mkdir |
3646 | 23 | 24 | ||
3647 | 24 | import six | ||
3648 | 25 | if six.PY3: | ||
3649 | 26 | raise ImportError('bzrlib does not support Python3') | ||
3650 | 27 | 25 | ||
3659 | 28 | try: | 26 | if filter_installed_packages(['bzr']) != []: |
3660 | 29 | from bzrlib.branch import Branch | 27 | install(['bzr']) |
3661 | 30 | from bzrlib import bzrdir, workingtree, errors | 28 | if filter_installed_packages(['bzr']) != []: |
3662 | 31 | except ImportError: | 29 | raise NotImplementedError('Unable to install bzr') |
3655 | 32 | from charmhelpers.fetch import apt_install | ||
3656 | 33 | apt_install("python-bzrlib") | ||
3657 | 34 | from bzrlib.branch import Branch | ||
3658 | 35 | from bzrlib import bzrdir, workingtree, errors | ||
3663 | 36 | 30 | ||
3664 | 37 | 31 | ||
3665 | 38 | class BzrUrlFetchHandler(BaseFetchHandler): | 32 | class BzrUrlFetchHandler(BaseFetchHandler): |
3667 | 39 | """Handler for bazaar branches via generic and lp URLs""" | 33 | """Handler for bazaar branches via generic and lp URLs.""" |
3668 | 34 | |||
3669 | 40 | def can_handle(self, source): | 35 | def can_handle(self, source): |
3670 | 41 | url_parts = self.parse_url(source) | 36 | url_parts = self.parse_url(source) |
3672 | 42 | if url_parts.scheme not in ('bzr+ssh', 'lp'): | 37 | if url_parts.scheme not in ('bzr+ssh', 'lp', ''): |
3673 | 43 | return False | 38 | return False |
3674 | 39 | elif not url_parts.scheme: | ||
3675 | 40 | return os.path.exists(os.path.join(source, '.bzr')) | ||
3676 | 44 | else: | 41 | else: |
3677 | 45 | return True | 42 | return True |
3678 | 46 | 43 | ||
3682 | 47 | def branch(self, source, dest): | 44 | def branch(self, source, dest, revno=None): |
3680 | 48 | url_parts = self.parse_url(source) | ||
3681 | 49 | # If we use lp:branchname scheme we need to load plugins | ||
3683 | 50 | if not self.can_handle(source): | 45 | if not self.can_handle(source): |
3684 | 51 | raise UnhandledSource("Cannot handle {}".format(source)) | 46 | raise UnhandledSource("Cannot handle {}".format(source)) |
3699 | 52 | if url_parts.scheme == "lp": | 47 | cmd_opts = [] |
3700 | 53 | from bzrlib.plugin import load_plugins | 48 | if revno: |
3701 | 54 | load_plugins() | 49 | cmd_opts += ['-r', str(revno)] |
3702 | 55 | try: | 50 | if os.path.exists(dest): |
3703 | 56 | local_branch = bzrdir.BzrDir.create_branch_convenience(dest) | 51 | cmd = ['bzr', 'pull'] |
3704 | 57 | except errors.AlreadyControlDirError: | 52 | cmd += cmd_opts |
3705 | 58 | local_branch = Branch.open(dest) | 53 | cmd += ['--overwrite', '-d', dest, source] |
3706 | 59 | try: | 54 | else: |
3707 | 60 | remote_branch = Branch.open(source) | 55 | cmd = ['bzr', 'branch'] |
3708 | 61 | remote_branch.push(local_branch) | 56 | cmd += cmd_opts |
3709 | 62 | tree = workingtree.WorkingTree.open(dest) | 57 | cmd += [source, dest] |
3710 | 63 | tree.update() | 58 | check_call(cmd) |
3697 | 64 | except Exception as e: | ||
3698 | 65 | raise e | ||
3711 | 66 | 59 | ||
3713 | 67 | def install(self, source): | 60 | def install(self, source, dest=None, revno=None): |
3714 | 68 | url_parts = self.parse_url(source) | 61 | url_parts = self.parse_url(source) |
3715 | 69 | branch_name = url_parts.path.strip("/").split("/")[-1] | 62 | branch_name = url_parts.path.strip("/").split("/")[-1] |
3720 | 70 | dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched", | 63 | if dest: |
3721 | 71 | branch_name) | 64 | dest_dir = os.path.join(dest, branch_name) |
3722 | 72 | if not os.path.exists(dest_dir): | 65 | else: |
3723 | 73 | mkdir(dest_dir, perms=0o755) | 66 | dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched", |
3724 | 67 | branch_name) | ||
3725 | 68 | |||
3726 | 69 | if dest and not os.path.exists(dest): | ||
3727 | 70 | mkdir(dest, perms=0o755) | ||
3728 | 71 | |||
3729 | 74 | try: | 72 | try: |
3731 | 75 | self.branch(source, dest_dir) | 73 | self.branch(source, dest_dir, revno) |
3732 | 76 | except OSError as e: | 74 | except OSError as e: |
3733 | 77 | raise UnhandledSource(e.strerror) | 75 | raise UnhandledSource(e.strerror) |
3734 | 78 | return dest_dir | 76 | return dest_dir |
3735 | 79 | 77 | ||
3736 | === added file 'hooks/charmhelpers/fetch/centos.py' | |||
3737 | --- hooks/charmhelpers/fetch/centos.py 1970-01-01 00:00:00 +0000 | |||
3738 | +++ hooks/charmhelpers/fetch/centos.py 2017-07-18 06:08:45 +0000 | |||
3739 | @@ -0,0 +1,171 @@ | |||
3740 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
3741 | 2 | # | ||
3742 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
3743 | 4 | # you may not use this file except in compliance with the License. | ||
3744 | 5 | # You may obtain a copy of the License at | ||
3745 | 6 | # | ||
3746 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
3747 | 8 | # | ||
3748 | 9 | # Unless required by applicable law or agreed to in writing, software | ||
3749 | 10 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
3750 | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
3751 | 12 | # See the License for the specific language governing permissions and | ||
3752 | 13 | # limitations under the License. | ||
3753 | 14 | |||
3754 | 15 | import subprocess | ||
3755 | 16 | import os | ||
3756 | 17 | import time | ||
3757 | 18 | import six | ||
3758 | 19 | import yum | ||
3759 | 20 | |||
3760 | 21 | from tempfile import NamedTemporaryFile | ||
3761 | 22 | from charmhelpers.core.hookenv import log | ||
3762 | 23 | |||
3763 | 24 | YUM_NO_LOCK = 1 # The return code for "couldn't acquire lock" in YUM. | ||
3764 | 25 | YUM_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks. | ||
3765 | 26 | YUM_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times. | ||
3766 | 27 | |||
3767 | 28 | |||
3768 | 29 | def filter_installed_packages(packages): | ||
3769 | 30 | """Return a list of packages that require installation.""" | ||
3770 | 31 | yb = yum.YumBase() | ||
3771 | 32 | package_list = yb.doPackageLists() | ||
3772 | 33 | temp_cache = {p.base_package_name: 1 for p in package_list['installed']} | ||
3773 | 34 | |||
3774 | 35 | _pkgs = [p for p in packages if not temp_cache.get(p, False)] | ||
3775 | 36 | return _pkgs | ||
3776 | 37 | |||
3777 | 38 | |||
3778 | 39 | def install(packages, options=None, fatal=False): | ||
3779 | 40 | """Install one or more packages.""" | ||
3780 | 41 | cmd = ['yum', '--assumeyes'] | ||
3781 | 42 | if options is not None: | ||
3782 | 43 | cmd.extend(options) | ||
3783 | 44 | cmd.append('install') | ||
3784 | 45 | if isinstance(packages, six.string_types): | ||
3785 | 46 | cmd.append(packages) | ||
3786 | 47 | else: | ||
3787 | 48 | cmd.extend(packages) | ||
3788 | 49 | log("Installing {} with options: {}".format(packages, | ||
3789 | 50 | options)) | ||
3790 | 51 | _run_yum_command(cmd, fatal) | ||
3791 | 52 | |||
3792 | 53 | |||
3793 | 54 | def upgrade(options=None, fatal=False, dist=False): | ||
3794 | 55 | """Upgrade all packages.""" | ||
3795 | 56 | cmd = ['yum', '--assumeyes'] | ||
3796 | 57 | if options is not None: | ||
3797 | 58 | cmd.extend(options) | ||
3798 | 59 | cmd.append('upgrade') | ||
3799 | 60 | log("Upgrading with options: {}".format(options)) | ||
3800 | 61 | _run_yum_command(cmd, fatal) | ||
3801 | 62 | |||
3802 | 63 | |||
3803 | 64 | def update(fatal=False): | ||
3804 | 65 | """Update local yum cache.""" | ||
3805 | 66 | cmd = ['yum', '--assumeyes', 'update'] | ||
3806 | 67 | log("Update with fatal: {}".format(fatal)) | ||
3807 | 68 | _run_yum_command(cmd, fatal) | ||
3808 | 69 | |||
3809 | 70 | |||
3810 | 71 | def purge(packages, fatal=False): | ||
3811 | 72 | """Purge one or more packages.""" | ||
3812 | 73 | cmd = ['yum', '--assumeyes', 'remove'] | ||
3813 | 74 | if isinstance(packages, six.string_types): | ||
3814 | 75 | cmd.append(packages) | ||
3815 | 76 | else: | ||
3816 | 77 | cmd.extend(packages) | ||
3817 | 78 | log("Purging {}".format(packages)) | ||
3818 | 79 | _run_yum_command(cmd, fatal) | ||
3819 | 80 | |||
3820 | 81 | |||
3821 | 82 | def yum_search(packages): | ||
3822 | 83 | """Search for a package.""" | ||
3823 | 84 | output = {} | ||
3824 | 85 | cmd = ['yum', 'search'] | ||
3825 | 86 | if isinstance(packages, six.string_types): | ||
3826 | 87 | cmd.append(packages) | ||
3827 | 88 | else: | ||
3828 | 89 | cmd.extend(packages) | ||
3829 | 90 | log("Searching for {}".format(packages)) | ||
3830 | 91 | result = subprocess.check_output(cmd) | ||
3831 | 92 | for package in list(packages): | ||
3832 | 93 | output[package] = package in result | ||
3833 | 94 | return output | ||
3834 | 95 | |||
3835 | 96 | |||
3836 | 97 | def add_source(source, key=None): | ||
3837 | 98 | """Add a package source to this system. | ||
3838 | 99 | |||
3839 | 100 | @param source: a URL with a rpm package | ||
3840 | 101 | |||
3841 | 102 | @param key: A key to be added to the system's keyring and used | ||
3842 | 103 | to verify the signatures on packages. Ideally, this should be an | ||
3843 | 104 | ASCII format GPG public key including the block headers. A GPG key | ||
3844 | 105 | id may also be used, but be aware that only insecure protocols are | ||
3845 | 106 | available to retrieve the actual public key from a public keyserver | ||
3846 | 107 | placing your Juju environment at risk. | ||
3847 | 108 | """ | ||
3848 | 109 | if source is None: | ||
3849 | 110 | log('Source is not present. Skipping') | ||
3850 | 111 | return | ||
3851 | 112 | |||
3852 | 113 | if source.startswith('http'): | ||
3853 | 114 | directory = '/etc/yum.repos.d/' | ||
3854 | 115 | for filename in os.listdir(directory): | ||
3855 | 116 | with open(directory + filename, 'r') as rpm_file: | ||
3856 | 117 | if source in rpm_file.read(): | ||
3857 | 118 | break | ||
3858 | 119 | else: | ||
3859 | 120 | log("Add source: {!r}".format(source)) | ||
3860 | 121 | # write in the charms.repo | ||
3861 | 122 | with open(directory + 'Charms.repo', 'a') as rpm_file: | ||
3862 | 123 | rpm_file.write('[%s]\n' % source[7:].replace('/', '_')) | ||
3863 | 124 | rpm_file.write('name=%s\n' % source[7:]) | ||
3864 | 125 | rpm_file.write('baseurl=%s\n\n' % source) | ||
3865 | 126 | else: | ||
3866 | 127 | log("Unknown source: {!r}".format(source)) | ||
3867 | 128 | |||
3868 | 129 | if key: | ||
3869 | 130 | if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key: | ||
3870 | 131 | with NamedTemporaryFile('w+') as key_file: | ||
3871 | 132 | key_file.write(key) | ||
3872 | 133 | key_file.flush() | ||
3873 | 134 | key_file.seek(0) | ||
3874 | 135 | subprocess.check_call(['rpm', '--import', key_file.name]) | ||
3875 | 136 | else: | ||
3876 | 137 | subprocess.check_call(['rpm', '--import', key]) | ||
3877 | 138 | |||
3878 | 139 | |||
3879 | 140 | def _run_yum_command(cmd, fatal=False): | ||
3880 | 141 | """Run an YUM command. | ||
3881 | 142 | |||
3882 | 143 | Checks the output and retry if the fatal flag is set to True. | ||
3883 | 144 | |||
3884 | 145 | :param: cmd: str: The yum command to run. | ||
3885 | 146 | :param: fatal: bool: Whether the command's output should be checked and | ||
3886 | 147 | retried. | ||
3887 | 148 | """ | ||
3888 | 149 | env = os.environ.copy() | ||
3889 | 150 | |||
3890 | 151 | if fatal: | ||
3891 | 152 | retry_count = 0 | ||
3892 | 153 | result = None | ||
3893 | 154 | |||
3894 | 155 | # If the command is considered "fatal", we need to retry if the yum | ||
3895 | 156 | # lock was not acquired. | ||
3896 | 157 | |||
3897 | 158 | while result is None or result == YUM_NO_LOCK: | ||
3898 | 159 | try: | ||
3899 | 160 | result = subprocess.check_call(cmd, env=env) | ||
3900 | 161 | except subprocess.CalledProcessError as e: | ||
3901 | 162 | retry_count = retry_count + 1 | ||
3902 | 163 | if retry_count > YUM_NO_LOCK_RETRY_COUNT: | ||
3903 | 164 | raise | ||
3904 | 165 | result = e.returncode | ||
3905 | 166 | log("Couldn't acquire YUM lock. Will retry in {} seconds." | ||
3906 | 167 | "".format(YUM_NO_LOCK_RETRY_DELAY)) | ||
3907 | 168 | time.sleep(YUM_NO_LOCK_RETRY_DELAY) | ||
3908 | 169 | |||
3909 | 170 | else: | ||
3910 | 171 | subprocess.call(cmd, env=env) | ||
3911 | 0 | 172 | ||
3912 | === modified file 'hooks/charmhelpers/fetch/giturl.py' | |||
3913 | --- hooks/charmhelpers/fetch/giturl.py 2015-04-08 05:57:06 +0000 | |||
3914 | +++ hooks/charmhelpers/fetch/giturl.py 2017-07-18 06:08:45 +0000 | |||
3915 | @@ -1,58 +1,58 @@ | |||
3916 | 1 | # Copyright 2014-2015 Canonical Limited. | 1 | # Copyright 2014-2015 Canonical Limited. |
3917 | 2 | # | 2 | # |
3931 | 3 | # This file is part of charm-helpers. | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
3932 | 4 | # | 4 | # you may not use this file except in compliance with the License. |
3933 | 5 | # charm-helpers is free software: you can redistribute it and/or modify | 5 | # You may obtain a copy of the License at |
3934 | 6 | # it under the terms of the GNU Lesser General Public License version 3 as | 6 | # |
3935 | 7 | # published by the Free Software Foundation. | 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
3936 | 8 | # | 8 | # |
3937 | 9 | # charm-helpers is distributed in the hope that it will be useful, | 9 | # Unless required by applicable law or agreed to in writing, software |
3938 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
3939 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
3940 | 12 | # GNU Lesser General Public License for more details. | 12 | # See the License for the specific language governing permissions and |
3941 | 13 | # | 13 | # limitations under the License. |
3929 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
3930 | 15 | # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. | ||
3942 | 16 | 14 | ||
3943 | 17 | import os | 15 | import os |
3944 | 16 | from subprocess import check_call, CalledProcessError | ||
3945 | 18 | from charmhelpers.fetch import ( | 17 | from charmhelpers.fetch import ( |
3946 | 19 | BaseFetchHandler, | 18 | BaseFetchHandler, |
3948 | 20 | UnhandledSource | 19 | UnhandledSource, |
3949 | 20 | filter_installed_packages, | ||
3950 | 21 | install, | ||
3951 | 21 | ) | 22 | ) |
3966 | 22 | from charmhelpers.core.host import mkdir | 23 | |
3967 | 23 | 24 | if filter_installed_packages(['git']) != []: | |
3968 | 24 | import six | 25 | install(['git']) |
3969 | 25 | if six.PY3: | 26 | if filter_installed_packages(['git']) != []: |
3970 | 26 | raise ImportError('GitPython does not support Python 3') | 27 | raise NotImplementedError('Unable to install git') |
3957 | 27 | |||
3958 | 28 | try: | ||
3959 | 29 | from git import Repo | ||
3960 | 30 | except ImportError: | ||
3961 | 31 | from charmhelpers.fetch import apt_install | ||
3962 | 32 | apt_install("python-git") | ||
3963 | 33 | from git import Repo | ||
3964 | 34 | |||
3965 | 35 | from git.exc import GitCommandError # noqa E402 | ||
3971 | 36 | 28 | ||
3972 | 37 | 29 | ||
3973 | 38 | class GitUrlFetchHandler(BaseFetchHandler): | 30 | class GitUrlFetchHandler(BaseFetchHandler): |
3975 | 39 | """Handler for git branches via generic and github URLs""" | 31 | """Handler for git branches via generic and github URLs.""" |
3976 | 32 | |||
3977 | 40 | def can_handle(self, source): | 33 | def can_handle(self, source): |
3978 | 41 | url_parts = self.parse_url(source) | 34 | url_parts = self.parse_url(source) |
3979 | 42 | # TODO (mattyw) no support for ssh git@ yet | 35 | # TODO (mattyw) no support for ssh git@ yet |
3981 | 43 | if url_parts.scheme not in ('http', 'https', 'git'): | 36 | if url_parts.scheme not in ('http', 'https', 'git', ''): |
3982 | 44 | return False | 37 | return False |
3983 | 38 | elif not url_parts.scheme: | ||
3984 | 39 | return os.path.exists(os.path.join(source, '.git')) | ||
3985 | 45 | else: | 40 | else: |
3986 | 46 | return True | 41 | return True |
3987 | 47 | 42 | ||
3989 | 48 | def clone(self, source, dest, branch): | 43 | def clone(self, source, dest, branch="master", depth=None): |
3990 | 49 | if not self.can_handle(source): | 44 | if not self.can_handle(source): |
3991 | 50 | raise UnhandledSource("Cannot handle {}".format(source)) | 45 | raise UnhandledSource("Cannot handle {}".format(source)) |
3992 | 51 | 46 | ||
3995 | 52 | repo = Repo.clone_from(source, dest) | 47 | if os.path.exists(dest): |
3996 | 53 | repo.git.checkout(branch) | 48 | cmd = ['git', '-C', dest, 'pull', source, branch] |
3997 | 49 | else: | ||
3998 | 50 | cmd = ['git', 'clone', source, dest, '--branch', branch] | ||
3999 | 51 | if depth: | ||
4000 | 52 | cmd.extend(['--depth', depth]) | ||
4001 | 53 | check_call(cmd) | ||
4002 | 54 | 54 | ||
4004 | 55 | def install(self, source, branch="master", dest=None): | 55 | def install(self, source, branch="master", dest=None, depth=None): |
4005 | 56 | url_parts = self.parse_url(source) | 56 | url_parts = self.parse_url(source) |
4006 | 57 | branch_name = url_parts.path.strip("/").split("/")[-1] | 57 | branch_name = url_parts.path.strip("/").split("/")[-1] |
4007 | 58 | if dest: | 58 | if dest: |
4008 | @@ -60,12 +60,10 @@ | |||
4009 | 60 | else: | 60 | else: |
4010 | 61 | dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched", | 61 | dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched", |
4011 | 62 | branch_name) | 62 | branch_name) |
4012 | 63 | if not os.path.exists(dest_dir): | ||
4013 | 64 | mkdir(dest_dir, perms=0o755) | ||
4014 | 65 | try: | 63 | try: |
4018 | 66 | self.clone(source, dest_dir, branch) | 64 | self.clone(source, dest_dir, branch, depth) |
4019 | 67 | except GitCommandError as e: | 65 | except CalledProcessError as e: |
4020 | 68 | raise UnhandledSource(e.message) | 66 | raise UnhandledSource(e) |
4021 | 69 | except OSError as e: | 67 | except OSError as e: |
4022 | 70 | raise UnhandledSource(e.strerror) | 68 | raise UnhandledSource(e.strerror) |
4023 | 71 | return dest_dir | 69 | return dest_dir |
4024 | 72 | 70 | ||
4025 | === added file 'hooks/charmhelpers/fetch/snap.py' | |||
4026 | --- hooks/charmhelpers/fetch/snap.py 1970-01-01 00:00:00 +0000 | |||
4027 | +++ hooks/charmhelpers/fetch/snap.py 2017-07-18 06:08:45 +0000 | |||
4028 | @@ -0,0 +1,122 @@ | |||
4029 | 1 | # Copyright 2014-2017 Canonical Limited. | ||
4030 | 2 | # | ||
4031 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
4032 | 4 | # you may not use this file except in compliance with the License. | ||
4033 | 5 | # You may obtain a copy of the License at | ||
4034 | 6 | # | ||
4035 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
4036 | 8 | # | ||
4037 | 9 | # Unless required by applicable law or agreed to in writing, software | ||
4038 | 10 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
4039 | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
4040 | 12 | # See the License for the specific language governing permissions and | ||
4041 | 13 | # limitations under the License. | ||
4042 | 14 | """ | ||
4043 | 15 | Charm helpers snap for classic charms. | ||
4044 | 16 | |||
4045 | 17 | If writing reactive charms, use the snap layer: | ||
4046 | 18 | https://lists.ubuntu.com/archives/snapcraft/2016-September/001114.html | ||
4047 | 19 | """ | ||
4048 | 20 | import subprocess | ||
4049 | 21 | from os import environ | ||
4050 | 22 | from time import sleep | ||
4051 | 23 | from charmhelpers.core.hookenv import log | ||
4052 | 24 | |||
4053 | 25 | __author__ = 'Joseph Borg <joseph.borg@canonical.com>' | ||
4054 | 26 | |||
4055 | 27 | SNAP_NO_LOCK = 1 # The return code for "couldn't acquire lock" in Snap (hopefully this will be improved). | ||
4056 | 28 | SNAP_NO_LOCK_RETRY_DELAY = 10 # Wait X seconds between Snap lock checks. | ||
4057 | 29 | SNAP_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times. | ||
4058 | 30 | |||
4059 | 31 | |||
4060 | 32 | class CouldNotAcquireLockException(Exception): | ||
4061 | 33 | pass | ||
4062 | 34 | |||
4063 | 35 | |||
4064 | 36 | def _snap_exec(commands): | ||
4065 | 37 | """ | ||
4066 | 38 | Execute snap commands. | ||
4067 | 39 | |||
4068 | 40 | :param commands: List commands | ||
4069 | 41 | :return: Integer exit code | ||
4070 | 42 | """ | ||
4071 | 43 | assert type(commands) == list | ||
4072 | 44 | |||
4073 | 45 | retry_count = 0 | ||
4074 | 46 | return_code = None | ||
4075 | 47 | |||
4076 | 48 | while return_code is None or return_code == SNAP_NO_LOCK: | ||
4077 | 49 | try: | ||
4078 | 50 | return_code = subprocess.check_call(['snap'] + commands, env=environ) | ||
4079 | 51 | except subprocess.CalledProcessError as e: | ||
4080 | 52 | retry_count += + 1 | ||
4081 | 53 | if retry_count > SNAP_NO_LOCK_RETRY_COUNT: | ||
4082 | 54 | raise CouldNotAcquireLockException('Could not aquire lock after %s attempts' % SNAP_NO_LOCK_RETRY_COUNT) | ||
4083 | 55 | return_code = e.returncode | ||
4084 | 56 | log('Snap failed to acquire lock, trying again in %s seconds.' % SNAP_NO_LOCK_RETRY_DELAY, level='WARN') | ||
4085 | 57 | sleep(SNAP_NO_LOCK_RETRY_DELAY) | ||
4086 | 58 | |||
4087 | 59 | return return_code | ||
4088 | 60 | |||
4089 | 61 | |||
4090 | 62 | def snap_install(packages, *flags): | ||
4091 | 63 | """ | ||
4092 | 64 | Install a snap package. | ||
4093 | 65 | |||
4094 | 66 | :param packages: String or List String package name | ||
4095 | 67 | :param flags: List String flags to pass to install command | ||
4096 | 68 | :return: Integer return code from snap | ||
4097 | 69 | """ | ||
4098 | 70 | if type(packages) is not list: | ||
4099 | 71 | packages = [packages] | ||
4100 | 72 | |||
4101 | 73 | flags = list(flags) | ||
4102 | 74 | |||
4103 | 75 | message = 'Installing snap(s) "%s"' % ', '.join(packages) | ||
4104 | 76 | if flags: | ||
4105 | 77 | message += ' with option(s) "%s"' % ', '.join(flags) | ||
4106 | 78 | |||
4107 | 79 | log(message, level='INFO') | ||
4108 | 80 | return _snap_exec(['install'] + flags + packages) | ||
4109 | 81 | |||
4110 | 82 | |||
4111 | 83 | def snap_remove(packages, *flags): | ||
4112 | 84 | """ | ||
4113 | 85 | Remove a snap package. | ||
4114 | 86 | |||
4115 | 87 | :param packages: String or List String package name | ||
4116 | 88 | :param flags: List String flags to pass to remove command | ||
4117 | 89 | :return: Integer return code from snap | ||
4118 | 90 | """ | ||
4119 | 91 | if type(packages) is not list: | ||
4120 | 92 | packages = [packages] | ||
4121 | 93 | |||
4122 | 94 | flags = list(flags) | ||
4123 | 95 | |||
4124 | 96 | message = 'Removing snap(s) "%s"' % ', '.join(packages) | ||
4125 | 97 | if flags: | ||
4126 | 98 | message += ' with options "%s"' % ', '.join(flags) | ||
4127 | 99 | |||
4128 | 100 | log(message, level='INFO') | ||
4129 | 101 | return _snap_exec(['remove'] + flags + packages) | ||
4130 | 102 | |||
4131 | 103 | |||
4132 | 104 | def snap_refresh(packages, *flags): | ||
4133 | 105 | """ | ||
4134 | 106 | Refresh / Update snap package. | ||
4135 | 107 | |||
4136 | 108 | :param packages: String or List String package name | ||
4137 | 109 | :param flags: List String flags to pass to refresh command | ||
4138 | 110 | :return: Integer return code from snap | ||
4139 | 111 | """ | ||
4140 | 112 | if type(packages) is not list: | ||
4141 | 113 | packages = [packages] | ||
4142 | 114 | |||
4143 | 115 | flags = list(flags) | ||
4144 | 116 | |||
4145 | 117 | message = 'Refreshing snap(s) "%s"' % ', '.join(packages) | ||
4146 | 118 | if flags: | ||
4147 | 119 | message += ' with options "%s"' % ', '.join(flags) | ||
4148 | 120 | |||
4149 | 121 | log(message, level='INFO') | ||
4150 | 122 | return _snap_exec(['refresh'] + flags + packages) | ||
4151 | 0 | 123 | ||
4152 | === added file 'hooks/charmhelpers/fetch/ubuntu.py' | |||
4153 | --- hooks/charmhelpers/fetch/ubuntu.py 1970-01-01 00:00:00 +0000 | |||
4154 | +++ hooks/charmhelpers/fetch/ubuntu.py 2017-07-18 06:08:45 +0000 | |||
4155 | @@ -0,0 +1,568 @@ | |||
4156 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
4157 | 2 | # | ||
4158 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
4159 | 4 | # you may not use this file except in compliance with the License. | ||
4160 | 5 | # You may obtain a copy of the License at | ||
4161 | 6 | # | ||
4162 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
4163 | 8 | # | ||
4164 | 9 | # Unless required by applicable law or agreed to in writing, software | ||
4165 | 10 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
4166 | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
4167 | 12 | # See the License for the specific language governing permissions and | ||
4168 | 13 | # limitations under the License. | ||
4169 | 14 | |||
4170 | 15 | from collections import OrderedDict | ||
4171 | 16 | import os | ||
4172 | 17 | import platform | ||
4173 | 18 | import re | ||
4174 | 19 | import six | ||
4175 | 20 | import time | ||
4176 | 21 | import subprocess | ||
4177 | 22 | from tempfile import NamedTemporaryFile | ||
4178 | 23 | |||
4179 | 24 | from charmhelpers.core.host import ( | ||
4180 | 25 | lsb_release | ||
4181 | 26 | ) | ||
4182 | 27 | from charmhelpers.core.hookenv import ( | ||
4183 | 28 | log, | ||
4184 | 29 | DEBUG, | ||
4185 | 30 | ) | ||
4186 | 31 | from charmhelpers.fetch import SourceConfigError, GPGKeyError | ||
4187 | 32 | |||
4188 | 33 | PROPOSED_POCKET = ( | ||
4189 | 34 | "# Proposed\n" | ||
4190 | 35 | "deb http://archive.ubuntu.com/ubuntu {}-proposed main universe " | ||
4191 | 36 | "multiverse restricted\n") | ||
4192 | 37 | PROPOSED_PORTS_POCKET = ( | ||
4193 | 38 | "# Proposed\n" | ||
4194 | 39 | "deb http://ports.ubuntu.com/ubuntu-ports {}-proposed main universe " | ||
4195 | 40 | "multiverse restricted\n") | ||
4196 | 41 | # Only supports 64bit and ppc64 at the moment. | ||
4197 | 42 | ARCH_TO_PROPOSED_POCKET = { | ||
4198 | 43 | 'x86_64': PROPOSED_POCKET, | ||
4199 | 44 | 'ppc64le': PROPOSED_PORTS_POCKET, | ||
4200 | 45 | 'aarch64': PROPOSED_PORTS_POCKET, | ||
4201 | 46 | } | ||
4202 | 47 | CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu" | ||
4203 | 48 | CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA' | ||
4204 | 49 | CLOUD_ARCHIVE = """# Ubuntu Cloud Archive | ||
4205 | 50 | deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main | ||
4206 | 51 | """ | ||
4207 | 52 | CLOUD_ARCHIVE_POCKETS = { | ||
4208 | 53 | # Folsom | ||
4209 | 54 | 'folsom': 'precise-updates/folsom', | ||
4210 | 55 | 'folsom/updates': 'precise-updates/folsom', | ||
4211 | 56 | 'precise-folsom': 'precise-updates/folsom', | ||
4212 | 57 | 'precise-folsom/updates': 'precise-updates/folsom', | ||
4213 | 58 | 'precise-updates/folsom': 'precise-updates/folsom', | ||
4214 | 59 | 'folsom/proposed': 'precise-proposed/folsom', | ||
4215 | 60 | 'precise-folsom/proposed': 'precise-proposed/folsom', | ||
4216 | 61 | 'precise-proposed/folsom': 'precise-proposed/folsom', | ||
4217 | 62 | # Grizzly | ||
4218 | 63 | 'grizzly': 'precise-updates/grizzly', | ||
4219 | 64 | 'grizzly/updates': 'precise-updates/grizzly', | ||
4220 | 65 | 'precise-grizzly': 'precise-updates/grizzly', | ||
4221 | 66 | 'precise-grizzly/updates': 'precise-updates/grizzly', | ||
4222 | 67 | 'precise-updates/grizzly': 'precise-updates/grizzly', | ||
4223 | 68 | 'grizzly/proposed': 'precise-proposed/grizzly', | ||
4224 | 69 | 'precise-grizzly/proposed': 'precise-proposed/grizzly', | ||
4225 | 70 | 'precise-proposed/grizzly': 'precise-proposed/grizzly', | ||
4226 | 71 | # Havana | ||
4227 | 72 | 'havana': 'precise-updates/havana', | ||
4228 | 73 | 'havana/updates': 'precise-updates/havana', | ||
4229 | 74 | 'precise-havana': 'precise-updates/havana', | ||
4230 | 75 | 'precise-havana/updates': 'precise-updates/havana', | ||
4231 | 76 | 'precise-updates/havana': 'precise-updates/havana', | ||
4232 | 77 | 'havana/proposed': 'precise-proposed/havana', | ||
4233 | 78 | 'precise-havana/proposed': 'precise-proposed/havana', | ||
4234 | 79 | 'precise-proposed/havana': 'precise-proposed/havana', | ||
4235 | 80 | # Icehouse | ||
4236 | 81 | 'icehouse': 'precise-updates/icehouse', | ||
4237 | 82 | 'icehouse/updates': 'precise-updates/icehouse', | ||
4238 | 83 | 'precise-icehouse': 'precise-updates/icehouse', | ||
4239 | 84 | 'precise-icehouse/updates': 'precise-updates/icehouse', | ||
4240 | 85 | 'precise-updates/icehouse': 'precise-updates/icehouse', | ||
4241 | 86 | 'icehouse/proposed': 'precise-proposed/icehouse', | ||
4242 | 87 | 'precise-icehouse/proposed': 'precise-proposed/icehouse', | ||
4243 | 88 | 'precise-proposed/icehouse': 'precise-proposed/icehouse', | ||
4244 | 89 | # Juno | ||
4245 | 90 | 'juno': 'trusty-updates/juno', | ||
4246 | 91 | 'juno/updates': 'trusty-updates/juno', | ||
4247 | 92 | 'trusty-juno': 'trusty-updates/juno', | ||
4248 | 93 | 'trusty-juno/updates': 'trusty-updates/juno', | ||
4249 | 94 | 'trusty-updates/juno': 'trusty-updates/juno', | ||
4250 | 95 | 'juno/proposed': 'trusty-proposed/juno', | ||
4251 | 96 | 'trusty-juno/proposed': 'trusty-proposed/juno', | ||
4252 | 97 | 'trusty-proposed/juno': 'trusty-proposed/juno', | ||
4253 | 98 | # Kilo | ||
4254 | 99 | 'kilo': 'trusty-updates/kilo', | ||
4255 | 100 | 'kilo/updates': 'trusty-updates/kilo', | ||
4256 | 101 | 'trusty-kilo': 'trusty-updates/kilo', | ||
4257 | 102 | 'trusty-kilo/updates': 'trusty-updates/kilo', | ||
4258 | 103 | 'trusty-updates/kilo': 'trusty-updates/kilo', | ||
4259 | 104 | 'kilo/proposed': 'trusty-proposed/kilo', | ||
4260 | 105 | 'trusty-kilo/proposed': 'trusty-proposed/kilo', | ||
4261 | 106 | 'trusty-proposed/kilo': 'trusty-proposed/kilo', | ||
4262 | 107 | # Liberty | ||
4263 | 108 | 'liberty': 'trusty-updates/liberty', | ||
4264 | 109 | 'liberty/updates': 'trusty-updates/liberty', | ||
4265 | 110 | 'trusty-liberty': 'trusty-updates/liberty', | ||
4266 | 111 | 'trusty-liberty/updates': 'trusty-updates/liberty', | ||
4267 | 112 | 'trusty-updates/liberty': 'trusty-updates/liberty', | ||
4268 | 113 | 'liberty/proposed': 'trusty-proposed/liberty', | ||
4269 | 114 | 'trusty-liberty/proposed': 'trusty-proposed/liberty', | ||
4270 | 115 | 'trusty-proposed/liberty': 'trusty-proposed/liberty', | ||
4271 | 116 | # Mitaka | ||
4272 | 117 | 'mitaka': 'trusty-updates/mitaka', | ||
4273 | 118 | 'mitaka/updates': 'trusty-updates/mitaka', | ||
4274 | 119 | 'trusty-mitaka': 'trusty-updates/mitaka', | ||
4275 | 120 | 'trusty-mitaka/updates': 'trusty-updates/mitaka', | ||
4276 | 121 | 'trusty-updates/mitaka': 'trusty-updates/mitaka', | ||
4277 | 122 | 'mitaka/proposed': 'trusty-proposed/mitaka', | ||
4278 | 123 | 'trusty-mitaka/proposed': 'trusty-proposed/mitaka', | ||
4279 | 124 | 'trusty-proposed/mitaka': 'trusty-proposed/mitaka', | ||
4280 | 125 | # Newton | ||
4281 | 126 | 'newton': 'xenial-updates/newton', | ||
4282 | 127 | 'newton/updates': 'xenial-updates/newton', | ||
4283 | 128 | 'xenial-newton': 'xenial-updates/newton', | ||
4284 | 129 | 'xenial-newton/updates': 'xenial-updates/newton', | ||
4285 | 130 | 'xenial-updates/newton': 'xenial-updates/newton', | ||
4286 | 131 | 'newton/proposed': 'xenial-proposed/newton', | ||
4287 | 132 | 'xenial-newton/proposed': 'xenial-proposed/newton', | ||
4288 | 133 | 'xenial-proposed/newton': 'xenial-proposed/newton', | ||
4289 | 134 | # Ocata | ||
4290 | 135 | 'ocata': 'xenial-updates/ocata', | ||
4291 | 136 | 'ocata/updates': 'xenial-updates/ocata', | ||
4292 | 137 | 'xenial-ocata': 'xenial-updates/ocata', | ||
4293 | 138 | 'xenial-ocata/updates': 'xenial-updates/ocata', | ||
4294 | 139 | 'xenial-updates/ocata': 'xenial-updates/ocata', | ||
4295 | 140 | 'ocata/proposed': 'xenial-proposed/ocata', | ||
4296 | 141 | 'xenial-ocata/proposed': 'xenial-proposed/ocata', | ||
4297 | 142 | 'xenial-ocata/newton': 'xenial-proposed/ocata', | ||
4298 | 143 | # Pike | ||
4299 | 144 | 'pike': 'xenial-updates/pike', | ||
4300 | 145 | 'xenial-pike': 'xenial-updates/pike', | ||
4301 | 146 | 'xenial-pike/updates': 'xenial-updates/pike', | ||
4302 | 147 | 'xenial-updates/pike': 'xenial-updates/pike', | ||
4303 | 148 | 'pike/proposed': 'xenial-proposed/pike', | ||
4304 | 149 | 'xenial-pike/proposed': 'xenial-proposed/pike', | ||
4305 | 150 | 'xenial-pike/newton': 'xenial-proposed/pike', | ||
4306 | 151 | # Queens | ||
4307 | 152 | 'queens': 'xenial-updates/queens', | ||
4308 | 153 | 'xenial-queens': 'xenial-updates/queens', | ||
4309 | 154 | 'xenial-queens/updates': 'xenial-updates/queens', | ||
4310 | 155 | 'xenial-updates/queens': 'xenial-updates/queens', | ||
4311 | 156 | 'queens/proposed': 'xenial-proposed/queens', | ||
4312 | 157 | 'xenial-queens/proposed': 'xenial-proposed/queens', | ||
4313 | 158 | 'xenial-queens/newton': 'xenial-proposed/queens', | ||
4314 | 159 | } | ||
4315 | 160 | |||
4316 | 161 | |||
4317 | 162 | APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT. | ||
4318 | 163 | CMD_RETRY_DELAY = 10 # Wait 10 seconds between command retries. | ||
4319 | 164 | CMD_RETRY_COUNT = 30 # Retry a failing fatal command X times. | ||
4320 | 165 | |||
4321 | 166 | |||
4322 | 167 | def filter_installed_packages(packages): | ||
4323 | 168 | """Return a list of packages that require installation.""" | ||
4324 | 169 | cache = apt_cache() | ||
4325 | 170 | _pkgs = [] | ||
4326 | 171 | for package in packages: | ||
4327 | 172 | try: | ||
4328 | 173 | p = cache[package] | ||
4329 | 174 | p.current_ver or _pkgs.append(package) | ||
4330 | 175 | except KeyError: | ||
4331 | 176 | log('Package {} has no installation candidate.'.format(package), | ||
4332 | 177 | level='WARNING') | ||
4333 | 178 | _pkgs.append(package) | ||
4334 | 179 | return _pkgs | ||
4335 | 180 | |||
4336 | 181 | |||
4337 | 182 | def apt_cache(in_memory=True, progress=None): | ||
4338 | 183 | """Build and return an apt cache.""" | ||
4339 | 184 | from apt import apt_pkg | ||
4340 | 185 | apt_pkg.init() | ||
4341 | 186 | if in_memory: | ||
4342 | 187 | apt_pkg.config.set("Dir::Cache::pkgcache", "") | ||
4343 | 188 | apt_pkg.config.set("Dir::Cache::srcpkgcache", "") | ||
4344 | 189 | return apt_pkg.Cache(progress) | ||
4345 | 190 | |||
4346 | 191 | |||
4347 | 192 | def apt_install(packages, options=None, fatal=False): | ||
4348 | 193 | """Install one or more packages.""" | ||
4349 | 194 | if options is None: | ||
4350 | 195 | options = ['--option=Dpkg::Options::=--force-confold'] | ||
4351 | 196 | |||
4352 | 197 | cmd = ['apt-get', '--assume-yes'] | ||
4353 | 198 | cmd.extend(options) | ||
4354 | 199 | cmd.append('install') | ||
4355 | 200 | if isinstance(packages, six.string_types): | ||
4356 | 201 | cmd.append(packages) | ||
4357 | 202 | else: | ||
4358 | 203 | cmd.extend(packages) | ||
4359 | 204 | log("Installing {} with options: {}".format(packages, | ||
4360 | 205 | options)) | ||
4361 | 206 | _run_apt_command(cmd, fatal) | ||
4362 | 207 | |||
4363 | 208 | |||
4364 | 209 | def apt_upgrade(options=None, fatal=False, dist=False): | ||
4365 | 210 | """Upgrade all packages.""" | ||
4366 | 211 | if options is None: | ||
4367 | 212 | options = ['--option=Dpkg::Options::=--force-confold'] | ||
4368 | 213 | |||
4369 | 214 | cmd = ['apt-get', '--assume-yes'] | ||
4370 | 215 | cmd.extend(options) | ||
4371 | 216 | if dist: | ||
4372 | 217 | cmd.append('dist-upgrade') | ||
4373 | 218 | else: | ||
4374 | 219 | cmd.append('upgrade') | ||
4375 | 220 | log("Upgrading with options: {}".format(options)) | ||
4376 | 221 | _run_apt_command(cmd, fatal) | ||
4377 | 222 | |||
4378 | 223 | |||
4379 | 224 | def apt_update(fatal=False): | ||
4380 | 225 | """Update local apt cache.""" | ||
4381 | 226 | cmd = ['apt-get', 'update'] | ||
4382 | 227 | _run_apt_command(cmd, fatal) | ||
4383 | 228 | |||
4384 | 229 | |||
4385 | 230 | def apt_purge(packages, fatal=False): | ||
4386 | 231 | """Purge one or more packages.""" | ||
4387 | 232 | cmd = ['apt-get', '--assume-yes', 'purge'] | ||
4388 | 233 | if isinstance(packages, six.string_types): | ||
4389 | 234 | cmd.append(packages) | ||
4390 | 235 | else: | ||
4391 | 236 | cmd.extend(packages) | ||
4392 | 237 | log("Purging {}".format(packages)) | ||
4393 | 238 | _run_apt_command(cmd, fatal) | ||
4394 | 239 | |||
4395 | 240 | |||
4396 | 241 | def apt_mark(packages, mark, fatal=False): | ||
4397 | 242 | """Flag one or more packages using apt-mark.""" | ||
4398 | 243 | log("Marking {} as {}".format(packages, mark)) | ||
4399 | 244 | cmd = ['apt-mark', mark] | ||
4400 | 245 | if isinstance(packages, six.string_types): | ||
4401 | 246 | cmd.append(packages) | ||
4402 | 247 | else: | ||
4403 | 248 | cmd.extend(packages) | ||
4404 | 249 | |||
4405 | 250 | if fatal: | ||
4406 | 251 | subprocess.check_call(cmd, universal_newlines=True) | ||
4407 | 252 | else: | ||
4408 | 253 | subprocess.call(cmd, universal_newlines=True) | ||
4409 | 254 | |||
4410 | 255 | |||
4411 | 256 | def apt_hold(packages, fatal=False): | ||
4412 | 257 | return apt_mark(packages, 'hold', fatal=fatal) | ||
4413 | 258 | |||
4414 | 259 | |||
4415 | 260 | def apt_unhold(packages, fatal=False): | ||
4416 | 261 | return apt_mark(packages, 'unhold', fatal=fatal) | ||
4417 | 262 | |||
4418 | 263 | |||
4419 | 264 | def import_key(keyid): | ||
4420 | 265 | """Import a key in either ASCII Armor or Radix64 format. | ||
4421 | 266 | |||
4422 | 267 | `keyid` is either the keyid to fetch from a PGP server, or | ||
4423 | 268 | the key in ASCII armor foramt. | ||
4424 | 269 | |||
4425 | 270 | :param keyid: String of key (or key id). | ||
4426 | 271 | :raises: GPGKeyError if the key could not be imported | ||
4427 | 272 | """ | ||
4428 | 273 | key = keyid.strip() | ||
4429 | 274 | if (key.startswith('-----BEGIN PGP PUBLIC KEY BLOCK-----') and | ||
4430 | 275 | key.endswith('-----END PGP PUBLIC KEY BLOCK-----')): | ||
4431 | 276 | log("PGP key found (looks like ASCII Armor format)", level=DEBUG) | ||
4432 | 277 | log("Importing ASCII Armor PGP key", level=DEBUG) | ||
4433 | 278 | with NamedTemporaryFile() as keyfile: | ||
4434 | 279 | with open(keyfile.name, 'w') as fd: | ||
4435 | 280 | fd.write(key) | ||
4436 | 281 | fd.write("\n") | ||
4437 | 282 | cmd = ['apt-key', 'add', keyfile.name] | ||
4438 | 283 | try: | ||
4439 | 284 | subprocess.check_call(cmd) | ||
4440 | 285 | except subprocess.CalledProcessError: | ||
4441 | 286 | error = "Error importing PGP key '{}'".format(key) | ||
4442 | 287 | log(error) | ||
4443 | 288 | raise GPGKeyError(error) | ||
4444 | 289 | else: | ||
4445 | 290 | log("PGP key found (looks like Radix64 format)", level=DEBUG) | ||
4446 | 291 | log("Importing PGP key from keyserver", level=DEBUG) | ||
4447 | 292 | cmd = ['apt-key', 'adv', '--keyserver', | ||
4448 | 293 | 'hkp://keyserver.ubuntu.com:80', '--recv-keys', key] | ||
4449 | 294 | try: | ||
4450 | 295 | subprocess.check_call(cmd) | ||
4451 | 296 | except subprocess.CalledProcessError: | ||
4452 | 297 | error = "Error importing PGP key '{}'".format(key) | ||
4453 | 298 | log(error) | ||
4454 | 299 | raise GPGKeyError(error) | ||
4455 | 300 | |||
4456 | 301 | |||
4457 | 302 | def add_source(source, key=None, fail_invalid=False): | ||
4458 | 303 | """Add a package source to this system. | ||
4459 | 304 | |||
4460 | 305 | @param source: a URL or sources.list entry, as supported by | ||
4461 | 306 | add-apt-repository(1). Examples:: | ||
4462 | 307 | |||
4463 | 308 | ppa:charmers/example | ||
4464 | 309 | deb https://stub:key@private.example.com/ubuntu trusty main | ||
4465 | 310 | |||
4466 | 311 | In addition: | ||
4467 | 312 | 'proposed:' may be used to enable the standard 'proposed' | ||
4468 | 313 | pocket for the release. | ||
4469 | 314 | 'cloud:' may be used to activate official cloud archive pockets, | ||
4470 | 315 | such as 'cloud:icehouse' | ||
4471 | 316 | 'distro' may be used as a noop | ||
4472 | 317 | |||
4473 | 318 | Full list of source specifications supported by the function are: | ||
4474 | 319 | |||
4475 | 320 | 'distro': A NOP; i.e. it has no effect. | ||
4476 | 321 | 'proposed': the proposed deb spec [2] is wrtten to | ||
4477 | 322 | /etc/apt/sources.list/proposed | ||
4478 | 323 | 'distro-proposed': adds <version>-proposed to the debs [2] | ||
4479 | 324 | 'ppa:<ppa-name>': add-apt-repository --yes <ppa_name> | ||
4480 | 325 | 'deb <deb-spec>': add-apt-repository --yes deb <deb-spec> | ||
4481 | 326 | 'http://....': add-apt-repository --yes http://... | ||
4482 | 327 | 'cloud-archive:<spec>': add-apt-repository -yes cloud-archive:<spec> | ||
4483 | 328 | 'cloud:<release>[-staging]': specify a Cloud Archive pocket <release> with | ||
4484 | 329 | optional staging version. If staging is used then the staging PPA [2] | ||
4485 | 330 | with be used. If staging is NOT used then the cloud archive [3] will be | ||
4486 | 331 | added, and the 'ubuntu-cloud-keyring' package will be added for the | ||
4487 | 332 | current distro. | ||
4488 | 333 | |||
4489 | 334 | Otherwise the source is not recognised and this is logged to the juju log. | ||
4490 | 335 | However, no error is raised, unless sys_error_on_exit is True. | ||
4491 | 336 | |||
4492 | 337 | [1] deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main | ||
4493 | 338 | where {} is replaced with the derived pocket name. | ||
4494 | 339 | [2] deb http://archive.ubuntu.com/ubuntu {}-proposed \ | ||
4495 | 340 | main universe multiverse restricted | ||
4496 | 341 | where {} is replaced with the lsb_release codename (e.g. xenial) | ||
4497 | 342 | [3] deb http://ubuntu-cloud.archive.canonical.com/ubuntu <pocket> | ||
4498 | 343 | to /etc/apt/sources.list.d/cloud-archive-list | ||
4499 | 344 | |||
4500 | 345 | @param key: A key to be added to the system's APT keyring and used | ||
4501 | 346 | to verify the signatures on packages. Ideally, this should be an | ||
4502 | 347 | ASCII format GPG public key including the block headers. A GPG key | ||
4503 | 348 | id may also be used, but be aware that only insecure protocols are | ||
4504 | 349 | available to retrieve the actual public key from a public keyserver | ||
4505 | 350 | placing your Juju environment at risk. ppa and cloud archive keys | ||
4506 | 351 | are securely added automtically, so sould not be provided. | ||
4507 | 352 | |||
4508 | 353 | @param fail_invalid: (boolean) if True, then the function raises a | ||
4509 | 354 | SourceConfigError is there is no matching installation source. | ||
4510 | 355 | |||
4511 | 356 | @raises SourceConfigError() if for cloud:<pocket>, the <pocket> is not a | ||
4512 | 357 | valid pocket in CLOUD_ARCHIVE_POCKETS | ||
4513 | 358 | """ | ||
4514 | 359 | _mapping = OrderedDict([ | ||
4515 | 360 | (r"^distro$", lambda: None), # This is a NOP | ||
4516 | 361 | (r"^(?:proposed|distro-proposed)$", _add_proposed), | ||
4517 | 362 | (r"^cloud-archive:(.*)$", _add_apt_repository), | ||
4518 | 363 | (r"^((?:deb |http:|https:|ppa:).*)$", _add_apt_repository), | ||
4519 | 364 | (r"^cloud:(.*)-(.*)\/staging$", _add_cloud_staging), | ||
4520 | 365 | (r"^cloud:(.*)-(.*)$", _add_cloud_distro_check), | ||
4521 | 366 | (r"^cloud:(.*)$", _add_cloud_pocket), | ||
4522 | 367 | ]) | ||
4523 | 368 | if source is None: | ||
4524 | 369 | source = '' | ||
4525 | 370 | for r, fn in six.iteritems(_mapping): | ||
4526 | 371 | m = re.match(r, source) | ||
4527 | 372 | if m: | ||
4528 | 373 | # call the assoicated function with the captured groups | ||
4529 | 374 | # raises SourceConfigError on error. | ||
4530 | 375 | fn(*m.groups()) | ||
4531 | 376 | if key: | ||
4532 | 377 | try: | ||
4533 | 378 | import_key(key) | ||
4534 | 379 | except GPGKeyError as e: | ||
4535 | 380 | raise SourceConfigError(str(e)) | ||
4536 | 381 | break | ||
4537 | 382 | else: | ||
4538 | 383 | # nothing matched. log an error and maybe sys.exit | ||
4539 | 384 | err = "Unknown source: {!r}".format(source) | ||
4540 | 385 | log(err) | ||
4541 | 386 | if fail_invalid: | ||
4542 | 387 | raise SourceConfigError(err) | ||
4543 | 388 | |||
4544 | 389 | |||
4545 | 390 | def _add_proposed(): | ||
4546 | 391 | """Add the PROPOSED_POCKET as /etc/apt/source.list.d/proposed.list | ||
4547 | 392 | |||
4548 | 393 | Uses lsb_release()['DISTRIB_CODENAME'] to determine the correct staza for | ||
4549 | 394 | the deb line. | ||
4550 | 395 | |||
4551 | 396 | For intel architecutres PROPOSED_POCKET is used for the release, but for | ||
4552 | 397 | other architectures PROPOSED_PORTS_POCKET is used for the release. | ||
4553 | 398 | """ | ||
4554 | 399 | release = lsb_release()['DISTRIB_CODENAME'] | ||
4555 | 400 | arch = platform.machine() | ||
4556 | 401 | if arch not in six.iterkeys(ARCH_TO_PROPOSED_POCKET): | ||
4557 | 402 | raise SourceConfigError("Arch {} not supported for (distro-)proposed" | ||
4558 | 403 | .format(arch)) | ||
4559 | 404 | with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt: | ||
4560 | 405 | apt.write(ARCH_TO_PROPOSED_POCKET[arch].format(release)) | ||
4561 | 406 | |||
4562 | 407 | |||
4563 | 408 | def _add_apt_repository(spec): | ||
4564 | 409 | """Add the spec using add_apt_repository | ||
4565 | 410 | |||
4566 | 411 | :param spec: the parameter to pass to add_apt_repository | ||
4567 | 412 | """ | ||
4568 | 413 | _run_with_retries(['add-apt-repository', '--yes', spec]) | ||
4569 | 414 | |||
4570 | 415 | |||
4571 | 416 | def _add_cloud_pocket(pocket): | ||
4572 | 417 | """Add a cloud pocket as /etc/apt/sources.d/cloud-archive.list | ||
4573 | 418 | |||
4574 | 419 | Note that this overwrites the existing file if there is one. | ||
4575 | 420 | |||
4576 | 421 | This function also converts the simple pocket in to the actual pocket using | ||
4577 | 422 | the CLOUD_ARCHIVE_POCKETS mapping. | ||
4578 | 423 | |||
4579 | 424 | :param pocket: string representing the pocket to add a deb spec for. | ||
4580 | 425 | :raises: SourceConfigError if the cloud pocket doesn't exist or the | ||
4581 | 426 | requested release doesn't match the current distro version. | ||
4582 | 427 | """ | ||
4583 | 428 | apt_install(filter_installed_packages(['ubuntu-cloud-keyring']), | ||
4584 | 429 | fatal=True) | ||
4585 | 430 | if pocket not in CLOUD_ARCHIVE_POCKETS: | ||
4586 | 431 | raise SourceConfigError( | ||
4587 | 432 | 'Unsupported cloud: source option %s' % | ||
4588 | 433 | pocket) | ||
4589 | 434 | actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket] | ||
4590 | 435 | with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt: | ||
4591 | 436 | apt.write(CLOUD_ARCHIVE.format(actual_pocket)) | ||
4592 | 437 | |||
4593 | 438 | |||
4594 | 439 | def _add_cloud_staging(cloud_archive_release, openstack_release): | ||
4595 | 440 | """Add the cloud staging repository which is in | ||
4596 | 441 | ppa:ubuntu-cloud-archive/<openstack_release>-staging | ||
4597 | 442 | |||
4598 | 443 | This function checks that the cloud_archive_release matches the current | ||
4599 | 444 | codename for the distro that charm is being installed on. | ||
4600 | 445 | |||
4601 | 446 | :param cloud_archive_release: string, codename for the release. | ||
4602 | 447 | :param openstack_release: String, codename for the openstack release. | ||
4603 | 448 | :raises: SourceConfigError if the cloud_archive_release doesn't match the | ||
4604 | 449 | current version of the os. | ||
4605 | 450 | """ | ||
4606 | 451 | _verify_is_ubuntu_rel(cloud_archive_release, openstack_release) | ||
4607 | 452 | ppa = 'ppa:ubuntu-cloud-archive/{}-staging'.format(openstack_release) | ||
4608 | 453 | cmd = 'add-apt-repository -y {}'.format(ppa) | ||
4609 | 454 | _run_with_retries(cmd.split(' ')) | ||
4610 | 455 | |||
4611 | 456 | |||
4612 | 457 | def _add_cloud_distro_check(cloud_archive_release, openstack_release): | ||
4613 | 458 | """Add the cloud pocket, but also check the cloud_archive_release against | ||
4614 | 459 | the current distro, and use the openstack_release as the full lookup. | ||
4615 | 460 | |||
4616 | 461 | This just calls _add_cloud_pocket() with the openstack_release as pocket | ||
4617 | 462 | to get the correct cloud-archive.list for dpkg to work with. | ||
4618 | 463 | |||
4619 | 464 | :param cloud_archive_release:String, codename for the distro release. | ||
4620 | 465 | :param openstack_release: String, spec for the release to look up in the | ||
4621 | 466 | CLOUD_ARCHIVE_POCKETS | ||
4622 | 467 | :raises: SourceConfigError if this is the wrong distro, or the pocket spec | ||
4623 | 468 | doesn't exist. | ||
4624 | 469 | """ | ||
4625 | 470 | _verify_is_ubuntu_rel(cloud_archive_release, openstack_release) | ||
4626 | 471 | _add_cloud_pocket("{}-{}".format(cloud_archive_release, openstack_release)) | ||
4627 | 472 | |||
4628 | 473 | |||
4629 | 474 | def _verify_is_ubuntu_rel(release, os_release): | ||
4630 | 475 | """Verify that the release is in the same as the current ubuntu release. | ||
4631 | 476 | |||
4632 | 477 | :param release: String, lowercase for the release. | ||
4633 | 478 | :param os_release: String, the os_release being asked for | ||
4634 | 479 | :raises: SourceConfigError if the release is not the same as the ubuntu | ||
4635 | 480 | release. | ||
4636 | 481 | """ | ||
4637 | 482 | ubuntu_rel = lsb_release()['DISTRIB_CODENAME'] | ||
4638 | 483 | if release != ubuntu_rel: | ||
4639 | 484 | raise SourceConfigError( | ||
4640 | 485 | 'Invalid Cloud Archive release specified: {}-{} on this Ubuntu' | ||
4641 | 486 | 'version ({})'.format(release, os_release, ubuntu_rel)) | ||
4642 | 487 | |||
4643 | 488 | |||
4644 | 489 | def _run_with_retries(cmd, max_retries=CMD_RETRY_COUNT, retry_exitcodes=(1,), | ||
4645 | 490 | retry_message="", cmd_env=None): | ||
4646 | 491 | """Run a command and retry until success or max_retries is reached. | ||
4647 | 492 | |||
4648 | 493 | :param: cmd: str: The apt command to run. | ||
4649 | 494 | :param: max_retries: int: The number of retries to attempt on a fatal | ||
4650 | 495 | command. Defaults to CMD_RETRY_COUNT. | ||
4651 | 496 | :param: retry_exitcodes: tuple: Optional additional exit codes to retry. | ||
4652 | 497 | Defaults to retry on exit code 1. | ||
4653 | 498 | :param: retry_message: str: Optional log prefix emitted during retries. | ||
4654 | 499 | :param: cmd_env: dict: Environment variables to add to the command run. | ||
4655 | 500 | """ | ||
4656 | 501 | |||
4657 | 502 | env = None | ||
4658 | 503 | kwargs = {} | ||
4659 | 504 | if cmd_env: | ||
4660 | 505 | env = os.environ.copy() | ||
4661 | 506 | env.update(cmd_env) | ||
4662 | 507 | kwargs['env'] = env | ||
4663 | 508 | |||
4664 | 509 | if not retry_message: | ||
4665 | 510 | retry_message = "Failed executing '{}'".format(" ".join(cmd)) | ||
4666 | 511 | retry_message += ". Will retry in {} seconds".format(CMD_RETRY_DELAY) | ||
4667 | 512 | |||
4668 | 513 | retry_count = 0 | ||
4669 | 514 | result = None | ||
4670 | 515 | |||
4671 | 516 | retry_results = (None,) + retry_exitcodes | ||
4672 | 517 | while result in retry_results: | ||
4673 | 518 | try: | ||
4674 | 519 | # result = subprocess.check_call(cmd, env=env) | ||
4675 | 520 | result = subprocess.check_call(cmd, **kwargs) | ||
4676 | 521 | except subprocess.CalledProcessError as e: | ||
4677 | 522 | retry_count = retry_count + 1 | ||
4678 | 523 | if retry_count > max_retries: | ||
4679 | 524 | raise | ||
4680 | 525 | result = e.returncode | ||
4681 | 526 | log(retry_message) | ||
4682 | 527 | time.sleep(CMD_RETRY_DELAY) | ||
4683 | 528 | |||
4684 | 529 | |||
4685 | 530 | def _run_apt_command(cmd, fatal=False): | ||
4686 | 531 | """Run an apt command with optional retries. | ||
4687 | 532 | |||
4688 | 533 | :param: cmd: str: The apt command to run. | ||
4689 | 534 | :param: fatal: bool: Whether the command's output should be checked and | ||
4690 | 535 | retried. | ||
4691 | 536 | """ | ||
4692 | 537 | # Provide DEBIAN_FRONTEND=noninteractive if not present in the environment. | ||
4693 | 538 | cmd_env = { | ||
4694 | 539 | 'DEBIAN_FRONTEND': os.environ.get('DEBIAN_FRONTEND', 'noninteractive')} | ||
4695 | 540 | |||
4696 | 541 | if fatal: | ||
4697 | 542 | _run_with_retries( | ||
4698 | 543 | cmd, cmd_env=cmd_env, retry_exitcodes=(1, APT_NO_LOCK,), | ||
4699 | 544 | retry_message="Couldn't acquire DPKG lock") | ||
4700 | 545 | else: | ||
4701 | 546 | env = os.environ.copy() | ||
4702 | 547 | env.update(cmd_env) | ||
4703 | 548 | subprocess.call(cmd, env=env) | ||
4704 | 549 | |||
4705 | 550 | |||
4706 | 551 | def get_upstream_version(package): | ||
4707 | 552 | """Determine upstream version based on installed package | ||
4708 | 553 | |||
4709 | 554 | @returns None (if not installed) or the upstream version | ||
4710 | 555 | """ | ||
4711 | 556 | import apt_pkg | ||
4712 | 557 | cache = apt_cache() | ||
4713 | 558 | try: | ||
4714 | 559 | pkg = cache[package] | ||
4715 | 560 | except: | ||
4716 | 561 | # the package is unknown to the current apt cache. | ||
4717 | 562 | return None | ||
4718 | 563 | |||
4719 | 564 | if not pkg.current_ver: | ||
4720 | 565 | # package is known, but no version is currently installed. | ||
4721 | 566 | return None | ||
4722 | 567 | |||
4723 | 568 | return apt_pkg.upstream_version(pkg.current_ver.ver_str) | ||
4724 | 0 | 569 | ||
4725 | === added file 'hooks/charmhelpers/osplatform.py' | |||
4726 | --- hooks/charmhelpers/osplatform.py 1970-01-01 00:00:00 +0000 | |||
4727 | +++ hooks/charmhelpers/osplatform.py 2017-07-18 06:08:45 +0000 | |||
4728 | @@ -0,0 +1,25 @@ | |||
4729 | 1 | import platform | ||
4730 | 2 | |||
4731 | 3 | |||
4732 | 4 | def get_platform(): | ||
4733 | 5 | """Return the current OS platform. | ||
4734 | 6 | |||
4735 | 7 | For example: if current os platform is Ubuntu then a string "ubuntu" | ||
4736 | 8 | will be returned (which is the name of the module). | ||
4737 | 9 | This string is used to decide which platform module should be imported. | ||
4738 | 10 | """ | ||
4739 | 11 | # linux_distribution is deprecated and will be removed in Python 3.7 | ||
4740 | 12 | # Warings *not* disabled, as we certainly need to fix this. | ||
4741 | 13 | tuple_platform = platform.linux_distribution() | ||
4742 | 14 | current_platform = tuple_platform[0] | ||
4743 | 15 | if "Ubuntu" in current_platform: | ||
4744 | 16 | return "ubuntu" | ||
4745 | 17 | elif "CentOS" in current_platform: | ||
4746 | 18 | return "centos" | ||
4747 | 19 | elif "debian" in current_platform: | ||
4748 | 20 | # Stock Python does not detect Ubuntu and instead returns debian. | ||
4749 | 21 | # Or at least it does in some build environments like Travis CI | ||
4750 | 22 | return "ubuntu" | ||
4751 | 23 | else: | ||
4752 | 24 | raise RuntimeError("This module is not supported on {}." | ||
4753 | 25 | .format(current_platform)) | ||
4754 | 0 | 26 | ||
4755 | === modified file 'hooks/install' | |||
4756 | --- hooks/install 2015-04-22 05:59:37 +0000 | |||
4757 | +++ hooks/install 2017-07-18 06:08:45 +0000 | |||
4758 | @@ -11,16 +11,11 @@ | |||
4759 | 11 | 11 | ||
4760 | 12 | def install(): | 12 | def install(): |
4761 | 13 | hookenv.log('Installing thruk-agent') | 13 | hookenv.log('Installing thruk-agent') |
4762 | 14 | # add steps for installing dependencies and packages here | ||
4763 | 15 | # e.g.: from charmhelpers import fetch | ||
4764 | 16 | # fetch.apt_install(fetch.filter_installed_packages(['nginx'])) | ||
4765 | 17 | config = hookenv.config() | 14 | config = hookenv.config() |
4772 | 18 | ppa = config.get('source') | 15 | add_source(config.get('source'), config.get('key', None)) |
4773 | 19 | if ppa is not None: | 16 | apt_update(fatal=True) |
4774 | 20 | add_source(ppa) | 17 | package_list = ["thruk", "pwgen", "apache2-utils"] |
4775 | 21 | apt_update() | 18 | apt_install(packages=package_list, fatal=True) |
4770 | 22 | |||
4771 | 23 | apt_install(["thruk", "pwgen", "apache2-utils"]) | ||
4776 | 24 | 19 | ||
4777 | 25 | if __name__ == "__main__": | 20 | if __name__ == "__main__": |
4778 | 26 | install() | 21 | install() |
4779 | 27 | 22 | ||
4780 | === modified file 'hooks/services.py' | |||
4781 | --- hooks/services.py 2015-05-26 00:30:33 +0000 | |||
4782 | +++ hooks/services.py 2017-07-18 06:08:45 +0000 | |||
4783 | @@ -22,6 +22,7 @@ | |||
4784 | 22 | source='thruk_local.conf', | 22 | source='thruk_local.conf', |
4785 | 23 | target='/etc/thruk/thruk_local.conf'), | 23 | target='/etc/thruk/thruk_local.conf'), |
4786 | 24 | actions.log_start, | 24 | actions.log_start, |
4787 | 25 | actions.update_ppa, | ||
4788 | 25 | actions.fix_livestatus_perms, | 26 | actions.fix_livestatus_perms, |
4789 | 26 | actions.thruk_set_password, | 27 | actions.thruk_set_password, |
4790 | 27 | actions.notify_thrukmaster_relation, | 28 | actions.notify_thrukmaster_relation, |
LGTM