From c709be563aaced5deab22114b39a46716ff3fc14 Mon Sep 17 00:00:00 2001 From: Alex Kavanagh Date: Mon, 30 Mar 2020 11:42:08 +0100 Subject: [PATCH] Sync charm-helpers for py38/focal pre-support This is to enable other charms to pass there enable-focal patchsets. The pre-enable is simply to sync in charmhelpers with py38 support so that this charm can participate in swift-proxy's focal enablement. Change-Id: Ic94761fd65e9f442fa08315bbbdc377d7d0a97c9 --- charmhelpers/contrib/network/ufw.py | 11 +- charmhelpers/contrib/openstack/policyd.py | 23 ++-- charmhelpers/contrib/openstack/utils.py | 113 ++++++++++++++++-- charmhelpers/contrib/storage/linux/ceph.py | 2 +- .../contrib/storage/linux/loopback.py | 8 +- charmhelpers/core/hookenv.py | 16 +-- charmhelpers/core/host_factory/ubuntu.py | 1 + charmhelpers/osplatform.py | 24 +++- 8 files changed, 159 insertions(+), 39 deletions(-) diff --git a/charmhelpers/contrib/network/ufw.py b/charmhelpers/contrib/network/ufw.py index b620ba2..b9bf7c9 100644 --- a/charmhelpers/contrib/network/ufw.py +++ b/charmhelpers/contrib/network/ufw.py @@ -37,7 +37,6 @@ Examples: """ import os import re -import six import subprocess from charmhelpers.core import hookenv @@ -366,12 +365,10 @@ def status(): (1, {'to': '', 'action':, 'from':, '', ipv6: True, 'comment': ''}) :rtype: Iterator[Tuple[int, Dict[str, Union[bool, str]]]] """ - if six.PY2: - raise RuntimeError('Call to function not supported on Python2') - cp = subprocess.run(('ufw', 'status', 'numbered',), - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - check=True, universal_newlines=True) - for line in cp.stdout.splitlines(): + cp = subprocess.check_output(('ufw', 'status', 'numbered',), + stderr=subprocess.STDOUT, + universal_newlines=True) + for line in cp.splitlines(): if not line.startswith('['): continue ipv6 = True if '(v6)' in line else False diff --git a/charmhelpers/contrib/openstack/policyd.py b/charmhelpers/contrib/openstack/policyd.py index d89d2cc..f2bb21e 100644 --- a/charmhelpers/contrib/openstack/policyd.py +++ b/charmhelpers/contrib/openstack/policyd.py @@ -17,7 +17,6 @@ import contextlib import os import six import shutil -import sys import yaml import zipfile @@ -531,7 +530,7 @@ def clean_policyd_dir_for(service, keep_paths=None, user=None, group=None): hookenv.log("Cleaning path: {}".format(path), level=hookenv.DEBUG) if not os.path.exists(path): ch_host.mkdir(path, owner=_user, group=_group, perms=0o775) - _scanner = os.scandir if sys.version_info > (3, 4) else _py2_scandir + _scanner = os.scandir if hasattr(os, 'scandir') else _fallback_scandir for direntry in _scanner(path): # see if the path should be kept. if direntry.path in keep_paths: @@ -560,23 +559,25 @@ def maybe_create_directory_for(path, user, group): @contextlib.contextmanager -def _py2_scandir(path): - """provide a py2 implementation of os.scandir if this module ever gets used - in a py2 charm (unlikely). uses os.listdir() to get the names in the path, - and then mocks the is_dir() function using os.path.isdir() to check for a +def _fallback_scandir(path): + """Fallback os.scandir implementation. + + provide a fallback implementation of os.scandir if this module ever gets + used in a py2 or py34 charm. Uses os.listdir() to get the names in the path, + and then mocks the is_dir() function using os.path.isdir() to check for directory. :param path: the path to list the directories for :type path: str - :returns: Generator that provides _P27Direntry objects - :rtype: ContextManager[_P27Direntry] + :returns: Generator that provides _FBDirectory objects + :rtype: ContextManager[_FBDirectory] """ for f in os.listdir(path): - yield _P27Direntry(f) + yield _FBDirectory(f) -class _P27Direntry(object): - """Mock a scandir Direntry object with enough to use in +class _FBDirectory(object): + """Mock a scandir Directory object with enough to use in clean_policyd_dir_for """ diff --git a/charmhelpers/contrib/openstack/utils.py b/charmhelpers/contrib/openstack/utils.py index 161199c..5c8f6ef 100644 --- a/charmhelpers/contrib/openstack/utils.py +++ b/charmhelpers/contrib/openstack/utils.py @@ -278,7 +278,7 @@ PACKAGE_CODENAMES = { ('14', 'rocky'), ('15', 'stein'), ('16', 'train'), - ('17', 'ussuri'), + ('18', 'ussuri'), ]), 'ceilometer-common': OrderedDict([ ('5', 'liberty'), @@ -326,7 +326,7 @@ PACKAGE_CODENAMES = { ('14', 'rocky'), ('15', 'stein'), ('16', 'train'), - ('17', 'ussuri'), + ('18', 'ussuri'), ]), } @@ -555,9 +555,8 @@ def reset_os_release(): _os_rel = None -def os_release(package, base=None, reset_cache=False): - ''' - Returns OpenStack release codename from a cached global. +def os_release(package, base=None, reset_cache=False, source_key=None): + """Returns OpenStack release codename from a cached global. If reset_cache then unset the cached os_release version and return the freshly determined version. @@ -565,7 +564,20 @@ def os_release(package, base=None, reset_cache=False): If the codename can not be determined from either an installed package or the installation source, the earliest release supported by the charm should be returned. - ''' + + :param package: Name of package to determine release from + :type package: str + :param base: Fallback codename if endavours to determine from package fail + :type base: Optional[str] + :param reset_cache: Reset any cached codename value + :type reset_cache: bool + :param source_key: Name of source configuration option + (default: 'openstack-origin') + :type source_key: Optional[str] + :returns: OpenStack release codename + :rtype: str + """ + source_key = source_key or 'openstack-origin' if not base: base = UBUNTU_OPENSTACK_RELEASE[lsb_release()['DISTRIB_CODENAME']] global _os_rel @@ -575,7 +587,7 @@ def os_release(package, base=None, reset_cache=False): return _os_rel _os_rel = ( get_os_codename_package(package, fatal=False) or - get_os_codename_install_source(config('openstack-origin')) or + get_os_codename_install_source(config(source_key)) or base) return _os_rel @@ -658,6 +670,93 @@ def config_value_changed(option): return current != saved +def get_endpoint_key(service_name, relation_id, unit_name): + """Return the key used to refer to an ep changed notification from a unit. + + :param service_name: Service name eg nova, neutron, placement etc + :type service_name: str + :param relation_id: The id of the relation the unit is on. + :type relation_id: str + :param unit_name: The name of the unit publishing the notification. + :type unit_name: str + :returns: The key used to refer to an ep changed notification from a unit + :rtype: str + """ + return '{}-{}-{}'.format( + service_name, + relation_id.replace(':', '_'), + unit_name.replace('/', '_')) + + +def get_endpoint_notifications(service_names, rel_name='identity-service'): + """Return all notifications for the given services. + + :param service_names: List of service name. + :type service_name: List + :param rel_name: Name of the relation to query + :type rel_name: str + :returns: A dict containing the source of the notification and its nonce. + :rtype: Dict[str, str] + """ + notifications = {} + for rid in relation_ids(rel_name): + for unit in related_units(relid=rid): + ep_changed_json = relation_get( + rid=rid, + unit=unit, + attribute='ep_changed') + if ep_changed_json: + ep_changed = json.loads(ep_changed_json) + for service in service_names: + if ep_changed.get(service): + key = get_endpoint_key(service, rid, unit) + notifications[key] = ep_changed[service] + return notifications + + +def endpoint_changed(service_name, rel_name='identity-service'): + """Whether a new notification has been recieved for an endpoint. + + :param service_name: Service name eg nova, neutron, placement etc + :type service_name: str + :param rel_name: Name of the relation to query + :type rel_name: str + :returns: Whether endpoint has changed + :rtype: bool + """ + changed = False + with unitdata.HookData()() as t: + db = t[0] + notifications = get_endpoint_notifications( + [service_name], + rel_name=rel_name) + for key, nonce in notifications.items(): + if db.get(key) != nonce: + juju_log(('New endpoint change notification found: ' + '{}={}').format(key, nonce), + 'INFO') + changed = True + break + return changed + + +def save_endpoint_changed_triggers(service_names, rel_name='identity-service'): + """Save the enpoint triggers in db so it can be tracked if they changed. + + :param service_names: List of service name. + :type service_name: List + :param rel_name: Name of the relation to query + :type rel_name: str + """ + with unitdata.HookData()() as t: + db = t[0] + notifications = get_endpoint_notifications( + service_names, + rel_name=rel_name) + for key, nonce in notifications.items(): + db.set(key, nonce) + + def save_script_rc(script_path="scripts/scriptrc", **env_vars): """ Write an rc file in the charm-delivered directory containing diff --git a/charmhelpers/contrib/storage/linux/ceph.py b/charmhelpers/contrib/storage/linux/ceph.py index 104977a..dabfb6c 100644 --- a/charmhelpers/contrib/storage/linux/ceph.py +++ b/charmhelpers/contrib/storage/linux/ceph.py @@ -1042,7 +1042,7 @@ def filesystem_mounted(fs): def make_filesystem(blk_device, fstype='ext4', timeout=10): """Make a new filesystem on the specified block device.""" count = 0 - e_noent = os.errno.ENOENT + e_noent = errno.ENOENT while not os.path.exists(blk_device): if count >= timeout: log('Gave up waiting on block device %s' % blk_device, diff --git a/charmhelpers/contrib/storage/linux/loopback.py b/charmhelpers/contrib/storage/linux/loopback.py index 82472ff..74bab40 100644 --- a/charmhelpers/contrib/storage/linux/loopback.py +++ b/charmhelpers/contrib/storage/linux/loopback.py @@ -32,6 +32,10 @@ def loopback_devices(): /dev/loop0: [0807]:961814 (/tmp/my.img) + or: + + /dev/loop0: [0807]:961814 (/tmp/my.img (deleted)) + :returns: dict: a dict mapping {loopback_dev: backing_file} ''' loopbacks = {} @@ -39,9 +43,9 @@ def loopback_devices(): output = check_output(cmd) if six.PY3: output = output.decode('utf-8') - devs = [d.strip().split(' ') for d in output.splitlines() if d != ''] + devs = [d.strip().split(' ', 2) for d in output.splitlines() if d != ''] for dev, _, f in devs: - loopbacks[dev.replace(':', '')] = re.search(r'\((\S+)\)', f).groups()[0] + loopbacks[dev.replace(':', '')] = re.search(r'\((.+)\)', f).groups()[0] return loopbacks diff --git a/charmhelpers/core/hookenv.py b/charmhelpers/core/hookenv.py index 647f6e4..56adbc9 100644 --- a/charmhelpers/core/hookenv.py +++ b/charmhelpers/core/hookenv.py @@ -1093,7 +1093,7 @@ def status_set(workload_state, message): Use status-set to set the workload state with a message which is visible to the user via juju status. If the status-set command is not found then - assume this is juju < 1.23 and juju-log the message unstead. + assume this is juju < 1.23 and juju-log the message instead. workload_state -- valid juju workload state. message -- status update message @@ -1526,13 +1526,13 @@ def env_proxy_settings(selected_settings=None): """Get proxy settings from process environment variables. Get charm proxy settings from environment variables that correspond to - juju-http-proxy, juju-https-proxy and juju-no-proxy (available as of 2.4.2, - see lp:1782236) in a format suitable for passing to an application that - reacts to proxy settings passed as environment variables. Some applications - support lowercase or uppercase notation (e.g. curl), some support only - lowercase (e.g. wget), there are also subjectively rare cases of only - uppercase notation support. no_proxy CIDR and wildcard support also varies - between runtimes and applications as there is no enforced standard. + juju-http-proxy, juju-https-proxy juju-no-proxy (available as of 2.4.2, see + lp:1782236) and juju-ftp-proxy in a format suitable for passing to an + application that reacts to proxy settings passed as environment variables. + Some applications support lowercase or uppercase notation (e.g. curl), some + support only lowercase (e.g. wget), there are also subjectively rare cases + of only uppercase notation support. no_proxy CIDR and wildcard support also + varies between runtimes and applications as there is no enforced standard. Some applications may connect to multiple destinations and expose config options that would affect only proxy settings for a specific destination diff --git a/charmhelpers/core/host_factory/ubuntu.py b/charmhelpers/core/host_factory/ubuntu.py index 1b57e2c..3edc068 100644 --- a/charmhelpers/core/host_factory/ubuntu.py +++ b/charmhelpers/core/host_factory/ubuntu.py @@ -25,6 +25,7 @@ UBUNTU_RELEASES = ( 'cosmic', 'disco', 'eoan', + 'focal' ) diff --git a/charmhelpers/osplatform.py b/charmhelpers/osplatform.py index c7fd136..78c81af 100644 --- a/charmhelpers/osplatform.py +++ b/charmhelpers/osplatform.py @@ -1,4 +1,5 @@ import platform +import os def get_platform(): @@ -9,9 +10,13 @@ def get_platform(): This string is used to decide which platform module should be imported. """ # linux_distribution is deprecated and will be removed in Python 3.7 - # Warings *not* disabled, as we certainly need to fix this. - tuple_platform = platform.linux_distribution() - current_platform = tuple_platform[0] + # Warnings *not* disabled, as we certainly need to fix this. + if hasattr(platform, 'linux_distribution'): + tuple_platform = platform.linux_distribution() + current_platform = tuple_platform[0] + else: + current_platform = _get_platform_from_fs() + if "Ubuntu" in current_platform: return "ubuntu" elif "CentOS" in current_platform: @@ -26,3 +31,16 @@ def get_platform(): else: raise RuntimeError("This module is not supported on {}." .format(current_platform)) + + +def _get_platform_from_fs(): + """Get Platform from /etc/os-release.""" + with open(os.path.join(os.sep, 'etc', 'os-release')) as fin: + content = dict( + line.split('=', 1) + for line in fin.read().splitlines() + if '=' in line + ) + for k, v in content.items(): + content[k] = v.strip('"') + return content["NAME"]