Merge lp:~simpoir/landscape-client-charm/focal into lp:landscape-client-charm
- focal
- Merge into trunk
Proposed by
Simon Poirier
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 71 | ||||
Proposed branch: | lp:~simpoir/landscape-client-charm/focal | ||||
Merge into: | lp:landscape-client-charm | ||||
Diff against target: |
834 lines (+547/-51) 8 files modified
hooks/charmhelpers/core/hookenv.py (+106/-5) hooks/charmhelpers/core/host.py (+27/-0) hooks/charmhelpers/core/host_factory/ubuntu.py (+3/-1) hooks/charmhelpers/fetch/__init__.py (+2/-0) hooks/charmhelpers/fetch/ubuntu.py (+117/-42) hooks/charmhelpers/fetch/ubuntu_apt_pkg.py (+267/-0) hooks/charmhelpers/osplatform.py (+24/-3) metadata.yaml (+1/-0) |
||||
To merge this branch: | bzr merge lp:~simpoir/landscape-client-charm/focal | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Guillermo Gonzalez | Approve | ||
Review via email: mp+379516@code.launchpad.net |
Commit message
Support for focal
Description of the change
I've added focal to the metadata.
Also updated charm-helpers for this one specific fix:
https:/
To post a comment you must log in.
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote : | # |
Voting does not meet specified criteria. Required: Approve >= 2, Disapprove == 0. Got: 1 Approve.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'hooks/charmhelpers/core/hookenv.py' |
2 | --- hooks/charmhelpers/core/hookenv.py 2019-05-24 20:31:47 +0000 |
3 | +++ hooks/charmhelpers/core/hookenv.py 2020-02-19 23:58:33 +0000 |
4 | @@ -34,6 +34,8 @@ |
5 | import tempfile |
6 | from subprocess import CalledProcessError |
7 | |
8 | +from charmhelpers import deprecate |
9 | + |
10 | import six |
11 | if not six.PY3: |
12 | from UserDict import UserDict |
13 | @@ -119,6 +121,24 @@ |
14 | raise |
15 | |
16 | |
17 | +def function_log(message): |
18 | + """Write a function progress message""" |
19 | + command = ['function-log'] |
20 | + if not isinstance(message, six.string_types): |
21 | + message = repr(message) |
22 | + command += [message[:SH_MAX_ARG]] |
23 | + # Missing function-log should not cause failures in unit tests |
24 | + # Send function_log output to stderr |
25 | + try: |
26 | + subprocess.call(command) |
27 | + except OSError as e: |
28 | + if e.errno == errno.ENOENT: |
29 | + message = "function-log: {}".format(message) |
30 | + print(message, file=sys.stderr) |
31 | + else: |
32 | + raise |
33 | + |
34 | + |
35 | class Serializable(UserDict): |
36 | """Wrapper, an object that can be serialized to yaml or json""" |
37 | |
38 | @@ -946,9 +966,23 @@ |
39 | return os.environ.get('CHARM_DIR') |
40 | |
41 | |
42 | +def cmd_exists(cmd): |
43 | + """Return True if the specified cmd exists in the path""" |
44 | + return any( |
45 | + os.access(os.path.join(path, cmd), os.X_OK) |
46 | + for path in os.environ["PATH"].split(os.pathsep) |
47 | + ) |
48 | + |
49 | + |
50 | @cached |
51 | +@deprecate("moved to function_get()", log=log) |
52 | def action_get(key=None): |
53 | - """Gets the value of an action parameter, or all key/value param pairs""" |
54 | + """ |
55 | + .. deprecated:: 0.20.7 |
56 | + Alias for :func:`function_get`. |
57 | + |
58 | + Gets the value of an action parameter, or all key/value param pairs. |
59 | + """ |
60 | cmd = ['action-get'] |
61 | if key is not None: |
62 | cmd.append(key) |
63 | @@ -957,36 +991,103 @@ |
64 | return action_data |
65 | |
66 | |
67 | +@cached |
68 | +def function_get(key=None): |
69 | + """Gets the value of an action parameter, or all key/value param pairs""" |
70 | + cmd = ['function-get'] |
71 | + # Fallback for older charms. |
72 | + if not cmd_exists('function-get'): |
73 | + cmd = ['action-get'] |
74 | + |
75 | + if key is not None: |
76 | + cmd.append(key) |
77 | + cmd.append('--format=json') |
78 | + function_data = json.loads(subprocess.check_output(cmd).decode('UTF-8')) |
79 | + return function_data |
80 | + |
81 | + |
82 | +@deprecate("moved to function_set()", log=log) |
83 | def action_set(values): |
84 | - """Sets the values to be returned after the action finishes""" |
85 | + """ |
86 | + .. deprecated:: 0.20.7 |
87 | + Alias for :func:`function_set`. |
88 | + |
89 | + Sets the values to be returned after the action finishes. |
90 | + """ |
91 | cmd = ['action-set'] |
92 | for k, v in list(values.items()): |
93 | cmd.append('{}={}'.format(k, v)) |
94 | subprocess.check_call(cmd) |
95 | |
96 | |
97 | +def function_set(values): |
98 | + """Sets the values to be returned after the function finishes""" |
99 | + cmd = ['function-set'] |
100 | + # Fallback for older charms. |
101 | + if not cmd_exists('function-get'): |
102 | + cmd = ['action-set'] |
103 | + |
104 | + for k, v in list(values.items()): |
105 | + cmd.append('{}={}'.format(k, v)) |
106 | + subprocess.check_call(cmd) |
107 | + |
108 | + |
109 | +@deprecate("moved to function_fail()", log=log) |
110 | def action_fail(message): |
111 | - """Sets the action status to failed and sets the error message. |
112 | - |
113 | - The results set by action_set are preserved.""" |
114 | + """ |
115 | + .. deprecated:: 0.20.7 |
116 | + Alias for :func:`function_fail`. |
117 | + |
118 | + Sets the action status to failed and sets the error message. |
119 | + |
120 | + The results set by action_set are preserved. |
121 | + """ |
122 | subprocess.check_call(['action-fail', message]) |
123 | |
124 | |
125 | +def function_fail(message): |
126 | + """Sets the function status to failed and sets the error message. |
127 | + |
128 | + The results set by function_set are preserved.""" |
129 | + cmd = ['function-fail'] |
130 | + # Fallback for older charms. |
131 | + if not cmd_exists('function-fail'): |
132 | + cmd = ['action-fail'] |
133 | + cmd.append(message) |
134 | + |
135 | + subprocess.check_call(cmd) |
136 | + |
137 | + |
138 | def action_name(): |
139 | """Get the name of the currently executing action.""" |
140 | return os.environ.get('JUJU_ACTION_NAME') |
141 | |
142 | |
143 | +def function_name(): |
144 | + """Get the name of the currently executing function.""" |
145 | + return os.environ.get('JUJU_FUNCTION_NAME') or action_name() |
146 | + |
147 | + |
148 | def action_uuid(): |
149 | """Get the UUID of the currently executing action.""" |
150 | return os.environ.get('JUJU_ACTION_UUID') |
151 | |
152 | |
153 | +def function_id(): |
154 | + """Get the ID of the currently executing function.""" |
155 | + return os.environ.get('JUJU_FUNCTION_ID') or action_uuid() |
156 | + |
157 | + |
158 | def action_tag(): |
159 | """Get the tag for the currently executing action.""" |
160 | return os.environ.get('JUJU_ACTION_TAG') |
161 | |
162 | |
163 | +def function_tag(): |
164 | + """Get the tag for the currently executing function.""" |
165 | + return os.environ.get('JUJU_FUNCTION_TAG') or action_tag() |
166 | + |
167 | + |
168 | def status_set(workload_state, message): |
169 | """Set the workload state with a message |
170 | |
171 | |
172 | === modified file 'hooks/charmhelpers/core/host.py' |
173 | --- hooks/charmhelpers/core/host.py 2019-05-24 20:31:47 +0000 |
174 | +++ hooks/charmhelpers/core/host.py 2020-02-19 23:58:33 +0000 |
175 | @@ -1075,3 +1075,30 @@ |
176 | log("Installing new CA cert at: {}".format(cert_file), level=INFO) |
177 | write_file(cert_file, ca_cert) |
178 | subprocess.check_call(['update-ca-certificates', '--fresh']) |
179 | + |
180 | + |
181 | +def get_system_env(key, default=None): |
182 | + """Get data from system environment as represented in ``/etc/environment``. |
183 | + |
184 | + :param key: Key to look up |
185 | + :type key: str |
186 | + :param default: Value to return if key is not found |
187 | + :type default: any |
188 | + :returns: Value for key if found or contents of default parameter |
189 | + :rtype: any |
190 | + :raises: subprocess.CalledProcessError |
191 | + """ |
192 | + env_file = '/etc/environment' |
193 | + # use the shell and env(1) to parse the global environments file. This is |
194 | + # done to get the correct result even if the user has shell variable |
195 | + # substitutions or other shell logic in that file. |
196 | + output = subprocess.check_output( |
197 | + ['env', '-i', '/bin/bash', '-c', |
198 | + 'set -a && source {} && env'.format(env_file)], |
199 | + universal_newlines=True) |
200 | + for k, v in (line.split('=', 1) |
201 | + for line in output.splitlines() if '=' in line): |
202 | + if k == key: |
203 | + return v |
204 | + else: |
205 | + return default |
206 | |
207 | === modified file 'hooks/charmhelpers/core/host_factory/ubuntu.py' |
208 | --- hooks/charmhelpers/core/host_factory/ubuntu.py 2019-05-24 20:31:47 +0000 |
209 | +++ hooks/charmhelpers/core/host_factory/ubuntu.py 2020-02-19 23:58:33 +0000 |
210 | @@ -24,6 +24,8 @@ |
211 | 'bionic', |
212 | 'cosmic', |
213 | 'disco', |
214 | + 'eoan', |
215 | + 'focal' |
216 | ) |
217 | |
218 | |
219 | @@ -93,7 +95,7 @@ |
220 | the pkgcache argument is None. Be sure to add charmhelpers.fetch if |
221 | you call this function, or pass an apt_pkg.Cache() instance. |
222 | """ |
223 | - import apt_pkg |
224 | + from charmhelpers.fetch import apt_pkg |
225 | if not pkgcache: |
226 | from charmhelpers.fetch import apt_cache |
227 | pkgcache = apt_cache() |
228 | |
229 | === modified file 'hooks/charmhelpers/fetch/__init__.py' |
230 | --- hooks/charmhelpers/fetch/__init__.py 2019-05-24 20:31:47 +0000 |
231 | +++ hooks/charmhelpers/fetch/__init__.py 2020-02-19 23:58:33 +0000 |
232 | @@ -103,6 +103,8 @@ |
233 | apt_unhold = fetch.apt_unhold |
234 | import_key = fetch.import_key |
235 | get_upstream_version = fetch.get_upstream_version |
236 | + apt_pkg = fetch.ubuntu_apt_pkg |
237 | + get_apt_dpkg_env = fetch.get_apt_dpkg_env |
238 | elif __platform__ == "centos": |
239 | yum_search = fetch.yum_search |
240 | |
241 | |
242 | === modified file 'hooks/charmhelpers/fetch/ubuntu.py' |
243 | --- hooks/charmhelpers/fetch/ubuntu.py 2019-05-24 20:31:47 +0000 |
244 | +++ hooks/charmhelpers/fetch/ubuntu.py 2020-02-19 23:58:33 +0000 |
245 | @@ -13,14 +13,14 @@ |
246 | # limitations under the License. |
247 | |
248 | from collections import OrderedDict |
249 | -import os |
250 | import platform |
251 | import re |
252 | import six |
253 | +import subprocess |
254 | +import sys |
255 | import time |
256 | -import subprocess |
257 | |
258 | -from charmhelpers.core.host import get_distrib_codename |
259 | +from charmhelpers.core.host import get_distrib_codename, get_system_env |
260 | |
261 | from charmhelpers.core.hookenv import ( |
262 | log, |
263 | @@ -29,6 +29,7 @@ |
264 | env_proxy_settings, |
265 | ) |
266 | from charmhelpers.fetch import SourceConfigError, GPGKeyError |
267 | +from charmhelpers.fetch import ubuntu_apt_pkg |
268 | |
269 | PROPOSED_POCKET = ( |
270 | "# Proposed\n" |
271 | @@ -173,6 +174,22 @@ |
272 | 'stein/proposed': 'bionic-proposed/stein', |
273 | 'bionic-stein/proposed': 'bionic-proposed/stein', |
274 | 'bionic-proposed/stein': 'bionic-proposed/stein', |
275 | + # Train |
276 | + 'train': 'bionic-updates/train', |
277 | + 'bionic-train': 'bionic-updates/train', |
278 | + 'bionic-train/updates': 'bionic-updates/train', |
279 | + 'bionic-updates/train': 'bionic-updates/train', |
280 | + 'train/proposed': 'bionic-proposed/train', |
281 | + 'bionic-train/proposed': 'bionic-proposed/train', |
282 | + 'bionic-proposed/train': 'bionic-proposed/train', |
283 | + # Ussuri |
284 | + 'ussuri': 'bionic-updates/ussuri', |
285 | + 'bionic-ussuri': 'bionic-updates/ussuri', |
286 | + 'bionic-ussuri/updates': 'bionic-updates/ussuri', |
287 | + 'bionic-updates/ussuri': 'bionic-updates/ussuri', |
288 | + 'ussuri/proposed': 'bionic-proposed/ussuri', |
289 | + 'bionic-ussuri/proposed': 'bionic-proposed/ussuri', |
290 | + 'bionic-proposed/ussuri': 'bionic-proposed/ussuri', |
291 | } |
292 | |
293 | |
294 | @@ -208,18 +225,42 @@ |
295 | ) |
296 | |
297 | |
298 | -def apt_cache(in_memory=True, progress=None): |
299 | - """Build and return an apt cache.""" |
300 | - from apt import apt_pkg |
301 | - apt_pkg.init() |
302 | - if in_memory: |
303 | - apt_pkg.config.set("Dir::Cache::pkgcache", "") |
304 | - apt_pkg.config.set("Dir::Cache::srcpkgcache", "") |
305 | - return apt_pkg.Cache(progress) |
306 | +def apt_cache(*_, **__): |
307 | + """Shim returning an object simulating the apt_pkg Cache. |
308 | + |
309 | + :param _: Accept arguments for compability, not used. |
310 | + :type _: any |
311 | + :param __: Accept keyword arguments for compability, not used. |
312 | + :type __: any |
313 | + :returns:Object used to interrogate the system apt and dpkg databases. |
314 | + :rtype:ubuntu_apt_pkg.Cache |
315 | + """ |
316 | + if 'apt_pkg' in sys.modules: |
317 | + # NOTE(fnordahl): When our consumer use the upstream ``apt_pkg`` module |
318 | + # in conjunction with the apt_cache helper function, they may expect us |
319 | + # to call ``apt_pkg.init()`` for them. |
320 | + # |
321 | + # Detect this situation, log a warning and make the call to |
322 | + # ``apt_pkg.init()`` to avoid the consumer Python interpreter from |
323 | + # crashing with a segmentation fault. |
324 | + log('Support for use of upstream ``apt_pkg`` module in conjunction' |
325 | + 'with charm-helpers is deprecated since 2019-06-25', level=WARNING) |
326 | + sys.modules['apt_pkg'].init() |
327 | + return ubuntu_apt_pkg.Cache() |
328 | |
329 | |
330 | def apt_install(packages, options=None, fatal=False): |
331 | - """Install one or more packages.""" |
332 | + """Install one or more packages. |
333 | + |
334 | + :param packages: Package(s) to install |
335 | + :type packages: Option[str, List[str]] |
336 | + :param options: Options to pass on to apt-get |
337 | + :type options: Option[None, List[str]] |
338 | + :param fatal: Whether the command's output should be checked and |
339 | + retried. |
340 | + :type fatal: bool |
341 | + :raises: subprocess.CalledProcessError |
342 | + """ |
343 | if options is None: |
344 | options = ['--option=Dpkg::Options::=--force-confold'] |
345 | |
346 | @@ -236,7 +277,17 @@ |
347 | |
348 | |
349 | def apt_upgrade(options=None, fatal=False, dist=False): |
350 | - """Upgrade all packages.""" |
351 | + """Upgrade all packages. |
352 | + |
353 | + :param options: Options to pass on to apt-get |
354 | + :type options: Option[None, List[str]] |
355 | + :param fatal: Whether the command's output should be checked and |
356 | + retried. |
357 | + :type fatal: bool |
358 | + :param dist: Whether ``dist-upgrade`` should be used over ``upgrade`` |
359 | + :type dist: bool |
360 | + :raises: subprocess.CalledProcessError |
361 | + """ |
362 | if options is None: |
363 | options = ['--option=Dpkg::Options::=--force-confold'] |
364 | |
365 | @@ -257,7 +308,15 @@ |
366 | |
367 | |
368 | def apt_purge(packages, fatal=False): |
369 | - """Purge one or more packages.""" |
370 | + """Purge one or more packages. |
371 | + |
372 | + :param packages: Package(s) to install |
373 | + :type packages: Option[str, List[str]] |
374 | + :param fatal: Whether the command's output should be checked and |
375 | + retried. |
376 | + :type fatal: bool |
377 | + :raises: subprocess.CalledProcessError |
378 | + """ |
379 | cmd = ['apt-get', '--assume-yes', 'purge'] |
380 | if isinstance(packages, six.string_types): |
381 | cmd.append(packages) |
382 | @@ -268,7 +327,14 @@ |
383 | |
384 | |
385 | def apt_autoremove(purge=True, fatal=False): |
386 | - """Purge one or more packages.""" |
387 | + """Purge one or more packages. |
388 | + :param purge: Whether the ``--purge`` option should be passed on or not. |
389 | + :type purge: bool |
390 | + :param fatal: Whether the command's output should be checked and |
391 | + retried. |
392 | + :type fatal: bool |
393 | + :raises: subprocess.CalledProcessError |
394 | + """ |
395 | cmd = ['apt-get', '--assume-yes', 'autoremove'] |
396 | if purge: |
397 | cmd.append('--purge') |
398 | @@ -652,21 +718,22 @@ |
399 | retry_message="", cmd_env=None): |
400 | """Run a command and retry until success or max_retries is reached. |
401 | |
402 | - :param: cmd: str: The apt command to run. |
403 | - :param: max_retries: int: The number of retries to attempt on a fatal |
404 | - command. Defaults to CMD_RETRY_COUNT. |
405 | - :param: retry_exitcodes: tuple: Optional additional exit codes to retry. |
406 | - Defaults to retry on exit code 1. |
407 | - :param: retry_message: str: Optional log prefix emitted during retries. |
408 | - :param: cmd_env: dict: Environment variables to add to the command run. |
409 | + :param cmd: The apt command to run. |
410 | + :type cmd: str |
411 | + :param max_retries: The number of retries to attempt on a fatal |
412 | + command. Defaults to CMD_RETRY_COUNT. |
413 | + :type max_retries: int |
414 | + :param retry_exitcodes: Optional additional exit codes to retry. |
415 | + Defaults to retry on exit code 1. |
416 | + :type retry_exitcodes: tuple |
417 | + :param retry_message: Optional log prefix emitted during retries. |
418 | + :type retry_message: str |
419 | + :param: cmd_env: Environment variables to add to the command run. |
420 | + :type cmd_env: Option[None, Dict[str, str]] |
421 | """ |
422 | - |
423 | - env = None |
424 | - kwargs = {} |
425 | + env = get_apt_dpkg_env() |
426 | if cmd_env: |
427 | - env = os.environ.copy() |
428 | env.update(cmd_env) |
429 | - kwargs['env'] = env |
430 | |
431 | if not retry_message: |
432 | retry_message = "Failed executing '{}'".format(" ".join(cmd)) |
433 | @@ -678,8 +745,7 @@ |
434 | retry_results = (None,) + retry_exitcodes |
435 | while result in retry_results: |
436 | try: |
437 | - # result = subprocess.check_call(cmd, env=env) |
438 | - result = subprocess.check_call(cmd, **kwargs) |
439 | + result = subprocess.check_call(cmd, env=env) |
440 | except subprocess.CalledProcessError as e: |
441 | retry_count = retry_count + 1 |
442 | if retry_count > max_retries: |
443 | @@ -692,22 +758,18 @@ |
444 | def _run_apt_command(cmd, fatal=False): |
445 | """Run an apt command with optional retries. |
446 | |
447 | - :param: cmd: str: The apt command to run. |
448 | - :param: fatal: bool: Whether the command's output should be checked and |
449 | - retried. |
450 | + :param cmd: The apt command to run. |
451 | + :type cmd: str |
452 | + :param fatal: Whether the command's output should be checked and |
453 | + retried. |
454 | + :type fatal: bool |
455 | """ |
456 | - # Provide DEBIAN_FRONTEND=noninteractive if not present in the environment. |
457 | - cmd_env = { |
458 | - 'DEBIAN_FRONTEND': os.environ.get('DEBIAN_FRONTEND', 'noninteractive')} |
459 | - |
460 | if fatal: |
461 | _run_with_retries( |
462 | - cmd, cmd_env=cmd_env, retry_exitcodes=(1, APT_NO_LOCK,), |
463 | + cmd, retry_exitcodes=(1, APT_NO_LOCK,), |
464 | retry_message="Couldn't acquire DPKG lock") |
465 | else: |
466 | - env = os.environ.copy() |
467 | - env.update(cmd_env) |
468 | - subprocess.call(cmd, env=env) |
469 | + subprocess.call(cmd, env=get_apt_dpkg_env()) |
470 | |
471 | |
472 | def get_upstream_version(package): |
473 | @@ -715,7 +777,6 @@ |
474 | |
475 | @returns None (if not installed) or the upstream version |
476 | """ |
477 | - import apt_pkg |
478 | cache = apt_cache() |
479 | try: |
480 | pkg = cache[package] |
481 | @@ -727,4 +788,18 @@ |
482 | # package is known, but no version is currently installed. |
483 | return None |
484 | |
485 | - return apt_pkg.upstream_version(pkg.current_ver.ver_str) |
486 | + return ubuntu_apt_pkg.upstream_version(pkg.current_ver.ver_str) |
487 | + |
488 | + |
489 | +def get_apt_dpkg_env(): |
490 | + """Get environment suitable for execution of APT and DPKG tools. |
491 | + |
492 | + We keep this in a helper function instead of in a global constant to |
493 | + avoid execution on import of the library. |
494 | + :returns: Environment suitable for execution of APT and DPKG tools. |
495 | + :rtype: Dict[str, str] |
496 | + """ |
497 | + # The fallback is used in the event of ``/etc/environment`` not containing |
498 | + # avalid PATH variable. |
499 | + return {'DEBIAN_FRONTEND': 'noninteractive', |
500 | + 'PATH': get_system_env('PATH', '/usr/sbin:/usr/bin:/sbin:/bin')} |
501 | |
502 | === added file 'hooks/charmhelpers/fetch/ubuntu_apt_pkg.py' |
503 | --- hooks/charmhelpers/fetch/ubuntu_apt_pkg.py 1970-01-01 00:00:00 +0000 |
504 | +++ hooks/charmhelpers/fetch/ubuntu_apt_pkg.py 2020-02-19 23:58:33 +0000 |
505 | @@ -0,0 +1,267 @@ |
506 | +# Copyright 2019 Canonical Ltd |
507 | +# |
508 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
509 | +# you may not use this file except in compliance with the License. |
510 | +# You may obtain a copy of the License at |
511 | +# |
512 | +# http://www.apache.org/licenses/LICENSE-2.0 |
513 | +# |
514 | +# Unless required by applicable law or agreed to in writing, software |
515 | +# distributed under the License is distributed on an "AS IS" BASIS, |
516 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
517 | +# See the License for the specific language governing permissions and |
518 | +# limitations under the License. |
519 | + |
520 | +"""Provide a subset of the ``python-apt`` module API. |
521 | + |
522 | +Data collection is done through subprocess calls to ``apt-cache`` and |
523 | +``dpkg-query`` commands. |
524 | + |
525 | +The main purpose for this module is to avoid dependency on the |
526 | +``python-apt`` python module. |
527 | + |
528 | +The indicated python module is a wrapper around the ``apt`` C++ library |
529 | +which is tightly connected to the version of the distribution it was |
530 | +shipped on. It is not developed in a backward/forward compatible manner. |
531 | + |
532 | +This in turn makes it incredibly hard to distribute as a wheel for a piece |
533 | +of python software that supports a span of distro releases [0][1]. |
534 | + |
535 | +Upstream feedback like [2] does not give confidence in this ever changing, |
536 | +so with this we get rid of the dependency. |
537 | + |
538 | +0: https://github.com/juju-solutions/layer-basic/pull/135 |
539 | +1: https://bugs.launchpad.net/charm-octavia/+bug/1824112 |
540 | +2: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=845330#10 |
541 | +""" |
542 | + |
543 | +import locale |
544 | +import os |
545 | +import subprocess |
546 | +import sys |
547 | + |
548 | + |
549 | +class _container(dict): |
550 | + """Simple container for attributes.""" |
551 | + __getattr__ = dict.__getitem__ |
552 | + __setattr__ = dict.__setitem__ |
553 | + |
554 | + |
555 | +class Package(_container): |
556 | + """Simple container for package attributes.""" |
557 | + |
558 | + |
559 | +class Version(_container): |
560 | + """Simple container for version attributes.""" |
561 | + |
562 | + |
563 | +class Cache(object): |
564 | + """Simulation of ``apt_pkg`` Cache object.""" |
565 | + def __init__(self, progress=None): |
566 | + pass |
567 | + |
568 | + def __contains__(self, package): |
569 | + try: |
570 | + pkg = self.__getitem__(package) |
571 | + return pkg is not None |
572 | + except KeyError: |
573 | + return False |
574 | + |
575 | + def __getitem__(self, package): |
576 | + """Get information about a package from apt and dpkg databases. |
577 | + |
578 | + :param package: Name of package |
579 | + :type package: str |
580 | + :returns: Package object |
581 | + :rtype: object |
582 | + :raises: KeyError, subprocess.CalledProcessError |
583 | + """ |
584 | + apt_result = self._apt_cache_show([package])[package] |
585 | + apt_result['name'] = apt_result.pop('package') |
586 | + pkg = Package(apt_result) |
587 | + dpkg_result = self._dpkg_list([package]).get(package, {}) |
588 | + current_ver = None |
589 | + installed_version = dpkg_result.get('version') |
590 | + if installed_version: |
591 | + current_ver = Version({'ver_str': installed_version}) |
592 | + pkg.current_ver = current_ver |
593 | + pkg.architecture = dpkg_result.get('architecture') |
594 | + return pkg |
595 | + |
596 | + def _dpkg_list(self, packages): |
597 | + """Get data from system dpkg database for package. |
598 | + |
599 | + :param packages: Packages to get data from |
600 | + :type packages: List[str] |
601 | + :returns: Structured data about installed packages, keys like |
602 | + ``dpkg-query --list`` |
603 | + :rtype: dict |
604 | + :raises: subprocess.CalledProcessError |
605 | + """ |
606 | + pkgs = {} |
607 | + cmd = ['dpkg-query', '--list'] |
608 | + cmd.extend(packages) |
609 | + if locale.getlocale() == (None, None): |
610 | + # subprocess calls out to locale.getpreferredencoding(False) to |
611 | + # determine encoding. Workaround for Trusty where the |
612 | + # environment appears to not be set up correctly. |
613 | + locale.setlocale(locale.LC_ALL, 'en_US.UTF-8') |
614 | + try: |
615 | + output = subprocess.check_output(cmd, |
616 | + stderr=subprocess.STDOUT, |
617 | + universal_newlines=True) |
618 | + except subprocess.CalledProcessError as cp: |
619 | + # ``dpkg-query`` may return error and at the same time have |
620 | + # produced useful output, for example when asked for multiple |
621 | + # packages where some are not installed |
622 | + if cp.returncode != 1: |
623 | + raise |
624 | + output = cp.output |
625 | + headings = [] |
626 | + for line in output.splitlines(): |
627 | + if line.startswith('||/'): |
628 | + headings = line.split() |
629 | + headings.pop(0) |
630 | + continue |
631 | + elif (line.startswith('|') or line.startswith('+') or |
632 | + line.startswith('dpkg-query:')): |
633 | + continue |
634 | + else: |
635 | + data = line.split(None, 4) |
636 | + status = data.pop(0) |
637 | + if status != 'ii': |
638 | + continue |
639 | + pkg = {} |
640 | + pkg.update({k.lower(): v for k, v in zip(headings, data)}) |
641 | + if 'name' in pkg: |
642 | + pkgs.update({pkg['name']: pkg}) |
643 | + return pkgs |
644 | + |
645 | + def _apt_cache_show(self, packages): |
646 | + """Get data from system apt cache for package. |
647 | + |
648 | + :param packages: Packages to get data from |
649 | + :type packages: List[str] |
650 | + :returns: Structured data about package, keys like |
651 | + ``apt-cache show`` |
652 | + :rtype: dict |
653 | + :raises: subprocess.CalledProcessError |
654 | + """ |
655 | + pkgs = {} |
656 | + cmd = ['apt-cache', 'show', '--no-all-versions'] |
657 | + cmd.extend(packages) |
658 | + if locale.getlocale() == (None, None): |
659 | + # subprocess calls out to locale.getpreferredencoding(False) to |
660 | + # determine encoding. Workaround for Trusty where the |
661 | + # environment appears to not be set up correctly. |
662 | + locale.setlocale(locale.LC_ALL, 'en_US.UTF-8') |
663 | + try: |
664 | + output = subprocess.check_output(cmd, |
665 | + stderr=subprocess.STDOUT, |
666 | + universal_newlines=True) |
667 | + previous = None |
668 | + pkg = {} |
669 | + for line in output.splitlines(): |
670 | + if not line: |
671 | + if 'package' in pkg: |
672 | + pkgs.update({pkg['package']: pkg}) |
673 | + pkg = {} |
674 | + continue |
675 | + if line.startswith(' '): |
676 | + if previous and previous in pkg: |
677 | + pkg[previous] += os.linesep + line.lstrip() |
678 | + continue |
679 | + if ':' in line: |
680 | + kv = line.split(':', 1) |
681 | + key = kv[0].lower() |
682 | + if key == 'n': |
683 | + continue |
684 | + previous = key |
685 | + pkg.update({key: kv[1].lstrip()}) |
686 | + except subprocess.CalledProcessError as cp: |
687 | + # ``apt-cache`` returns 100 if none of the packages asked for |
688 | + # exist in the apt cache. |
689 | + if cp.returncode != 100: |
690 | + raise |
691 | + return pkgs |
692 | + |
693 | + |
694 | +class Config(_container): |
695 | + def __init__(self): |
696 | + super(Config, self).__init__(self._populate()) |
697 | + |
698 | + def _populate(self): |
699 | + cfgs = {} |
700 | + cmd = ['apt-config', 'dump'] |
701 | + output = subprocess.check_output(cmd, |
702 | + stderr=subprocess.STDOUT, |
703 | + universal_newlines=True) |
704 | + for line in output.splitlines(): |
705 | + if not line.startswith("CommandLine"): |
706 | + k, v = line.split(" ", 1) |
707 | + cfgs[k] = v.strip(";").strip("\"") |
708 | + |
709 | + return cfgs |
710 | + |
711 | + |
712 | +# Backwards compatibility with old apt_pkg module |
713 | +sys.modules[__name__].config = Config() |
714 | + |
715 | + |
716 | +def init(): |
717 | + """Compability shim that does nothing.""" |
718 | + pass |
719 | + |
720 | + |
721 | +def upstream_version(version): |
722 | + """Extracts upstream version from a version string. |
723 | + |
724 | + Upstream reference: https://salsa.debian.org/apt-team/apt/blob/master/ |
725 | + apt-pkg/deb/debversion.cc#L259 |
726 | + |
727 | + :param version: Version string |
728 | + :type version: str |
729 | + :returns: Upstream version |
730 | + :rtype: str |
731 | + """ |
732 | + if version: |
733 | + version = version.split(':')[-1] |
734 | + version = version.split('-')[0] |
735 | + return version |
736 | + |
737 | + |
738 | +def version_compare(a, b): |
739 | + """Compare the given versions. |
740 | + |
741 | + Call out to ``dpkg`` to make sure the code doing the comparison is |
742 | + compatible with what the ``apt`` library would do. Mimic the return |
743 | + values. |
744 | + |
745 | + Upstream reference: |
746 | + https://apt-team.pages.debian.net/python-apt/library/apt_pkg.html |
747 | + ?highlight=version_compare#apt_pkg.version_compare |
748 | + |
749 | + :param a: version string |
750 | + :type a: str |
751 | + :param b: version string |
752 | + :type b: str |
753 | + :returns: >0 if ``a`` is greater than ``b``, 0 if a equals b, |
754 | + <0 if ``a`` is smaller than ``b`` |
755 | + :rtype: int |
756 | + :raises: subprocess.CalledProcessError, RuntimeError |
757 | + """ |
758 | + for op in ('gt', 1), ('eq', 0), ('lt', -1): |
759 | + try: |
760 | + subprocess.check_call(['dpkg', '--compare-versions', |
761 | + a, op[0], b], |
762 | + stderr=subprocess.STDOUT, |
763 | + universal_newlines=True) |
764 | + return op[1] |
765 | + except subprocess.CalledProcessError as cp: |
766 | + if cp.returncode == 1: |
767 | + continue |
768 | + raise |
769 | + else: |
770 | + raise RuntimeError('Unable to compare "{}" and "{}", according to ' |
771 | + 'our logic they are neither greater, equal nor ' |
772 | + 'less than each other.') |
773 | |
774 | === modified file 'hooks/charmhelpers/osplatform.py' |
775 | --- hooks/charmhelpers/osplatform.py 2017-03-03 22:25:32 +0000 |
776 | +++ hooks/charmhelpers/osplatform.py 2020-02-19 23:58:33 +0000 |
777 | @@ -1,4 +1,5 @@ |
778 | import platform |
779 | +import os |
780 | |
781 | |
782 | def get_platform(): |
783 | @@ -9,9 +10,13 @@ |
784 | This string is used to decide which platform module should be imported. |
785 | """ |
786 | # linux_distribution is deprecated and will be removed in Python 3.7 |
787 | - # Warings *not* disabled, as we certainly need to fix this. |
788 | - tuple_platform = platform.linux_distribution() |
789 | - current_platform = tuple_platform[0] |
790 | + # Warnings *not* disabled, as we certainly need to fix this. |
791 | + if hasattr(platform, 'linux_distribution'): |
792 | + tuple_platform = platform.linux_distribution() |
793 | + current_platform = tuple_platform[0] |
794 | + else: |
795 | + current_platform = _get_platform_from_fs() |
796 | + |
797 | if "Ubuntu" in current_platform: |
798 | return "ubuntu" |
799 | elif "CentOS" in current_platform: |
800 | @@ -20,6 +25,22 @@ |
801 | # Stock Python does not detect Ubuntu and instead returns debian. |
802 | # Or at least it does in some build environments like Travis CI |
803 | return "ubuntu" |
804 | + elif "elementary" in current_platform: |
805 | + # ElementaryOS fails to run tests locally without this. |
806 | + return "ubuntu" |
807 | else: |
808 | raise RuntimeError("This module is not supported on {}." |
809 | .format(current_platform)) |
810 | + |
811 | + |
812 | +def _get_platform_from_fs(): |
813 | + """Get Platform from /etc/os-release.""" |
814 | + with open(os.path.join(os.sep, 'etc', 'os-release')) as fin: |
815 | + content = dict( |
816 | + line.split('=', 1) |
817 | + for line in fin.read().splitlines() |
818 | + if '=' in line |
819 | + ) |
820 | + for k, v in content.items(): |
821 | + content[k] = v.strip('"') |
822 | + return content["NAME"] |
823 | |
824 | === modified file 'metadata.yaml' |
825 | --- metadata.yaml 2018-02-24 05:26:59 +0000 |
826 | +++ metadata.yaml 2020-02-19 23:58:33 +0000 |
827 | @@ -11,6 +11,7 @@ |
828 | - trusty |
829 | - xenial |
830 | - bionic |
831 | + - focal |
832 | tags: [ ops, monitoring ] |
833 | requires: |
834 | container: |
+1