Merge lp:~silverdrake11/landscape-client-charm/jammy_series into lp:landscape-client-charm
- jammy_series
- Merge into trunk
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 | ||||
Related bugs: |
|
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-
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
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: |
+1 lgtm