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