Merge lp:~simpoir/landscape-client-charm/focal into lp:landscape-client-charm

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
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://github.com/juju/charm-helpers/pull/423/files

To post a comment you must log in.
Revision history for this message
Guillermo Gonzalez (verterok) wrote :

+1

review: Approve
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:

Subscribers

People subscribed via source and target branches

to all changes: