Merge lp:~silverdrake11/landscape-client-charm/jammy_series into lp:landscape-client-charm

Proposed by Kevin Nasto
Status: Merged
Approved by: Kevin Nasto
Approved revision: 74
Merged at revision: 73
Proposed branch: lp:~silverdrake11/landscape-client-charm/jammy_series
Merge into: lp:landscape-client-charm
Diff against target: 2275 lines (+860/-292)
21 files modified
hooks/charmhelpers/__init__.py (+7/-20)
hooks/charmhelpers/core/decorators.py (+38/-0)
hooks/charmhelpers/core/hookenv.py (+117/-72)
hooks/charmhelpers/core/host.py (+271/-69)
hooks/charmhelpers/core/host_factory/ubuntu.py (+13/-6)
hooks/charmhelpers/core/services/base.py (+4/-3)
hooks/charmhelpers/core/services/helpers.py (+2/-2)
hooks/charmhelpers/core/strutils.py (+11/-9)
hooks/charmhelpers/core/sysctl.py (+12/-2)
hooks/charmhelpers/core/templating.py (+3/-8)
hooks/charmhelpers/core/unitdata.py (+3/-3)
hooks/charmhelpers/fetch/__init__.py (+8/-9)
hooks/charmhelpers/fetch/archiveurl.py (+34/-26)
hooks/charmhelpers/fetch/centos.py (+3/-4)
hooks/charmhelpers/fetch/python/debug.py (+0/-2)
hooks/charmhelpers/fetch/python/packages.py (+6/-12)
hooks/charmhelpers/fetch/snap.py (+3/-3)
hooks/charmhelpers/fetch/ubuntu.py (+248/-37)
hooks/charmhelpers/fetch/ubuntu_apt_pkg.py (+73/-5)
hooks/charmhelpers/osplatform.py (+3/-0)
metadata.yaml (+1/-0)
To merge this branch: bzr merge lp:~silverdrake11/landscape-client-charm/jammy_series
Reviewer Review Type Date Requested Status
Mitch Burton Approve
Review via email: mp+429307@code.launchpad.net

Commit message

Adding jammy to series to yaml config

Description of the change

Testing instructions:

juju deploy ./jammy_series --config account-name=kevin-nasto --config registration-key=landscapeisgreat --config url=https://landscape.canonical.com/message-system --config ping-url=http://landscape.canonical.com/ping --series jammy

juju deploy ubuntu --series jammy

juju relate landscape-client ubuntu

To post a comment you must log in.
74. By Kevin Nasto

Updating charmhelpers for jammy

Revision history for this message
Mitch Burton (mitchburton) wrote :

+1 lgtm

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'hooks/charmhelpers/__init__.py'
2--- hooks/charmhelpers/__init__.py 2019-05-24 20:31:47 +0000
3+++ hooks/charmhelpers/__init__.py 2022-09-01 15:45:51 +0000
4@@ -14,30 +14,15 @@
5
6 # Bootstrap charm-helpers, installing its dependencies if necessary using
7 # only standard libraries.
8-from __future__ import print_function
9-from __future__ import absolute_import
10-
11 import functools
12 import inspect
13 import subprocess
14-import sys
15
16-try:
17- import six # NOQA:F401
18-except ImportError:
19- if sys.version_info.major == 2:
20- subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
21- else:
22- subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
23- import six # NOQA:F401
24
25 try:
26 import yaml # NOQA:F401
27 except ImportError:
28- if sys.version_info.major == 2:
29- subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
30- else:
31- subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
32+ subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
33 import yaml # NOQA:F401
34
35
36@@ -49,7 +34,8 @@
37
38 def deprecate(warning, date=None, log=None):
39 """Add a deprecation warning the first time the function is used.
40- The date, which is a string in semi-ISO8660 format indicate the year-month
41+
42+ The date which is a string in semi-ISO8660 format indicates the year-month
43 that the function is officially going to be removed.
44
45 usage:
46@@ -62,10 +48,11 @@
47 The reason for passing the logging function (log) is so that hookenv.log
48 can be used for a charm if needed.
49
50- :param warning: String to indicat where it has moved ot.
51- :param date: optional sting, in YYYY-MM format to indicate when the
52+ :param warning: String to indicate what is to be used instead.
53+ :param date: Optional string in YYYY-MM format to indicate when the
54 function will definitely (probably) be removed.
55- :param log: The log function to call to log. If not, logs to stdout
56+ :param log: The log function to call in order to log. If None, logs to
57+ stdout
58 """
59 def wrap(f):
60
61
62=== modified file 'hooks/charmhelpers/core/decorators.py'
63--- hooks/charmhelpers/core/decorators.py 2017-03-03 19:56:10 +0000
64+++ hooks/charmhelpers/core/decorators.py 2022-09-01 15:45:51 +0000
65@@ -53,3 +53,41 @@
66 return _retry_on_exception_inner_2
67
68 return _retry_on_exception_inner_1
69+
70+
71+def retry_on_predicate(num_retries, predicate_fun, base_delay=0):
72+ """Retry based on return value
73+
74+ The return value of the decorated function is passed to the given predicate_fun. If the
75+ result of the predicate is False, retry the decorated function up to num_retries times
76+
77+ An exponential backoff up to base_delay^num_retries seconds can be introduced by setting
78+ base_delay to a nonzero value. The default is to run with a zero (i.e. no) delay
79+
80+ :param num_retries: Max. number of retries to perform
81+ :type num_retries: int
82+ :param predicate_fun: Predicate function to determine if a retry is necessary
83+ :type predicate_fun: callable
84+ :param base_delay: Starting value in seconds for exponential delay, defaults to 0 (no delay)
85+ :type base_delay: float
86+ """
87+ def _retry_on_pred_inner_1(f):
88+ def _retry_on_pred_inner_2(*args, **kwargs):
89+ retries = num_retries
90+ multiplier = 1
91+ delay = base_delay
92+ while True:
93+ result = f(*args, **kwargs)
94+ if predicate_fun(result) or retries <= 0:
95+ return result
96+ delay *= multiplier
97+ multiplier += 1
98+ log("Result {}, retrying '{}' {} more times (delay={})".format(
99+ result, f.__name__, retries, delay), level=INFO)
100+ retries -= 1
101+ if delay:
102+ time.sleep(delay)
103+
104+ return _retry_on_pred_inner_2
105+
106+ return _retry_on_pred_inner_1
107
108=== modified file 'hooks/charmhelpers/core/hookenv.py'
109--- hooks/charmhelpers/core/hookenv.py 2020-02-19 23:47:52 +0000
110+++ hooks/charmhelpers/core/hookenv.py 2022-09-01 15:45:51 +0000
111@@ -1,4 +1,4 @@
112-# Copyright 2014-2015 Canonical Limited.
113+# Copyright 2013-2021 Canonical Limited.
114 #
115 # Licensed under the Apache License, Version 2.0 (the "License");
116 # you may not use this file except in compliance with the License.
117@@ -13,16 +13,15 @@
118 # limitations under the License.
119
120 "Interactions with the Juju environment"
121-# Copyright 2013 Canonical Ltd.
122 #
123 # Authors:
124 # Charm Helpers Developers <juju@lists.ubuntu.com>
125
126-from __future__ import print_function
127 import copy
128 from distutils.version import LooseVersion
129+from enum import Enum
130 from functools import wraps
131-from collections import namedtuple
132+from collections import namedtuple, UserDict
133 import glob
134 import os
135 import json
136@@ -36,12 +35,6 @@
137
138 from charmhelpers import deprecate
139
140-import six
141-if not six.PY3:
142- from UserDict import UserDict
143-else:
144- from collections import UserDict
145-
146
147 CRITICAL = "CRITICAL"
148 ERROR = "ERROR"
149@@ -57,6 +50,14 @@
150 'This may not be compatible with software you are '
151 'running in your shell.')
152
153+
154+class WORKLOAD_STATES(Enum):
155+ ACTIVE = 'active'
156+ BLOCKED = 'blocked'
157+ MAINTENANCE = 'maintenance'
158+ WAITING = 'waiting'
159+
160+
161 cache = {}
162
163
164@@ -104,7 +105,7 @@
165 command = ['juju-log']
166 if level:
167 command += ['-l', level]
168- if not isinstance(message, six.string_types):
169+ if not isinstance(message, str):
170 message = repr(message)
171 command += [message[:SH_MAX_ARG]]
172 # Missing juju-log should not cause failures in unit tests
173@@ -124,7 +125,7 @@
174 def function_log(message):
175 """Write a function progress message"""
176 command = ['function-log']
177- if not isinstance(message, six.string_types):
178+ if not isinstance(message, str):
179 message = repr(message)
180 command += [message[:SH_MAX_ARG]]
181 # Missing function-log should not cause failures in unit tests
182@@ -217,6 +218,17 @@
183 raise ValueError('Must specify neither or both of relation_name and service_or_unit')
184
185
186+def departing_unit():
187+ """The departing unit for the current relation hook.
188+
189+ Available since juju 2.8.
190+
191+ :returns: the departing unit, or None if the information isn't available.
192+ :rtype: Optional[str]
193+ """
194+ return os.environ.get('JUJU_DEPARTING_UNIT', None)
195+
196+
197 def local_unit():
198 """Local unit ID"""
199 return os.environ['JUJU_UNIT_NAME']
200@@ -363,8 +375,10 @@
201 try:
202 self._prev_dict = json.load(f)
203 except ValueError as e:
204- log('Unable to parse previous config data - {}'.format(str(e)),
205- level=ERROR)
206+ log('Found but was unable to parse previous config data, '
207+ 'ignoring which will report all values as changed - {}'
208+ .format(str(e)), level=ERROR)
209+ return
210 for k, v in copy.deepcopy(self._prev_dict).items():
211 if k not in self:
212 self[k] = v
213@@ -425,12 +439,6 @@
214 global _cache_config
215 config_cmd_line = ['config-get', '--all', '--format=json']
216 try:
217- # JSON Decode Exception for Python3.5+
218- exc_json = json.decoder.JSONDecodeError
219- except AttributeError:
220- # JSON Decode Exception for Python2.7 through Python3.4
221- exc_json = ValueError
222- try:
223 if _cache_config is None:
224 config_data = json.loads(
225 subprocess.check_output(config_cmd_line).decode('UTF-8'))
226@@ -438,7 +446,7 @@
227 if scope is not None:
228 return _cache_config.get(scope)
229 return _cache_config
230- except (exc_json, UnicodeDecodeError) as e:
231+ except (json.decoder.JSONDecodeError, UnicodeDecodeError) as e:
232 log('Unable to parse output from config-get: config_cmd_line="{}" '
233 'message="{}"'
234 .format(config_cmd_line, str(e)), level=ERROR)
235@@ -446,15 +454,20 @@
236
237
238 @cached
239-def relation_get(attribute=None, unit=None, rid=None):
240+def relation_get(attribute=None, unit=None, rid=None, app=None):
241 """Get relation information"""
242 _args = ['relation-get', '--format=json']
243+ if app is not None:
244+ if unit is not None:
245+ raise ValueError("Cannot use both 'unit' and 'app'")
246+ _args.append('--app')
247 if rid:
248 _args.append('-r')
249 _args.append(rid)
250 _args.append(attribute or '-')
251- if unit:
252- _args.append(unit)
253+ # unit or application name
254+ if unit or app:
255+ _args.append(unit or app)
256 try:
257 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
258 except ValueError:
259@@ -465,12 +478,28 @@
260 raise
261
262
263-def relation_set(relation_id=None, relation_settings=None, **kwargs):
264+@cached
265+def _relation_set_accepts_file():
266+ """Return True if the juju relation-set command accepts a file.
267+
268+ Cache the result as it won't change during the execution of a hook, and
269+ thus we can make relation_set() more efficient by only checking for the
270+ first relation_set() call.
271+
272+ :returns: True if relation_set accepts a file.
273+ :rtype: bool
274+ :raises: subprocess.CalledProcessError if the check fails.
275+ """
276+ return "--file" in subprocess.check_output(
277+ ["relation-set", "--help"], universal_newlines=True)
278+
279+
280+def relation_set(relation_id=None, relation_settings=None, app=False, **kwargs):
281 """Set relation information for the current unit"""
282 relation_settings = relation_settings if relation_settings else {}
283 relation_cmd_line = ['relation-set']
284- accepts_file = "--file" in subprocess.check_output(
285- relation_cmd_line + ["--help"], universal_newlines=True)
286+ if app:
287+ relation_cmd_line.append('--app')
288 if relation_id is not None:
289 relation_cmd_line.extend(('-r', relation_id))
290 settings = relation_settings.copy()
291@@ -480,7 +509,7 @@
292 # sites pass in things like dicts or numbers.
293 if value is not None:
294 settings[key] = "{}".format(value)
295- if accepts_file:
296+ if _relation_set_accepts_file():
297 # --file was introduced in Juju 1.23.2. Use it by default if
298 # available, since otherwise we'll break if the relation data is
299 # too big. Ideally we should tell relation-set to read the data from
300@@ -581,7 +610,7 @@
301 relation_type()))
302
303 :param reltype: Relation type to list data for, default is to list data for
304- the realtion type we are currently executing a hook for.
305+ the relation type we are currently executing a hook for.
306 :type reltype: str
307 :returns: iterator
308 :rtype: types.GeneratorType
309@@ -598,7 +627,7 @@
310
311 @cached
312 def relation_for_unit(unit=None, rid=None):
313- """Get the json represenation of a unit's relation"""
314+ """Get the json representation of a unit's relation"""
315 unit = unit or remote_unit()
316 relation = relation_get(unit=unit, rid=rid)
317 for key in relation:
318@@ -975,14 +1004,8 @@
319
320
321 @cached
322-@deprecate("moved to function_get()", log=log)
323 def action_get(key=None):
324- """
325- .. deprecated:: 0.20.7
326- Alias for :func:`function_get`.
327-
328- Gets the value of an action parameter, or all key/value param pairs.
329- """
330+ """Gets the value of an action parameter, or all key/value param pairs."""
331 cmd = ['action-get']
332 if key is not None:
333 cmd.append(key)
334@@ -992,8 +1015,12 @@
335
336
337 @cached
338+@deprecate("moved to action_get()", log=log)
339 def function_get(key=None):
340- """Gets the value of an action parameter, or all key/value param pairs"""
341+ """
342+ .. deprecated::
343+ Gets the value of an action parameter, or all key/value param pairs.
344+ """
345 cmd = ['function-get']
346 # Fallback for older charms.
347 if not cmd_exists('function-get'):
348@@ -1006,22 +1033,20 @@
349 return function_data
350
351
352-@deprecate("moved to function_set()", log=log)
353 def action_set(values):
354- """
355- .. deprecated:: 0.20.7
356- Alias for :func:`function_set`.
357-
358- Sets the values to be returned after the action finishes.
359- """
360+ """Sets the values to be returned after the action finishes."""
361 cmd = ['action-set']
362 for k, v in list(values.items()):
363 cmd.append('{}={}'.format(k, v))
364 subprocess.check_call(cmd)
365
366
367+@deprecate("moved to action_set()", log=log)
368 def function_set(values):
369- """Sets the values to be returned after the function finishes"""
370+ """
371+ .. deprecated::
372+ Sets the values to be returned after the function finishes.
373+ """
374 cmd = ['function-set']
375 # Fallback for older charms.
376 if not cmd_exists('function-get'):
377@@ -1032,12 +1057,8 @@
378 subprocess.check_call(cmd)
379
380
381-@deprecate("moved to function_fail()", log=log)
382 def action_fail(message):
383 """
384- .. deprecated:: 0.20.7
385- Alias for :func:`function_fail`.
386-
387 Sets the action status to failed and sets the error message.
388
389 The results set by action_set are preserved.
390@@ -1045,10 +1066,14 @@
391 subprocess.check_call(['action-fail', message])
392
393
394+@deprecate("moved to action_fail()", log=log)
395 def function_fail(message):
396- """Sets the function status to failed and sets the error message.
397+ """
398+ .. deprecated::
399+ Sets the function status to failed and sets the error message.
400
401- The results set by function_set are preserved."""
402+ The results set by function_set are preserved.
403+ """
404 cmd = ['function-fail']
405 # Fallback for older charms.
406 if not cmd_exists('function-fail'):
407@@ -1088,22 +1113,33 @@
408 return os.environ.get('JUJU_FUNCTION_TAG') or action_tag()
409
410
411-def status_set(workload_state, message):
412+def status_set(workload_state, message, application=False):
413 """Set the workload state with a message
414
415 Use status-set to set the workload state with a message which is visible
416 to the user via juju status. If the status-set command is not found then
417- assume this is juju < 1.23 and juju-log the message unstead.
418+ assume this is juju < 1.23 and juju-log the message instead.
419
420- workload_state -- valid juju workload state.
421- message -- status update message
422+ workload_state -- valid juju workload state. str or WORKLOAD_STATES
423+ message -- status update message
424+ application -- Whether this is an application state set
425 """
426- valid_states = ['maintenance', 'blocked', 'waiting', 'active']
427- if workload_state not in valid_states:
428- raise ValueError(
429- '{!r} is not a valid workload state'.format(workload_state)
430- )
431- cmd = ['status-set', workload_state, message]
432+ bad_state_msg = '{!r} is not a valid workload state'
433+
434+ if isinstance(workload_state, str):
435+ try:
436+ # Convert string to enum.
437+ workload_state = WORKLOAD_STATES[workload_state.upper()]
438+ except KeyError:
439+ raise ValueError(bad_state_msg.format(workload_state))
440+
441+ if workload_state not in WORKLOAD_STATES:
442+ raise ValueError(bad_state_msg.format(workload_state))
443+
444+ cmd = ['status-set']
445+ if application:
446+ cmd.append('--application')
447+ cmd.extend([workload_state.value, message])
448 try:
449 ret = subprocess.call(cmd)
450 if ret == 0:
451@@ -1111,7 +1147,7 @@
452 except OSError as e:
453 if e.errno != errno.ENOENT:
454 raise
455- log_message = 'status-set failed: {} {}'.format(workload_state,
456+ log_message = 'status-set failed: {} {}'.format(workload_state.value,
457 message)
458 log(log_message, level='INFO')
459
460@@ -1526,13 +1562,13 @@
461 """Get proxy settings from process environment variables.
462
463 Get charm proxy settings from environment variables that correspond to
464- juju-http-proxy, juju-https-proxy and juju-no-proxy (available as of 2.4.2,
465- see lp:1782236) in a format suitable for passing to an application that
466- reacts to proxy settings passed as environment variables. Some applications
467- support lowercase or uppercase notation (e.g. curl), some support only
468- lowercase (e.g. wget), there are also subjectively rare cases of only
469- uppercase notation support. no_proxy CIDR and wildcard support also varies
470- between runtimes and applications as there is no enforced standard.
471+ juju-http-proxy, juju-https-proxy juju-no-proxy (available as of 2.4.2, see
472+ lp:1782236) and juju-ftp-proxy in a format suitable for passing to an
473+ application that reacts to proxy settings passed as environment variables.
474+ Some applications support lowercase or uppercase notation (e.g. curl), some
475+ support only lowercase (e.g. wget), there are also subjectively rare cases
476+ of only uppercase notation support. no_proxy CIDR and wildcard support also
477+ varies between runtimes and applications as there is no enforced standard.
478
479 Some applications may connect to multiple destinations and expose config
480 options that would affect only proxy settings for a specific destination
481@@ -1574,11 +1610,11 @@
482 def _contains_range(addresses):
483 """Check for cidr or wildcard domain in a string.
484
485- Given a string comprising a comma seperated list of ip addresses
486+ Given a string comprising a comma separated list of ip addresses
487 and domain names, determine whether the string contains IP ranges
488 or wildcard domains.
489
490- :param addresses: comma seperated list of domains and ip addresses.
491+ :param addresses: comma separated list of domains and ip addresses.
492 :type addresses: str
493 """
494 return (
495@@ -1589,3 +1625,12 @@
496 addresses.startswith(".") or
497 ",." in addresses or
498 " ." in addresses)
499+
500+
501+def is_subordinate():
502+ """Check whether charm is subordinate in unit metadata.
503+
504+ :returns: True if unit is subordniate, False otherwise.
505+ :rtype: bool
506+ """
507+ return metadata().get('subordinate') is True
508
509=== modified file 'hooks/charmhelpers/core/host.py'
510--- hooks/charmhelpers/core/host.py 2020-02-19 23:47:52 +0000
511+++ hooks/charmhelpers/core/host.py 2022-09-01 15:45:51 +0000
512@@ -1,4 +1,4 @@
513-# Copyright 2014-2015 Canonical Limited.
514+# Copyright 2014-2021 Canonical Limited.
515 #
516 # Licensed under the Apache License, Version 2.0 (the "License");
517 # you may not use this file except in compliance with the License.
518@@ -19,6 +19,7 @@
519 # Nick Moffitt <nick.moffitt@canonical.com>
520 # Matthew Wedgwood <matthew.wedgwood@canonical.com>
521
522+import errno
523 import os
524 import re
525 import pwd
526@@ -30,10 +31,9 @@
527 import hashlib
528 import functools
529 import itertools
530-import six
531
532 from contextlib import contextmanager
533-from collections import OrderedDict
534+from collections import OrderedDict, defaultdict
535 from .hookenv import log, INFO, DEBUG, local_unit, charm_name
536 from .fstab import Fstab
537 from charmhelpers.osplatform import get_platform
538@@ -59,6 +59,7 @@
539 ) # flake8: noqa -- ignore F401 for this import
540
541 UPDATEDB_PATH = '/etc/updatedb.conf'
542+CA_CERT_DIR = '/usr/local/share/ca-certificates'
543
544
545 def service_start(service_name, **kwargs):
546@@ -113,6 +114,33 @@
547 return service('stop', service_name, **kwargs)
548
549
550+def service_enable(service_name, **kwargs):
551+ """Enable a system service.
552+
553+ The specified service name is managed via the system level init system.
554+ Some init systems (e.g. upstart) require that additional arguments be
555+ provided in order to directly control service instances whereas other init
556+ systems allow for addressing instances of a service directly by name (e.g.
557+ systemd).
558+
559+ The kwargs allow for the additional parameters to be passed to underlying
560+ init systems for those systems which require/allow for them. For example,
561+ the ceph-osd upstart script requires the id parameter to be passed along
562+ in order to identify which running daemon should be restarted. The follow-
563+ ing example restarts the ceph-osd service for instance id=4:
564+
565+ service_enable('ceph-osd', id=4)
566+
567+ :param service_name: the name of the service to enable
568+ :param **kwargs: additional parameters to pass to the init system when
569+ managing services. These will be passed as key=value
570+ parameters to the init system's commandline. kwargs
571+ are ignored for init systems not allowing additional
572+ parameters via the commandline (systemd).
573+ """
574+ return service('enable', service_name, **kwargs)
575+
576+
577 def service_restart(service_name, **kwargs):
578 """Restart a system service.
579
580@@ -133,7 +161,7 @@
581 :param service_name: the name of the service to restart
582 :param **kwargs: additional parameters to pass to the init system when
583 managing services. These will be passed as key=value
584- parameters to the init system's commandline. kwargs
585+ parameters to the init system's commandline. kwargs
586 are ignored for init systems not allowing additional
587 parameters via the commandline (systemd).
588 """
589@@ -193,7 +221,7 @@
590 stopped = service_stop(service_name, **kwargs)
591 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
592 sysv_file = os.path.join(initd_dir, service_name)
593- if init_is_systemd():
594+ if init_is_systemd(service_name=service_name):
595 service('disable', service_name)
596 service('mask', service_name)
597 elif os.path.exists(upstart_file):
598@@ -215,7 +243,7 @@
599 initd_dir="/etc/init.d", **kwargs):
600 """Resume a system service.
601
602- Reenable starting again at boot. Start the service.
603+ Re-enable starting again at boot. Start the service.
604
605 :param service_name: the name of the service to resume
606 :param init_dir: the path to the init dir
607@@ -227,7 +255,7 @@
608 """
609 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
610 sysv_file = os.path.join(initd_dir, service_name)
611- if init_is_systemd():
612+ if init_is_systemd(service_name=service_name):
613 service('unmask', service_name)
614 service('enable', service_name)
615 elif os.path.exists(upstart_file):
616@@ -249,7 +277,7 @@
617 return started
618
619
620-def service(action, service_name, **kwargs):
621+def service(action, service_name=None, **kwargs):
622 """Control a system service.
623
624 :param action: the action to take on the service
625@@ -257,11 +285,13 @@
626 :param **kwargs: additional params to be passed to the service command in
627 the form of key=value.
628 """
629- if init_is_systemd():
630- cmd = ['systemctl', action, service_name]
631+ if init_is_systemd(service_name=service_name):
632+ cmd = ['systemctl', action]
633+ if service_name is not None:
634+ cmd.append(service_name)
635 else:
636 cmd = ['service', service_name, action]
637- for key, value in six.iteritems(kwargs):
638+ for key, value in kwargs.items():
639 parameter = '%s=%s' % (key, value)
640 cmd.append(parameter)
641 return subprocess.call(cmd) == 0
642@@ -281,13 +311,13 @@
643 units (e.g. service ceph-osd status id=2). The kwargs
644 are ignored in systemd services.
645 """
646- if init_is_systemd():
647+ if init_is_systemd(service_name=service_name):
648 return service('is-active', service_name)
649 else:
650 if os.path.exists(_UPSTART_CONF.format(service_name)):
651 try:
652 cmd = ['status', service_name]
653- for key, value in six.iteritems(kwargs):
654+ for key, value in kwargs.items():
655 parameter = '%s=%s' % (key, value)
656 cmd.append(parameter)
657 output = subprocess.check_output(
658@@ -311,8 +341,14 @@
659 SYSTEMD_SYSTEM = '/run/systemd/system'
660
661
662-def init_is_systemd():
663- """Return True if the host system uses systemd, False otherwise."""
664+def init_is_systemd(service_name=None):
665+ """
666+ Returns whether the host uses systemd for the specified service.
667+
668+ @param Optional[str] service_name: specific name of service
669+ """
670+ if str(service_name).startswith("snap."):
671+ return True
672 if lsb_release()['DISTRIB_CODENAME'] == 'trusty':
673 return False
674 return os.path.isdir(SYSTEMD_SYSTEM)
675@@ -556,7 +592,7 @@
676 with open(path, 'wb') as target:
677 os.fchown(target.fileno(), uid, gid)
678 os.fchmod(target.fileno(), perms)
679- if six.PY3 and isinstance(content, six.string_types):
680+ if isinstance(content, str):
681 content = content.encode('UTF-8')
682 target.write(content)
683 return
684@@ -671,7 +707,7 @@
685
686 :param str checksum: Value of the checksum used to validate the file.
687 :param str hash_type: Hash algorithm used to generate `checksum`.
688- Can be any hash alrgorithm supported by :mod:`hashlib`,
689+ Can be any hash algorithm supported by :mod:`hashlib`,
690 such as md5, sha1, sha256, sha512, etc.
691 :raises ChecksumError: If the file fails the checksum
692
693@@ -686,78 +722,227 @@
694 pass
695
696
697-def restart_on_change(restart_map, stopstart=False, restart_functions=None):
698- """Restart services based on configuration files changing
699-
700- This function is used a decorator, for example::
701-
702- @restart_on_change({
703- '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
704- '/etc/apache/sites-enabled/*': [ 'apache2' ]
705- })
706- def config_changed():
707- pass # your code here
708-
709- In this example, the cinder-api and cinder-volume services
710- would be restarted if /etc/ceph/ceph.conf is changed by the
711- ceph_client_changed function. The apache2 service would be
712- restarted if any file matching the pattern got changed, created
713- or removed. Standard wildcards are supported, see documentation
714- for the 'glob' module for more information.
715-
716- @param restart_map: {path_file_name: [service_name, ...]
717- @param stopstart: DEFAULT false; whether to stop, start OR restart
718- @param restart_functions: nonstandard functions to use to restart services
719- {svc: func, ...}
720- @returns result from decorated function
721+class restart_on_change(object):
722+ """Decorator and context manager to handle restarts.
723+
724+ Usage:
725+
726+ @restart_on_change(restart_map, ...)
727+ def function_that_might_trigger_a_restart(...)
728+ ...
729+
730+ Or:
731+
732+ with restart_on_change(restart_map, ...):
733+ do_stuff_that_might_trigger_a_restart()
734+ ...
735 """
736- def wrap(f):
737+
738+ def __init__(self, restart_map, stopstart=False, restart_functions=None,
739+ can_restart_now_f=None, post_svc_restart_f=None,
740+ pre_restarts_wait_f=None):
741+ """
742+ :param restart_map: {file: [service, ...]}
743+ :type restart_map: Dict[str, List[str,]]
744+ :param stopstart: whether to stop, start or restart a service
745+ :type stopstart: booleean
746+ :param restart_functions: nonstandard functions to use to restart
747+ services {svc: func, ...}
748+ :type restart_functions: Dict[str, Callable[[str], None]]
749+ :param can_restart_now_f: A function used to check if the restart is
750+ permitted.
751+ :type can_restart_now_f: Callable[[str, List[str]], boolean]
752+ :param post_svc_restart_f: A function run after a service has
753+ restarted.
754+ :type post_svc_restart_f: Callable[[str], None]
755+ :param pre_restarts_wait_f: A function called before any restarts.
756+ :type pre_restarts_wait_f: Callable[None, None]
757+ """
758+ self.restart_map = restart_map
759+ self.stopstart = stopstart
760+ self.restart_functions = restart_functions
761+ self.can_restart_now_f = can_restart_now_f
762+ self.post_svc_restart_f = post_svc_restart_f
763+ self.pre_restarts_wait_f = pre_restarts_wait_f
764+
765+ def __call__(self, f):
766+ """Work like a decorator.
767+
768+ Returns a wrapped function that performs the restart if triggered.
769+
770+ :param f: The function that is being wrapped.
771+ :type f: Callable[[Any], Any]
772+ :returns: the wrapped function
773+ :rtype: Callable[[Any], Any]
774+ """
775 @functools.wraps(f)
776 def wrapped_f(*args, **kwargs):
777 return restart_on_change_helper(
778- (lambda: f(*args, **kwargs)), restart_map, stopstart,
779- restart_functions)
780+ (lambda: f(*args, **kwargs)),
781+ self.restart_map,
782+ stopstart=self.stopstart,
783+ restart_functions=self.restart_functions,
784+ can_restart_now_f=self.can_restart_now_f,
785+ post_svc_restart_f=self.post_svc_restart_f,
786+ pre_restarts_wait_f=self.pre_restarts_wait_f)
787 return wrapped_f
788- return wrap
789+
790+ def __enter__(self):
791+ """Enter the runtime context related to this object. """
792+ self.checksums = _pre_restart_on_change_helper(self.restart_map)
793+
794+ def __exit__(self, exc_type, exc_val, exc_tb):
795+ """Exit the runtime context related to this object.
796+
797+ The parameters describe the exception that caused the context to be
798+ exited. If the context was exited without an exception, all three
799+ arguments will be None.
800+ """
801+ if exc_type is None:
802+ _post_restart_on_change_helper(
803+ self.checksums,
804+ self.restart_map,
805+ stopstart=self.stopstart,
806+ restart_functions=self.restart_functions,
807+ can_restart_now_f=self.can_restart_now_f,
808+ post_svc_restart_f=self.post_svc_restart_f,
809+ pre_restarts_wait_f=self.pre_restarts_wait_f)
810+ # All is good, so return False; any exceptions will propagate.
811+ return False
812
813
814 def restart_on_change_helper(lambda_f, restart_map, stopstart=False,
815- restart_functions=None):
816+ restart_functions=None,
817+ can_restart_now_f=None,
818+ post_svc_restart_f=None,
819+ pre_restarts_wait_f=None):
820 """Helper function to perform the restart_on_change function.
821
822 This is provided for decorators to restart services if files described
823 in the restart_map have changed after an invocation of lambda_f().
824
825- @param lambda_f: function to call.
826- @param restart_map: {file: [service, ...]}
827- @param stopstart: whether to stop, start or restart a service
828- @param restart_functions: nonstandard functions to use to restart services
829- {svc: func, ...}
830- @returns result of lambda_f()
831+ This functions allows for a number of helper functions to be passed.
832+
833+ `restart_functions` is a map with a service as the key and the
834+ corresponding value being the function to call to restart the service. For
835+ example if `restart_functions={'some-service': my_restart_func}` then
836+ `my_restart_func` should a function which takes one argument which is the
837+ service name to be retstarted.
838+
839+ `can_restart_now_f` is a function which checks that a restart is permitted.
840+ It should return a bool which indicates if a restart is allowed and should
841+ take a service name (str) and a list of changed files (List[str]) as
842+ arguments.
843+
844+ `post_svc_restart_f` is a function which runs after a service has been
845+ restarted. It takes the service name that was restarted as an argument.
846+
847+ `pre_restarts_wait_f` is a function which is called before any restarts
848+ occur. The use case for this is an application which wants to try and
849+ stagger restarts between units.
850+
851+ :param lambda_f: function to call.
852+ :type lambda_f: Callable[[], ANY]
853+ :param restart_map: {file: [service, ...]}
854+ :type restart_map: Dict[str, List[str,]]
855+ :param stopstart: whether to stop, start or restart a service
856+ :type stopstart: booleean
857+ :param restart_functions: nonstandard functions to use to restart services
858+ {svc: func, ...}
859+ :type restart_functions: Dict[str, Callable[[str], None]]
860+ :param can_restart_now_f: A function used to check if the restart is
861+ permitted.
862+ :type can_restart_now_f: Callable[[str, List[str]], boolean]
863+ :param post_svc_restart_f: A function run after a service has
864+ restarted.
865+ :type post_svc_restart_f: Callable[[str], None]
866+ :param pre_restarts_wait_f: A function called before any restarts.
867+ :type pre_restarts_wait_f: Callable[None, None]
868+ :returns: result of lambda_f()
869+ :rtype: ANY
870+ """
871+ checksums = _pre_restart_on_change_helper(restart_map)
872+ r = lambda_f()
873+ _post_restart_on_change_helper(checksums,
874+ restart_map,
875+ stopstart,
876+ restart_functions,
877+ can_restart_now_f,
878+ post_svc_restart_f,
879+ pre_restarts_wait_f)
880+ return r
881+
882+
883+def _pre_restart_on_change_helper(restart_map):
884+ """Take a snapshot of file hashes.
885+
886+ :param restart_map: {file: [service, ...]}
887+ :type restart_map: Dict[str, List[str,]]
888+ :returns: Dictionary of file paths and the files checksum.
889+ :rtype: Dict[str, str]
890+ """
891+ return {path: path_hash(path) for path in restart_map}
892+
893+
894+def _post_restart_on_change_helper(checksums,
895+ restart_map,
896+ stopstart=False,
897+ restart_functions=None,
898+ can_restart_now_f=None,
899+ post_svc_restart_f=None,
900+ pre_restarts_wait_f=None):
901+ """Check whether files have changed.
902+
903+ :param checksums: Dictionary of file paths and the files checksum.
904+ :type checksums: Dict[str, str]
905+ :param restart_map: {file: [service, ...]}
906+ :type restart_map: Dict[str, List[str,]]
907+ :param stopstart: whether to stop, start or restart a service
908+ :type stopstart: booleean
909+ :param restart_functions: nonstandard functions to use to restart services
910+ {svc: func, ...}
911+ :type restart_functions: Dict[str, Callable[[str], None]]
912+ :param can_restart_now_f: A function used to check if the restart is
913+ permitted.
914+ :type can_restart_now_f: Callable[[str, List[str]], boolean]
915+ :param post_svc_restart_f: A function run after a service has
916+ restarted.
917+ :type post_svc_restart_f: Callable[[str], None]
918+ :param pre_restarts_wait_f: A function called before any restarts.
919+ :type pre_restarts_wait_f: Callable[None, None]
920 """
921 if restart_functions is None:
922 restart_functions = {}
923- checksums = {path: path_hash(path) for path in restart_map}
924- r = lambda_f()
925+ changed_files = defaultdict(list)
926+ restarts = []
927 # create a list of lists of the services to restart
928- restarts = [restart_map[path]
929- for path in restart_map
930- if path_hash(path) != checksums[path]]
931+ for path, services in restart_map.items():
932+ if path_hash(path) != checksums[path]:
933+ restarts.append(services)
934+ for svc in services:
935+ changed_files[svc].append(path)
936 # create a flat list of ordered services without duplicates from lists
937 services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts)))
938 if services_list:
939+ if pre_restarts_wait_f:
940+ pre_restarts_wait_f()
941 actions = ('stop', 'start') if stopstart else ('restart',)
942 for service_name in services_list:
943+ if can_restart_now_f:
944+ if not can_restart_now_f(service_name,
945+ changed_files[service_name]):
946+ continue
947 if service_name in restart_functions:
948 restart_functions[service_name](service_name)
949 else:
950 for action in actions:
951 service(action, service_name)
952- return r
953+ if post_svc_restart_f:
954+ post_svc_restart_f(service_name)
955
956
957 def pwgen(length=None):
958- """Generate a random pasword."""
959+ """Generate a random password."""
960 if length is None:
961 # A random length is ok to use a weak PRNG
962 length = random.choice(range(35, 45))
963@@ -769,7 +954,7 @@
964 random_generator = random.SystemRandom()
965 random_chars = [
966 random_generator.choice(alphanumeric_chars) for _ in range(length)]
967- return(''.join(random_chars))
968+ return ''.join(random_chars)
969
970
971 def is_phy_iface(interface):
972@@ -810,7 +995,7 @@
973
974 def list_nics(nic_type=None):
975 """Return a list of nics of given type(s)"""
976- if isinstance(nic_type, six.string_types):
977+ if isinstance(nic_type, str):
978 int_types = [nic_type]
979 else:
980 int_types = nic_type
981@@ -819,7 +1004,8 @@
982 if nic_type:
983 for int_type in int_types:
984 cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
985- ip_output = subprocess.check_output(cmd).decode('UTF-8')
986+ ip_output = subprocess.check_output(
987+ cmd).decode('UTF-8', errors='replace')
988 ip_output = ip_output.split('\n')
989 ip_output = (line for line in ip_output if line)
990 for line in ip_output:
991@@ -835,7 +1021,8 @@
992 interfaces.append(iface)
993 else:
994 cmd = ['ip', 'a']
995- ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
996+ ip_output = subprocess.check_output(
997+ cmd).decode('UTF-8', errors='replace').split('\n')
998 ip_output = (line.strip() for line in ip_output if line)
999
1000 key = re.compile(r'^[0-9]+:\s+(.+):')
1001@@ -859,7 +1046,8 @@
1002 def get_nic_mtu(nic):
1003 """Return the Maximum Transmission Unit (MTU) for a network interface."""
1004 cmd = ['ip', 'addr', 'show', nic]
1005- ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
1006+ ip_output = subprocess.check_output(
1007+ cmd).decode('UTF-8', errors='replace').split('\n')
1008 mtu = ""
1009 for line in ip_output:
1010 words = line.split()
1011@@ -871,7 +1059,7 @@
1012 def get_nic_hwaddr(nic):
1013 """Return the Media Access Control (MAC) for a network interface."""
1014 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
1015- ip_output = subprocess.check_output(cmd).decode('UTF-8')
1016+ ip_output = subprocess.check_output(cmd).decode('UTF-8', errors='replace')
1017 hwaddr = ""
1018 words = ip_output.split()
1019 if 'link/ether' in words:
1020@@ -883,7 +1071,7 @@
1021 def chdir(directory):
1022 """Change the current working directory to a different directory for a code
1023 block and return the previous directory after the block exits. Useful to
1024- run commands from a specificed directory.
1025+ run commands from a specified directory.
1026
1027 :param str directory: The directory path to change to for this context.
1028 """
1029@@ -918,9 +1106,12 @@
1030 for root, dirs, files in os.walk(path, followlinks=follow_links):
1031 for name in dirs + files:
1032 full = os.path.join(root, name)
1033- broken_symlink = os.path.lexists(full) and not os.path.exists(full)
1034- if not broken_symlink:
1035+ try:
1036 chown(full, uid, gid)
1037+ except (IOError, OSError) as e:
1038+ # Intended to ignore "file not found".
1039+ if e.errno == errno.ENOENT:
1040+ pass
1041
1042
1043 def lchownr(path, owner, group):
1044@@ -1053,6 +1244,17 @@
1045 return calculated_wait_time
1046
1047
1048+def ca_cert_absolute_path(basename_without_extension):
1049+ """Returns absolute path to CA certificate.
1050+
1051+ :param basename_without_extension: Filename without extension
1052+ :type basename_without_extension: str
1053+ :returns: Absolute full path
1054+ :rtype: str
1055+ """
1056+ return '{}/{}.crt'.format(CA_CERT_DIR, basename_without_extension)
1057+
1058+
1059 def install_ca_cert(ca_cert, name=None):
1060 """
1061 Install the given cert as a trusted CA.
1062@@ -1068,7 +1270,7 @@
1063 ca_cert = ca_cert.encode('utf8')
1064 if not name:
1065 name = 'juju-{}'.format(charm_name())
1066- cert_file = '/usr/local/share/ca-certificates/{}.crt'.format(name)
1067+ cert_file = ca_cert_absolute_path(name)
1068 new_hash = hashlib.md5(ca_cert).hexdigest()
1069 if file_hash(cert_file) == new_hash:
1070 return
1071
1072=== modified file 'hooks/charmhelpers/core/host_factory/ubuntu.py'
1073--- hooks/charmhelpers/core/host_factory/ubuntu.py 2020-02-19 23:47:52 +0000
1074+++ hooks/charmhelpers/core/host_factory/ubuntu.py 2022-09-01 15:45:51 +0000
1075@@ -25,7 +25,12 @@
1076 'cosmic',
1077 'disco',
1078 'eoan',
1079- 'focal'
1080+ 'focal',
1081+ 'groovy',
1082+ 'hirsute',
1083+ 'impish',
1084+ 'jammy',
1085+ 'kinetic',
1086 )
1087
1088
1089@@ -95,12 +100,14 @@
1090 the pkgcache argument is None. Be sure to add charmhelpers.fetch if
1091 you call this function, or pass an apt_pkg.Cache() instance.
1092 """
1093- from charmhelpers.fetch import apt_pkg
1094+ from charmhelpers.fetch import apt_pkg, get_installed_version
1095 if not pkgcache:
1096- from charmhelpers.fetch import apt_cache
1097- pkgcache = apt_cache()
1098- pkg = pkgcache[package]
1099- return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
1100+ current_ver = get_installed_version(package)
1101+ else:
1102+ pkg = pkgcache[package]
1103+ current_ver = pkg.current_ver
1104+
1105+ return apt_pkg.version_compare(current_ver.ver_str, revno)
1106
1107
1108 @cached
1109
1110=== modified file 'hooks/charmhelpers/core/services/base.py'
1111--- hooks/charmhelpers/core/services/base.py 2019-05-24 20:31:47 +0000
1112+++ hooks/charmhelpers/core/services/base.py 2022-09-01 15:45:51 +0000
1113@@ -14,8 +14,9 @@
1114
1115 import os
1116 import json
1117-from inspect import getargspec
1118-from collections import Iterable, OrderedDict
1119+import inspect
1120+from collections import OrderedDict
1121+from collections.abc import Iterable
1122
1123 from charmhelpers.core import host
1124 from charmhelpers.core import hookenv
1125@@ -169,7 +170,7 @@
1126 if not units:
1127 continue
1128 remote_service = units[0].split('/')[0]
1129- argspec = getargspec(provider.provide_data)
1130+ argspec = inspect.getfullargspec(provider.provide_data)
1131 if len(argspec.args) > 1:
1132 data = provider.provide_data(remote_service, service_ready)
1133 else:
1134
1135=== modified file 'hooks/charmhelpers/core/services/helpers.py'
1136--- hooks/charmhelpers/core/services/helpers.py 2017-03-03 19:56:10 +0000
1137+++ hooks/charmhelpers/core/services/helpers.py 2022-09-01 15:45:51 +0000
1138@@ -179,7 +179,7 @@
1139 self.required_options = args
1140 self['config'] = hookenv.config()
1141 with open(os.path.join(hookenv.charm_dir(), 'config.yaml')) as fp:
1142- self.config = yaml.load(fp).get('options', {})
1143+ self.config = yaml.safe_load(fp).get('options', {})
1144
1145 def __bool__(self):
1146 for option in self.required_options:
1147@@ -227,7 +227,7 @@
1148 if not os.path.isabs(file_name):
1149 file_name = os.path.join(hookenv.charm_dir(), file_name)
1150 with open(file_name, 'r') as file_stream:
1151- data = yaml.load(file_stream)
1152+ data = yaml.safe_load(file_stream)
1153 if not data:
1154 raise OSError("%s is empty" % file_name)
1155 return data
1156
1157=== modified file 'hooks/charmhelpers/core/strutils.py'
1158--- hooks/charmhelpers/core/strutils.py 2019-05-24 20:31:47 +0000
1159+++ hooks/charmhelpers/core/strutils.py 2022-09-01 15:45:51 +0000
1160@@ -15,26 +15,28 @@
1161 # See the License for the specific language governing permissions and
1162 # limitations under the License.
1163
1164-import six
1165 import re
1166
1167-
1168-def bool_from_string(value):
1169+TRUTHY_STRINGS = {'y', 'yes', 'true', 't', 'on'}
1170+FALSEY_STRINGS = {'n', 'no', 'false', 'f', 'off'}
1171+
1172+
1173+def bool_from_string(value, truthy_strings=TRUTHY_STRINGS, falsey_strings=FALSEY_STRINGS, assume_false=False):
1174 """Interpret string value as boolean.
1175
1176 Returns True if value translates to True otherwise False.
1177 """
1178- if isinstance(value, six.string_types):
1179- value = six.text_type(value)
1180+ if isinstance(value, str):
1181+ value = str(value)
1182 else:
1183 msg = "Unable to interpret non-string value '%s' as boolean" % (value)
1184 raise ValueError(msg)
1185
1186 value = value.strip().lower()
1187
1188- if value in ['y', 'yes', 'true', 't', 'on']:
1189+ if value in truthy_strings:
1190 return True
1191- elif value in ['n', 'no', 'false', 'f', 'off']:
1192+ elif value in falsey_strings or assume_false:
1193 return False
1194
1195 msg = "Unable to interpret string value '%s' as boolean" % (value)
1196@@ -58,8 +60,8 @@
1197 'P': 5,
1198 'PB': 5,
1199 }
1200- if isinstance(value, six.string_types):
1201- value = six.text_type(value)
1202+ if isinstance(value, str):
1203+ value = str(value)
1204 else:
1205 msg = "Unable to interpret non-string value '%s' as bytes" % (value)
1206 raise ValueError(msg)
1207
1208=== modified file 'hooks/charmhelpers/core/sysctl.py'
1209--- hooks/charmhelpers/core/sysctl.py 2019-05-24 20:31:47 +0000
1210+++ hooks/charmhelpers/core/sysctl.py 2022-09-01 15:45:51 +0000
1211@@ -17,14 +17,17 @@
1212
1213 import yaml
1214
1215-from subprocess import check_call
1216+from subprocess import check_call, CalledProcessError
1217
1218 from charmhelpers.core.hookenv import (
1219 log,
1220 DEBUG,
1221 ERROR,
1222+ WARNING,
1223 )
1224
1225+from charmhelpers.core.host import is_container
1226+
1227 __author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
1228
1229
1230@@ -62,4 +65,11 @@
1231 if ignore:
1232 call.append("-e")
1233
1234- check_call(call)
1235+ try:
1236+ check_call(call)
1237+ except CalledProcessError as e:
1238+ if is_container():
1239+ log("Error setting some sysctl keys in this container: {}".format(e.output),
1240+ level=WARNING)
1241+ else:
1242+ raise e
1243
1244=== modified file 'hooks/charmhelpers/core/templating.py'
1245--- hooks/charmhelpers/core/templating.py 2019-05-24 20:31:47 +0000
1246+++ hooks/charmhelpers/core/templating.py 2022-09-01 15:45:51 +0000
1247@@ -13,7 +13,6 @@
1248 # limitations under the License.
1249
1250 import os
1251-import sys
1252
1253 from charmhelpers.core import host
1254 from charmhelpers.core import hookenv
1255@@ -43,9 +42,8 @@
1256 The rendered template will be written to the file as well as being returned
1257 as a string.
1258
1259- Note: Using this requires python-jinja2 or python3-jinja2; if it is not
1260- installed, calling this will attempt to use charmhelpers.fetch.apt_install
1261- to install it.
1262+ Note: Using this requires python3-jinja2; if it is not installed, calling
1263+ this will attempt to use charmhelpers.fetch.apt_install to install it.
1264 """
1265 try:
1266 from jinja2 import FileSystemLoader, Environment, exceptions
1267@@ -57,10 +55,7 @@
1268 'charmhelpers.fetch to install it',
1269 level=hookenv.ERROR)
1270 raise
1271- if sys.version_info.major == 2:
1272- apt_install('python-jinja2', fatal=True)
1273- else:
1274- apt_install('python3-jinja2', fatal=True)
1275+ apt_install('python3-jinja2', fatal=True)
1276 from jinja2 import FileSystemLoader, Environment, exceptions
1277
1278 if template_loader:
1279
1280=== modified file 'hooks/charmhelpers/core/unitdata.py'
1281--- hooks/charmhelpers/core/unitdata.py 2019-05-24 20:31:47 +0000
1282+++ hooks/charmhelpers/core/unitdata.py 2022-09-01 15:45:51 +0000
1283@@ -1,7 +1,7 @@
1284 #!/usr/bin/env python
1285 # -*- coding: utf-8 -*-
1286 #
1287-# Copyright 2014-2015 Canonical Limited.
1288+# Copyright 2014-2021 Canonical Limited.
1289 #
1290 # Licensed under the Apache License, Version 2.0 (the "License");
1291 # you may not use this file except in compliance with the License.
1292@@ -61,7 +61,7 @@
1293 'previous value', prev,
1294 'current value', cur)
1295
1296- # Get some unit specific bookeeping
1297+ # Get some unit specific bookkeeping
1298 if not db.get('pkg_key'):
1299 key = urllib.urlopen('https://example.com/pkg_key').read()
1300 db.set('pkg_key', key)
1301@@ -449,7 +449,7 @@
1302 'previous value', prev,
1303 'current value', cur)
1304
1305- # Get some unit specific bookeeping
1306+ # Get some unit specific bookkeeping
1307 if not db.get('pkg_key'):
1308 key = urllib.urlopen('https://example.com/pkg_key').read()
1309 db.set('pkg_key', key)
1310
1311=== modified file 'hooks/charmhelpers/fetch/__init__.py'
1312--- hooks/charmhelpers/fetch/__init__.py 2020-02-19 23:47:52 +0000
1313+++ hooks/charmhelpers/fetch/__init__.py 2022-09-01 15:45:51 +0000
1314@@ -1,4 +1,4 @@
1315-# Copyright 2014-2015 Canonical Limited.
1316+# Copyright 2014-2021 Canonical Limited.
1317 #
1318 # Licensed under the Apache License, Version 2.0 (the "License");
1319 # you may not use this file except in compliance with the License.
1320@@ -20,11 +20,7 @@
1321 log,
1322 )
1323
1324-import six
1325-if six.PY3:
1326- from urllib.parse import urlparse, urlunparse
1327-else:
1328- from urlparse import urlparse, urlunparse
1329+from urllib.parse import urlparse, urlunparse
1330
1331
1332 # The order of this list is very important. Handlers should be listed in from
1333@@ -105,6 +101,9 @@
1334 get_upstream_version = fetch.get_upstream_version
1335 apt_pkg = fetch.ubuntu_apt_pkg
1336 get_apt_dpkg_env = fetch.get_apt_dpkg_env
1337+ get_installed_version = fetch.get_installed_version
1338+ OPENSTACK_RELEASES = fetch.OPENSTACK_RELEASES
1339+ UBUNTU_OPENSTACK_RELEASE = fetch.UBUNTU_OPENSTACK_RELEASE
1340 elif __platform__ == "centos":
1341 yum_search = fetch.yum_search
1342
1343@@ -131,14 +130,14 @@
1344 sources = safe_load((config(sources_var) or '').strip()) or []
1345 keys = safe_load((config(keys_var) or '').strip()) or None
1346
1347- if isinstance(sources, six.string_types):
1348+ if isinstance(sources, str):
1349 sources = [sources]
1350
1351 if keys is None:
1352 for source in sources:
1353 add_source(source, None)
1354 else:
1355- if isinstance(keys, six.string_types):
1356+ if isinstance(keys, str):
1357 keys = [keys]
1358
1359 if len(sources) != len(keys):
1360@@ -202,7 +201,7 @@
1361 classname)
1362 plugin_list.append(handler_class())
1363 except NotImplementedError:
1364- # Skip missing plugins so that they can be ommitted from
1365+ # Skip missing plugins so that they can be omitted from
1366 # installation if desired
1367 log("FetchHandler {} not found, skipping plugin".format(
1368 handler_name))
1369
1370=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
1371--- hooks/charmhelpers/fetch/archiveurl.py 2019-05-24 20:31:47 +0000
1372+++ hooks/charmhelpers/fetch/archiveurl.py 2022-09-01 15:45:51 +0000
1373@@ -12,6 +12,7 @@
1374 # See the License for the specific language governing permissions and
1375 # limitations under the License.
1376
1377+import contextlib
1378 import os
1379 import hashlib
1380 import re
1381@@ -24,28 +25,21 @@
1382 get_archive_handler,
1383 extract,
1384 )
1385+from charmhelpers.core.hookenv import (
1386+ env_proxy_settings,
1387+)
1388 from charmhelpers.core.host import mkdir, check_hash
1389
1390-import six
1391-if six.PY3:
1392- from urllib.request import (
1393- build_opener, install_opener, urlopen, urlretrieve,
1394- HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler,
1395- )
1396- from urllib.parse import urlparse, urlunparse, parse_qs
1397- from urllib.error import URLError
1398-else:
1399- from urllib import urlretrieve
1400- from urllib2 import (
1401- build_opener, install_opener, urlopen,
1402- HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler,
1403- URLError
1404- )
1405- from urlparse import urlparse, urlunparse, parse_qs
1406+from urllib.request import (
1407+ build_opener, install_opener, urlopen, urlretrieve,
1408+ HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler,
1409+ ProxyHandler
1410+)
1411+from urllib.parse import urlparse, urlunparse, parse_qs
1412+from urllib.error import URLError
1413
1414
1415 def splituser(host):
1416- '''urllib.splituser(), but six's support of this seems broken'''
1417 _userprog = re.compile('^(.*)@(.*)$')
1418 match = _userprog.match(host)
1419 if match:
1420@@ -54,7 +48,6 @@
1421
1422
1423 def splitpasswd(user):
1424- '''urllib.splitpasswd(), but six's support of this is missing'''
1425 _passwdprog = re.compile('^([^:]*):(.*)$', re.S)
1426 match = _passwdprog.match(user)
1427 if match:
1428@@ -62,6 +55,20 @@
1429 return user, None
1430
1431
1432+@contextlib.contextmanager
1433+def proxy_env():
1434+ """
1435+ Creates a context which temporarily modifies the proxy settings in os.environ.
1436+ """
1437+ restore = {**os.environ} # Copy the current os.environ
1438+ juju_proxies = env_proxy_settings() or {}
1439+ os.environ.update(**juju_proxies) # Insert or Update the os.environ
1440+ yield os.environ
1441+ for key in juju_proxies:
1442+ del os.environ[key] # remove any keys which were added or updated
1443+ os.environ.update(**restore) # restore any original values
1444+
1445+
1446 class ArchiveUrlFetchHandler(BaseFetchHandler):
1447 """
1448 Handler to download archive files from arbitrary URLs.
1449@@ -92,6 +99,7 @@
1450 # propagate all exceptions
1451 # URLError, OSError, etc
1452 proto, netloc, path, params, query, fragment = urlparse(source)
1453+ handlers = []
1454 if proto in ('http', 'https'):
1455 auth, barehost = splituser(netloc)
1456 if auth is not None:
1457@@ -101,10 +109,13 @@
1458 # Realm is set to None in add_password to force the username and password
1459 # to be used whatever the realm
1460 passman.add_password(None, source, username, password)
1461- authhandler = HTTPBasicAuthHandler(passman)
1462- opener = build_opener(authhandler)
1463- install_opener(opener)
1464- response = urlopen(source)
1465+ handlers.append(HTTPBasicAuthHandler(passman))
1466+
1467+ with proxy_env():
1468+ handlers.append(ProxyHandler())
1469+ opener = build_opener(*handlers)
1470+ install_opener(opener)
1471+ response = urlopen(source)
1472 try:
1473 with open(dest, 'wb') as dest_file:
1474 dest_file.write(response.read())
1475@@ -150,10 +161,7 @@
1476 raise UnhandledSource(e.strerror)
1477 options = parse_qs(url_parts.fragment)
1478 for key, value in options.items():
1479- if not six.PY3:
1480- algorithms = hashlib.algorithms
1481- else:
1482- algorithms = hashlib.algorithms_available
1483+ algorithms = hashlib.algorithms_available
1484 if key in algorithms:
1485 if len(value) != 1:
1486 raise TypeError(
1487
1488=== modified file 'hooks/charmhelpers/fetch/centos.py'
1489--- hooks/charmhelpers/fetch/centos.py 2019-05-24 20:31:47 +0000
1490+++ hooks/charmhelpers/fetch/centos.py 2022-09-01 15:45:51 +0000
1491@@ -15,7 +15,6 @@
1492 import subprocess
1493 import os
1494 import time
1495-import six
1496 import yum
1497
1498 from tempfile import NamedTemporaryFile
1499@@ -42,7 +41,7 @@
1500 if options is not None:
1501 cmd.extend(options)
1502 cmd.append('install')
1503- if isinstance(packages, six.string_types):
1504+ if isinstance(packages, str):
1505 cmd.append(packages)
1506 else:
1507 cmd.extend(packages)
1508@@ -71,7 +70,7 @@
1509 def purge(packages, fatal=False):
1510 """Purge one or more packages."""
1511 cmd = ['yum', '--assumeyes', 'remove']
1512- if isinstance(packages, six.string_types):
1513+ if isinstance(packages, str):
1514 cmd.append(packages)
1515 else:
1516 cmd.extend(packages)
1517@@ -83,7 +82,7 @@
1518 """Search for a package."""
1519 output = {}
1520 cmd = ['yum', 'search']
1521- if isinstance(packages, six.string_types):
1522+ if isinstance(packages, str):
1523 cmd.append(packages)
1524 else:
1525 cmd.extend(packages)
1526
1527=== modified file 'hooks/charmhelpers/fetch/python/debug.py'
1528--- hooks/charmhelpers/fetch/python/debug.py 2019-05-24 20:31:47 +0000
1529+++ hooks/charmhelpers/fetch/python/debug.py 2022-09-01 15:45:51 +0000
1530@@ -15,8 +15,6 @@
1531 # See the License for the specific language governing permissions and
1532 # limitations under the License.
1533
1534-from __future__ import print_function
1535-
1536 import atexit
1537 import sys
1538
1539
1540=== modified file 'hooks/charmhelpers/fetch/python/packages.py'
1541--- hooks/charmhelpers/fetch/python/packages.py 2019-05-24 20:31:47 +0000
1542+++ hooks/charmhelpers/fetch/python/packages.py 2022-09-01 15:45:51 +0000
1543@@ -1,7 +1,7 @@
1544 #!/usr/bin/env python
1545 # coding: utf-8
1546
1547-# Copyright 2014-2015 Canonical Limited.
1548+# Copyright 2014-2021 Canonical Limited.
1549 #
1550 # Licensed under the Apache License, Version 2.0 (the "License");
1551 # you may not use this file except in compliance with the License.
1552@@ -16,7 +16,6 @@
1553 # limitations under the License.
1554
1555 import os
1556-import six
1557 import subprocess
1558 import sys
1559
1560@@ -27,7 +26,7 @@
1561
1562
1563 def pip_execute(*args, **kwargs):
1564- """Overriden pip_execute() to stop sys.path being changed.
1565+ """Overridden pip_execute() to stop sys.path being changed.
1566
1567 The act of importing main from the pip module seems to cause add wheels
1568 from the /usr/share/python-wheels which are installed by various tools.
1569@@ -40,10 +39,7 @@
1570 from pip import main as _pip_execute
1571 except ImportError:
1572 apt_update()
1573- if six.PY2:
1574- apt_install('python-pip')
1575- else:
1576- apt_install('python3-pip')
1577+ apt_install('python3-pip')
1578 from pip import main as _pip_execute
1579 _pip_execute(*args, **kwargs)
1580 finally:
1581@@ -140,10 +136,8 @@
1582
1583 def pip_create_virtualenv(path=None):
1584 """Create an isolated Python environment."""
1585- if six.PY2:
1586- apt_install('python-virtualenv')
1587- else:
1588- apt_install('python3-virtualenv')
1589+ apt_install(['python3-virtualenv', 'virtualenv'])
1590+ extra_flags = ['--python=python3']
1591
1592 if path:
1593 venv_path = path
1594@@ -151,4 +145,4 @@
1595 venv_path = os.path.join(charm_dir(), 'venv')
1596
1597 if not os.path.exists(venv_path):
1598- subprocess.check_call(['virtualenv', venv_path])
1599+ subprocess.check_call(['virtualenv', venv_path] + extra_flags)
1600
1601=== modified file 'hooks/charmhelpers/fetch/snap.py'
1602--- hooks/charmhelpers/fetch/snap.py 2019-05-24 20:31:47 +0000
1603+++ hooks/charmhelpers/fetch/snap.py 2022-09-01 15:45:51 +0000
1604@@ -1,4 +1,4 @@
1605-# Copyright 2014-2017 Canonical Limited.
1606+# Copyright 2014-2021 Canonical Limited.
1607 #
1608 # Licensed under the Apache License, Version 2.0 (the "License");
1609 # you may not use this file except in compliance with the License.
1610@@ -65,11 +65,11 @@
1611 retry_count += + 1
1612 if retry_count > SNAP_NO_LOCK_RETRY_COUNT:
1613 raise CouldNotAcquireLockException(
1614- 'Could not aquire lock after {} attempts'
1615+ 'Could not acquire lock after {} attempts'
1616 .format(SNAP_NO_LOCK_RETRY_COUNT))
1617 return_code = e.returncode
1618 log('Snap failed to acquire lock, trying again in {} seconds.'
1619- .format(SNAP_NO_LOCK_RETRY_DELAY, level='WARN'))
1620+ .format(SNAP_NO_LOCK_RETRY_DELAY), level='WARN')
1621 sleep(SNAP_NO_LOCK_RETRY_DELAY)
1622
1623 return return_code
1624
1625=== modified file 'hooks/charmhelpers/fetch/ubuntu.py'
1626--- hooks/charmhelpers/fetch/ubuntu.py 2020-02-19 23:47:52 +0000
1627+++ hooks/charmhelpers/fetch/ubuntu.py 2022-09-01 15:45:51 +0000
1628@@ -1,4 +1,4 @@
1629-# Copyright 2014-2015 Canonical Limited.
1630+# Copyright 2014-2021 Canonical Limited.
1631 #
1632 # Licensed under the Apache License, Version 2.0 (the "License");
1633 # you may not use this file except in compliance with the License.
1634@@ -15,11 +15,11 @@
1635 from collections import OrderedDict
1636 import platform
1637 import re
1638-import six
1639 import subprocess
1640 import sys
1641 import time
1642
1643+from charmhelpers import deprecate
1644 from charmhelpers.core.host import get_distrib_codename, get_system_env
1645
1646 from charmhelpers.core.hookenv import (
1647@@ -190,12 +190,106 @@
1648 'ussuri/proposed': 'bionic-proposed/ussuri',
1649 'bionic-ussuri/proposed': 'bionic-proposed/ussuri',
1650 'bionic-proposed/ussuri': 'bionic-proposed/ussuri',
1651+ # Victoria
1652+ 'victoria': 'focal-updates/victoria',
1653+ 'focal-victoria': 'focal-updates/victoria',
1654+ 'focal-victoria/updates': 'focal-updates/victoria',
1655+ 'focal-updates/victoria': 'focal-updates/victoria',
1656+ 'victoria/proposed': 'focal-proposed/victoria',
1657+ 'focal-victoria/proposed': 'focal-proposed/victoria',
1658+ 'focal-proposed/victoria': 'focal-proposed/victoria',
1659+ # Wallaby
1660+ 'wallaby': 'focal-updates/wallaby',
1661+ 'focal-wallaby': 'focal-updates/wallaby',
1662+ 'focal-wallaby/updates': 'focal-updates/wallaby',
1663+ 'focal-updates/wallaby': 'focal-updates/wallaby',
1664+ 'wallaby/proposed': 'focal-proposed/wallaby',
1665+ 'focal-wallaby/proposed': 'focal-proposed/wallaby',
1666+ 'focal-proposed/wallaby': 'focal-proposed/wallaby',
1667+ # Xena
1668+ 'xena': 'focal-updates/xena',
1669+ 'focal-xena': 'focal-updates/xena',
1670+ 'focal-xena/updates': 'focal-updates/xena',
1671+ 'focal-updates/xena': 'focal-updates/xena',
1672+ 'xena/proposed': 'focal-proposed/xena',
1673+ 'focal-xena/proposed': 'focal-proposed/xena',
1674+ 'focal-proposed/xena': 'focal-proposed/xena',
1675+ # Yoga
1676+ 'yoga': 'focal-updates/yoga',
1677+ 'focal-yoga': 'focal-updates/yoga',
1678+ 'focal-yoga/updates': 'focal-updates/yoga',
1679+ 'focal-updates/yoga': 'focal-updates/yoga',
1680+ 'yoga/proposed': 'focal-proposed/yoga',
1681+ 'focal-yoga/proposed': 'focal-proposed/yoga',
1682+ 'focal-proposed/yoga': 'focal-proposed/yoga',
1683+ # Zed
1684+ 'zed': 'jammy-updates/zed',
1685+ 'jammy-zed': 'jammy-updates/zed',
1686+ 'jammy-zed/updates': 'jammy-updates/zed',
1687+ 'jammy-updates/zed': 'jammy-updates/zed',
1688+ 'zed/proposed': 'jammy-proposed/zed',
1689+ 'jammy-zed/proposed': 'jammy-proposed/zed',
1690+ 'jammy-proposed/zed': 'jammy-proposed/zed',
1691 }
1692
1693
1694+OPENSTACK_RELEASES = (
1695+ 'diablo',
1696+ 'essex',
1697+ 'folsom',
1698+ 'grizzly',
1699+ 'havana',
1700+ 'icehouse',
1701+ 'juno',
1702+ 'kilo',
1703+ 'liberty',
1704+ 'mitaka',
1705+ 'newton',
1706+ 'ocata',
1707+ 'pike',
1708+ 'queens',
1709+ 'rocky',
1710+ 'stein',
1711+ 'train',
1712+ 'ussuri',
1713+ 'victoria',
1714+ 'wallaby',
1715+ 'xena',
1716+ 'yoga',
1717+ 'zed',
1718+)
1719+
1720+
1721+UBUNTU_OPENSTACK_RELEASE = OrderedDict([
1722+ ('oneiric', 'diablo'),
1723+ ('precise', 'essex'),
1724+ ('quantal', 'folsom'),
1725+ ('raring', 'grizzly'),
1726+ ('saucy', 'havana'),
1727+ ('trusty', 'icehouse'),
1728+ ('utopic', 'juno'),
1729+ ('vivid', 'kilo'),
1730+ ('wily', 'liberty'),
1731+ ('xenial', 'mitaka'),
1732+ ('yakkety', 'newton'),
1733+ ('zesty', 'ocata'),
1734+ ('artful', 'pike'),
1735+ ('bionic', 'queens'),
1736+ ('cosmic', 'rocky'),
1737+ ('disco', 'stein'),
1738+ ('eoan', 'train'),
1739+ ('focal', 'ussuri'),
1740+ ('groovy', 'victoria'),
1741+ ('hirsute', 'wallaby'),
1742+ ('impish', 'xena'),
1743+ ('jammy', 'yoga'),
1744+ ('kinetic', 'zed'),
1745+])
1746+
1747+
1748 APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
1749 CMD_RETRY_DELAY = 10 # Wait 10 seconds between command retries.
1750-CMD_RETRY_COUNT = 3 # Retry a failing fatal command X times.
1751+CMD_RETRY_COUNT = 10 # Retry a failing fatal command X times.
1752
1753
1754 def filter_installed_packages(packages):
1755@@ -228,9 +322,9 @@
1756 def apt_cache(*_, **__):
1757 """Shim returning an object simulating the apt_pkg Cache.
1758
1759- :param _: Accept arguments for compability, not used.
1760+ :param _: Accept arguments for compatibility, not used.
1761 :type _: any
1762- :param __: Accept keyword arguments for compability, not used.
1763+ :param __: Accept keyword arguments for compatibility, not used.
1764 :type __: any
1765 :returns:Object used to interrogate the system apt and dpkg databases.
1766 :rtype:ubuntu_apt_pkg.Cache
1767@@ -243,13 +337,19 @@
1768 # Detect this situation, log a warning and make the call to
1769 # ``apt_pkg.init()`` to avoid the consumer Python interpreter from
1770 # crashing with a segmentation fault.
1771- log('Support for use of upstream ``apt_pkg`` module in conjunction'
1772- 'with charm-helpers is deprecated since 2019-06-25', level=WARNING)
1773+ @deprecate(
1774+ 'Support for use of upstream ``apt_pkg`` module in conjunction'
1775+ 'with charm-helpers is deprecated since 2019-06-25',
1776+ date=None, log=lambda x: log(x, level=WARNING))
1777+ def one_shot_log():
1778+ pass
1779+
1780+ one_shot_log()
1781 sys.modules['apt_pkg'].init()
1782 return ubuntu_apt_pkg.Cache()
1783
1784
1785-def apt_install(packages, options=None, fatal=False):
1786+def apt_install(packages, options=None, fatal=False, quiet=False):
1787 """Install one or more packages.
1788
1789 :param packages: Package(s) to install
1790@@ -259,21 +359,27 @@
1791 :param fatal: Whether the command's output should be checked and
1792 retried.
1793 :type fatal: bool
1794+ :param quiet: if True (default), suppress log message to stdout/stderr
1795+ :type quiet: bool
1796 :raises: subprocess.CalledProcessError
1797 """
1798+ if not packages:
1799+ log("Nothing to install", level=DEBUG)
1800+ return
1801 if options is None:
1802 options = ['--option=Dpkg::Options::=--force-confold']
1803
1804 cmd = ['apt-get', '--assume-yes']
1805 cmd.extend(options)
1806 cmd.append('install')
1807- if isinstance(packages, six.string_types):
1808+ if isinstance(packages, str):
1809 cmd.append(packages)
1810 else:
1811 cmd.extend(packages)
1812- log("Installing {} with options: {}".format(packages,
1813- options))
1814- _run_apt_command(cmd, fatal)
1815+ if not quiet:
1816+ log("Installing {} with options: {}"
1817+ .format(packages, options))
1818+ _run_apt_command(cmd, fatal, quiet=quiet)
1819
1820
1821 def apt_upgrade(options=None, fatal=False, dist=False):
1822@@ -318,7 +424,7 @@
1823 :raises: subprocess.CalledProcessError
1824 """
1825 cmd = ['apt-get', '--assume-yes', 'purge']
1826- if isinstance(packages, six.string_types):
1827+ if isinstance(packages, str):
1828 cmd.append(packages)
1829 else:
1830 cmd.extend(packages)
1831@@ -345,7 +451,7 @@
1832 """Flag one or more packages using apt-mark."""
1833 log("Marking {} as {}".format(packages, mark))
1834 cmd = ['apt-mark', mark]
1835- if isinstance(packages, six.string_types):
1836+ if isinstance(packages, str):
1837 cmd.append(packages)
1838 else:
1839 cmd.extend(packages)
1840@@ -370,7 +476,7 @@
1841 A Radix64 format keyid is also supported for backwards
1842 compatibility. In this case Ubuntu keyserver will be
1843 queried for a key via HTTPS by its keyid. This method
1844- is less preferrable because https proxy servers may
1845+ is less preferable because https proxy servers may
1846 require traffic decryption which is equivalent to a
1847 man-in-the-middle attack (a proxy server impersonates
1848 keyserver TLS certificates and has to be explicitly
1849@@ -390,10 +496,7 @@
1850 if ('-----BEGIN PGP PUBLIC KEY BLOCK-----' in key and
1851 '-----END PGP PUBLIC KEY BLOCK-----' in key):
1852 log("Writing provided PGP key in the binary format", level=DEBUG)
1853- if six.PY3:
1854- key_bytes = key.encode('utf-8')
1855- else:
1856- key_bytes = key
1857+ key_bytes = key.encode('utf-8')
1858 key_name = _get_keyid_by_gpg_key(key_bytes)
1859 key_gpg = _dearmor_gpg_key(key_bytes)
1860 _write_apt_gpg_keyfile(key_name=key_name, key_material=key_gpg)
1861@@ -433,9 +536,8 @@
1862 stderr=subprocess.PIPE,
1863 stdin=subprocess.PIPE)
1864 out, err = ps.communicate(input=key_material)
1865- if six.PY3:
1866- out = out.decode('utf-8')
1867- err = err.decode('utf-8')
1868+ out = out.decode('utf-8')
1869+ err = err.decode('utf-8')
1870 if 'gpg: no valid OpenPGP data found.' in err:
1871 raise GPGKeyError('Invalid GPG key material provided')
1872 # from gnupg2 docs: fpr :: Fingerprint (fingerprint is in field 10)
1873@@ -493,8 +595,7 @@
1874 stdin=subprocess.PIPE)
1875 out, err = ps.communicate(input=key_asc)
1876 # no need to decode output as it is binary (invalid utf-8), only error
1877- if six.PY3:
1878- err = err.decode('utf-8')
1879+ err = err.decode('utf-8')
1880 if 'gpg: no valid OpenPGP data found.' in err:
1881 raise GPGKeyError('Invalid GPG key material. Check your network setup'
1882 ' (MTU, routing, DNS) and/or proxy server settings'
1883@@ -547,6 +648,10 @@
1884 with be used. If staging is NOT used then the cloud archive [3] will be
1885 added, and the 'ubuntu-cloud-keyring' package will be added for the
1886 current distro.
1887+ '<openstack-version>': translate to cloud:<release> based on the current
1888+ distro version (i.e. for 'ussuri' this will either be 'bionic-ussuri' or
1889+ 'distro'.
1890+ '<openstack-version>/proposed': as above, but for proposed.
1891
1892 Otherwise the source is not recognised and this is logged to the juju log.
1893 However, no error is raised, unless sys_error_on_exit is True.
1894@@ -565,7 +670,7 @@
1895 id may also be used, but be aware that only insecure protocols are
1896 available to retrieve the actual public key from a public keyserver
1897 placing your Juju environment at risk. ppa and cloud archive keys
1898- are securely added automtically, so sould not be provided.
1899+ are securely added automatically, so should not be provided.
1900
1901 @param fail_invalid: (boolean) if True, then the function raises a
1902 SourceConfigError is there is no matching installation source.
1903@@ -573,6 +678,12 @@
1904 @raises SourceConfigError() if for cloud:<pocket>, the <pocket> is not a
1905 valid pocket in CLOUD_ARCHIVE_POCKETS
1906 """
1907+ # extract the OpenStack versions from the CLOUD_ARCHIVE_POCKETS; can't use
1908+ # the list in contrib.openstack.utils as it might not be included in
1909+ # classic charms and would break everything. Having OpenStack specific
1910+ # code in this file is a bit of an antipattern, anyway.
1911+ os_versions_regex = "({})".format("|".join(OPENSTACK_RELEASES))
1912+
1913 _mapping = OrderedDict([
1914 (r"^distro$", lambda: None), # This is a NOP
1915 (r"^(?:proposed|distro-proposed)$", _add_proposed),
1916@@ -582,10 +693,13 @@
1917 (r"^cloud:(.*)-(.*)$", _add_cloud_distro_check),
1918 (r"^cloud:(.*)$", _add_cloud_pocket),
1919 (r"^snap:.*-(.*)-(.*)$", _add_cloud_distro_check),
1920+ (r"^{}\/proposed$".format(os_versions_regex),
1921+ _add_bare_openstack_proposed),
1922+ (r"^{}$".format(os_versions_regex), _add_bare_openstack),
1923 ])
1924 if source is None:
1925 source = ''
1926- for r, fn in six.iteritems(_mapping):
1927+ for r, fn in _mapping.items():
1928 m = re.match(r, source)
1929 if m:
1930 if key:
1931@@ -613,12 +727,12 @@
1932 Uses get_distrib_codename to determine the correct stanza for
1933 the deb line.
1934
1935- For intel architecutres PROPOSED_POCKET is used for the release, but for
1936+ For Intel architectures PROPOSED_POCKET is used for the release, but for
1937 other architectures PROPOSED_PORTS_POCKET is used for the release.
1938 """
1939 release = get_distrib_codename()
1940 arch = platform.machine()
1941- if arch not in six.iterkeys(ARCH_TO_PROPOSED_POCKET):
1942+ if arch not in ARCH_TO_PROPOSED_POCKET.keys():
1943 raise SourceConfigError("Arch {} not supported for (distro-)proposed"
1944 .format(arch))
1945 with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
1946@@ -634,11 +748,9 @@
1947 if '{series}' in spec:
1948 series = get_distrib_codename()
1949 spec = spec.replace('{series}', series)
1950- # software-properties package for bionic properly reacts to proxy settings
1951- # passed as environment variables (See lp:1433761). This is not the case
1952- # LTS and non-LTS releases below bionic.
1953 _run_with_retries(['add-apt-repository', '--yes', spec],
1954- cmd_env=env_proxy_settings(['https']))
1955+ cmd_env=env_proxy_settings(['https', 'http', 'no_proxy'])
1956+ )
1957
1958
1959 def _add_cloud_pocket(pocket):
1960@@ -714,8 +826,75 @@
1961 'version ({})'.format(release, os_release, ubuntu_rel))
1962
1963
1964+def _add_bare_openstack(openstack_release):
1965+ """Add cloud or distro based on the release given.
1966+
1967+ The spec given is, say, 'ussuri', but this could apply cloud:bionic-ussuri
1968+ or 'distro' depending on whether the ubuntu release is bionic or focal.
1969+
1970+ :param openstack_release: the OpenStack codename to determine the release
1971+ for.
1972+ :type openstack_release: str
1973+ :raises: SourceConfigError
1974+ """
1975+ # TODO(ajkavanagh) - surely this means we should be removing cloud archives
1976+ # if they exist?
1977+ __add_bare_helper(openstack_release, "{}-{}", lambda: None)
1978+
1979+
1980+def _add_bare_openstack_proposed(openstack_release):
1981+ """Add cloud of distro but with proposed.
1982+
1983+ The spec given is, say, 'ussuri' but this could apply
1984+ cloud:bionic-ussuri/proposed or 'distro/proposed' depending on whether the
1985+ ubuntu release is bionic or focal.
1986+
1987+ :param openstack_release: the OpenStack codename to determine the release
1988+ for.
1989+ :type openstack_release: str
1990+ :raises: SourceConfigError
1991+ """
1992+ __add_bare_helper(openstack_release, "{}-{}/proposed", _add_proposed)
1993+
1994+
1995+def __add_bare_helper(openstack_release, pocket_format, final_function):
1996+ """Helper for _add_bare_openstack[_proposed]
1997+
1998+ The bulk of the work between the two functions is exactly the same except
1999+ for the pocket format and the function that is run if it's the distro
2000+ version.
2001+
2002+ :param openstack_release: the OpenStack codename. e.g. ussuri
2003+ :type openstack_release: str
2004+ :param pocket_format: the pocket formatter string to construct a pocket str
2005+ from the openstack_release and the current ubuntu version.
2006+ :type pocket_format: str
2007+ :param final_function: the function to call if it is the distro version.
2008+ :type final_function: Callable
2009+ :raises SourceConfigError on error
2010+ """
2011+ ubuntu_version = get_distrib_codename()
2012+ possible_pocket = pocket_format.format(ubuntu_version, openstack_release)
2013+ if possible_pocket in CLOUD_ARCHIVE_POCKETS:
2014+ _add_cloud_pocket(possible_pocket)
2015+ return
2016+ # Otherwise it's almost certainly the distro version; verify that it
2017+ # exists.
2018+ try:
2019+ assert UBUNTU_OPENSTACK_RELEASE[ubuntu_version] == openstack_release
2020+ except KeyError:
2021+ raise SourceConfigError(
2022+ "Invalid ubuntu version {} isn't known to this library"
2023+ .format(ubuntu_version))
2024+ except AssertionError:
2025+ raise SourceConfigError(
2026+ 'Invalid OpenStack release specified: {} for Ubuntu version {}'
2027+ .format(openstack_release, ubuntu_version))
2028+ final_function()
2029+
2030+
2031 def _run_with_retries(cmd, max_retries=CMD_RETRY_COUNT, retry_exitcodes=(1,),
2032- retry_message="", cmd_env=None):
2033+ retry_message="", cmd_env=None, quiet=False):
2034 """Run a command and retry until success or max_retries is reached.
2035
2036 :param cmd: The apt command to run.
2037@@ -730,11 +909,19 @@
2038 :type retry_message: str
2039 :param: cmd_env: Environment variables to add to the command run.
2040 :type cmd_env: Option[None, Dict[str, str]]
2041+ :param quiet: if True, silence the output of the command from stdout and
2042+ stderr
2043+ :type quiet: bool
2044 """
2045 env = get_apt_dpkg_env()
2046 if cmd_env:
2047 env.update(cmd_env)
2048
2049+ kwargs = {}
2050+ if quiet:
2051+ kwargs['stdout'] = subprocess.DEVNULL
2052+ kwargs['stderr'] = subprocess.DEVNULL
2053+
2054 if not retry_message:
2055 retry_message = "Failed executing '{}'".format(" ".join(cmd))
2056 retry_message += ". Will retry in {} seconds".format(CMD_RETRY_DELAY)
2057@@ -745,7 +932,7 @@
2058 retry_results = (None,) + retry_exitcodes
2059 while result in retry_results:
2060 try:
2061- result = subprocess.check_call(cmd, env=env)
2062+ result = subprocess.check_call(cmd, env=env, **kwargs)
2063 except subprocess.CalledProcessError as e:
2064 retry_count = retry_count + 1
2065 if retry_count > max_retries:
2066@@ -755,7 +942,7 @@
2067 time.sleep(CMD_RETRY_DELAY)
2068
2069
2070-def _run_apt_command(cmd, fatal=False):
2071+def _run_apt_command(cmd, fatal=False, quiet=False):
2072 """Run an apt command with optional retries.
2073
2074 :param cmd: The apt command to run.
2075@@ -763,13 +950,21 @@
2076 :param fatal: Whether the command's output should be checked and
2077 retried.
2078 :type fatal: bool
2079+ :param quiet: if True, silence the output of the command from stdout and
2080+ stderr
2081+ :type quiet: bool
2082 """
2083 if fatal:
2084 _run_with_retries(
2085 cmd, retry_exitcodes=(1, APT_NO_LOCK,),
2086- retry_message="Couldn't acquire DPKG lock")
2087+ retry_message="Couldn't acquire DPKG lock",
2088+ quiet=quiet)
2089 else:
2090- subprocess.call(cmd, env=get_apt_dpkg_env())
2091+ kwargs = {}
2092+ if quiet:
2093+ kwargs['stdout'] = subprocess.DEVNULL
2094+ kwargs['stderr'] = subprocess.DEVNULL
2095+ subprocess.call(cmd, env=get_apt_dpkg_env(), **kwargs)
2096
2097
2098 def get_upstream_version(package):
2099@@ -791,6 +986,22 @@
2100 return ubuntu_apt_pkg.upstream_version(pkg.current_ver.ver_str)
2101
2102
2103+def get_installed_version(package):
2104+ """Determine installed version of a package
2105+
2106+ @returns None (if not installed) or the installed version as
2107+ Version object
2108+ """
2109+ cache = apt_cache()
2110+ dpkg_result = cache.dpkg_list([package]).get(package, {})
2111+ current_ver = None
2112+ installed_version = dpkg_result.get('version')
2113+
2114+ if installed_version:
2115+ current_ver = ubuntu_apt_pkg.Version({'ver_str': installed_version})
2116+ return current_ver
2117+
2118+
2119 def get_apt_dpkg_env():
2120 """Get environment suitable for execution of APT and DPKG tools.
2121
2122
2123=== modified file 'hooks/charmhelpers/fetch/ubuntu_apt_pkg.py'
2124--- hooks/charmhelpers/fetch/ubuntu_apt_pkg.py 2020-02-19 23:47:52 +0000
2125+++ hooks/charmhelpers/fetch/ubuntu_apt_pkg.py 2022-09-01 15:45:51 +0000
2126@@ -1,4 +1,4 @@
2127-# Copyright 2019 Canonical Ltd
2128+# Copyright 2019-2021 Canonical Ltd
2129 #
2130 # Licensed under the Apache License, Version 2.0 (the "License");
2131 # you may not use this file except in compliance with the License.
2132@@ -40,6 +40,9 @@
2133 import subprocess
2134 import sys
2135
2136+from charmhelpers import deprecate
2137+from charmhelpers.core.hookenv import log
2138+
2139
2140 class _container(dict):
2141 """Simple container for attributes."""
2142@@ -79,7 +82,7 @@
2143 apt_result = self._apt_cache_show([package])[package]
2144 apt_result['name'] = apt_result.pop('package')
2145 pkg = Package(apt_result)
2146- dpkg_result = self._dpkg_list([package]).get(package, {})
2147+ dpkg_result = self.dpkg_list([package]).get(package, {})
2148 current_ver = None
2149 installed_version = dpkg_result.get('version')
2150 if installed_version:
2151@@ -88,9 +91,29 @@
2152 pkg.architecture = dpkg_result.get('architecture')
2153 return pkg
2154
2155+ @deprecate("use dpkg_list() instead.", "2022-05", log=log)
2156 def _dpkg_list(self, packages):
2157+ return self.dpkg_list(packages)
2158+
2159+ def dpkg_list(self, packages):
2160 """Get data from system dpkg database for package.
2161
2162+ Note that this method is also useful for querying package names
2163+ containing wildcards, for example
2164+
2165+ apt_cache().dpkg_list(['nvidia-vgpu-ubuntu-*'])
2166+
2167+ may return
2168+
2169+ {
2170+ 'nvidia-vgpu-ubuntu-470': {
2171+ 'name': 'nvidia-vgpu-ubuntu-470',
2172+ 'version': '470.68',
2173+ 'architecture': 'amd64',
2174+ 'description': 'NVIDIA vGPU driver - version 470.68'
2175+ }
2176+ }
2177+
2178 :param packages: Packages to get data from
2179 :type packages: List[str]
2180 :returns: Structured data about installed packages, keys like
2181@@ -129,7 +152,7 @@
2182 else:
2183 data = line.split(None, 4)
2184 status = data.pop(0)
2185- if status != 'ii':
2186+ if status not in ('ii', 'hi'):
2187 continue
2188 pkg = {}
2189 pkg.update({k.lower(): v for k, v in zip(headings, data)})
2190@@ -209,7 +232,7 @@
2191
2192
2193 def init():
2194- """Compability shim that does nothing."""
2195+ """Compatibility shim that does nothing."""
2196 pass
2197
2198
2199@@ -264,4 +287,49 @@
2200 else:
2201 raise RuntimeError('Unable to compare "{}" and "{}", according to '
2202 'our logic they are neither greater, equal nor '
2203- 'less than each other.')
2204+ 'less than each other.'.format(a, b))
2205+
2206+
2207+class PkgVersion():
2208+ """Allow package versions to be compared.
2209+
2210+ For example::
2211+
2212+ >>> import charmhelpers.fetch as fetch
2213+ >>> (fetch.apt_pkg.PkgVersion('2:20.4.0') <
2214+ ... fetch.apt_pkg.PkgVersion('2:20.5.0'))
2215+ True
2216+ >>> pkgs = [fetch.apt_pkg.PkgVersion('2:20.4.0'),
2217+ ... fetch.apt_pkg.PkgVersion('2:21.4.0'),
2218+ ... fetch.apt_pkg.PkgVersion('2:17.4.0')]
2219+ >>> pkgs.sort()
2220+ >>> pkgs
2221+ [2:17.4.0, 2:20.4.0, 2:21.4.0]
2222+ """
2223+
2224+ def __init__(self, version):
2225+ self.version = version
2226+
2227+ def __lt__(self, other):
2228+ return version_compare(self.version, other.version) == -1
2229+
2230+ def __le__(self, other):
2231+ return self.__lt__(other) or self.__eq__(other)
2232+
2233+ def __gt__(self, other):
2234+ return version_compare(self.version, other.version) == 1
2235+
2236+ def __ge__(self, other):
2237+ return self.__gt__(other) or self.__eq__(other)
2238+
2239+ def __eq__(self, other):
2240+ return version_compare(self.version, other.version) == 0
2241+
2242+ def __ne__(self, other):
2243+ return not self.__eq__(other)
2244+
2245+ def __repr__(self):
2246+ return self.version
2247+
2248+ def __hash__(self):
2249+ return hash(repr(self))
2250
2251=== modified file 'hooks/charmhelpers/osplatform.py'
2252--- hooks/charmhelpers/osplatform.py 2020-02-19 23:47:52 +0000
2253+++ hooks/charmhelpers/osplatform.py 2022-09-01 15:45:51 +0000
2254@@ -28,6 +28,9 @@
2255 elif "elementary" in current_platform:
2256 # ElementaryOS fails to run tests locally without this.
2257 return "ubuntu"
2258+ elif "Pop!_OS" in current_platform:
2259+ # Pop!_OS also fails to run tests locally without this.
2260+ return "ubuntu"
2261 else:
2262 raise RuntimeError("This module is not supported on {}."
2263 .format(current_platform))
2264
2265=== modified file 'metadata.yaml'
2266--- metadata.yaml 2020-02-19 17:40:49 +0000
2267+++ metadata.yaml 2022-09-01 15:45:51 +0000
2268@@ -12,6 +12,7 @@
2269 - xenial
2270 - bionic
2271 - focal
2272+ - jammy
2273 tags: [ ops, monitoring ]
2274 requires:
2275 container:

Subscribers

People subscribed via source and target branches

to all changes: