diff --git a/Makefile b/Makefile index 781d2299..8b1f157b 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ #!/usr/bin/make -PYTHON := /usr/bin/env python +PYTHON := /usr/bin/env python3 lint: @tox -e pep8 diff --git a/charmhelpers/contrib/openstack/policyd.py b/charmhelpers/contrib/openstack/policyd.py index d89d2cca..f2bb21e9 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 161199c4..5c8f6eff 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/openstack/vaultlocker.py b/charmhelpers/contrib/openstack/vaultlocker.py index c162de27..866a2697 100644 --- a/charmhelpers/contrib/openstack/vaultlocker.py +++ b/charmhelpers/contrib/openstack/vaultlocker.py @@ -37,7 +37,19 @@ class VaultKVContext(context.OSContextGenerator): ) def __call__(self): - import hvac + try: + import hvac + except ImportError: + # BUG: #1862085 - if the relation is made to vault, but the + # 'encrypt' option is not made, then the charm errors with an + # import warning. This catches that, logs a warning, and returns + # with an empty context. + hookenv.log("VaultKVContext: trying to use hvac pythong module " + "but it's not available. Is secrets-stroage relation " + "made, but encrypt option not set?", + level=hookenv.WARNING) + # return an emptry context on hvac import error + return {} ctxt = {} # NOTE(hopem): see https://bugs.launchpad.net/charm-helpers/+bug/1849323 db = unitdata.kv() diff --git a/charmhelpers/contrib/storage/linux/ceph.py b/charmhelpers/contrib/storage/linux/ceph.py index 104977af..dabfb6c2 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/core/host_factory/ubuntu.py b/charmhelpers/core/host_factory/ubuntu.py index 1b57e2ce..3edc0687 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 c7fd1363..78c81af5 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"] diff --git a/metadata.yaml b/metadata.yaml index da9c3111..910fef6f 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -13,6 +13,7 @@ series: - xenial - bionic - eoan + - focal - trusty extra-bindings: public: diff --git a/tests/bundles/bionic-ussuri.yaml b/tests/bundles/bionic-ussuri.yaml new file mode 100644 index 00000000..79dabedf --- /dev/null +++ b/tests/bundles/bionic-ussuri.yaml @@ -0,0 +1,40 @@ +series: bionic +comment: +- 'machines section to decide order of deployment. database sooner = faster' +machines: + '0': + constraints: mem=3072M + '1': + '2': + '3': + '4': +relations: +- - keystone:shared-db + - mysql:shared-db +- - glance:shared-db + - mysql:shared-db +- - glance:identity-service + - keystone:identity-service +applications: + mysql: + charm: cs:~openstack-charmers-next/percona-cluster + num_units: 1 + to: + - '0' + keystone: + charm: ../../../keystone + num_units: 3 + options: + openstack-origin: cloud:bionic-ussuri + token-expiration: 60 + to: + - '1' + - '2' + - '3' + glance: + charm: cs:~openstack-charmers-next/glance + num_units: 1 + options: + openstack-origin: cloud:bionic-ussuri + to: + - '4' diff --git a/tests/bundles/focal-ussuri.yaml b/tests/bundles/focal-ussuri.yaml new file mode 100644 index 00000000..1c3e57be --- /dev/null +++ b/tests/bundles/focal-ussuri.yaml @@ -0,0 +1,52 @@ +series: focal +comment: +- 'machines section to decide order of deployment. database sooner = faster' +machines: + '0': + constraints: mem=3072M + '1': + constraints: mem=3072M + '2': + constraints: mem=3072M + '3': + '4': + '5': + '6': +relations: +- - keystone:shared-db + - keystone-mysql-router:shared-db +- - glance:shared-db + - glance-mysql-router:shared-db +- - glance:identity-service + - keystone:identity-service +- - glance-mysql-router:db-router + - mysql-innodb-cluster:db-router +- - keystone-mysql-router:db-router + - mysql-innodb-cluster:db-router +applications: + glance-mysql-router: + charm: cs:~openstack-charmers-next/mysql-router + keystone-mysql-router: + charm: cs:~openstack-charmers-next/mysql-router + mysql-innodb-cluster: + charm: cs:~openstack-charmers-next/mysql-innodb-cluster + num_units: 3 + to: + - '0' + - '1' + - '2' + keystone: + charm: ../../../keystone + num_units: 3 + options: + token-provider: 'fernet' + token-expiration: 60 + to: + - '3' + - '4' + - '5' + glance: + charm: cs:~openstack-charmers-next/glance + num_units: 1 + to: + - '6' diff --git a/tests/tests.yaml b/tests/tests.yaml index 05b0f8a6..9a2fdcab 100644 --- a/tests/tests.yaml +++ b/tests/tests.yaml @@ -2,6 +2,8 @@ charm_name: keystone smoke_bundles: - bionic-train gate_bundles: +- focal-ussuri +- bionic-ussuri - bionic-train - bionic-stein - bionic-rocky @@ -11,6 +13,8 @@ gate_bundles: - xenial-ocata - xenial-mitaka - trusty-mitaka +dev_bundles: +- focal-ussuri comment: | the glance configure job validates operation of identity-service relation. The policyd test is generic and validates the policy.d overrides work @@ -25,3 +29,5 @@ tests: tests_options: policyd: service: keystone + force_deploy: + - focal-ussuri