From 9e020aac19caa034569130c64815ba50224b22ac Mon Sep 17 00:00:00 2001 From: David Ames Date: Tue, 7 Mar 2017 11:05:47 -0800 Subject: [PATCH] Enable Ocata Amulet Tests - Add Zesty as a supported series to metadata.yaml. - Turn on Xenial-Ocata Amulet test definitions. - Sync charm helpers to get Juju 2.x amulet compatibility. - Keeping Zesty-Ocata Amulet test definitions turned off until the metadata.yaml changes propagate to the charm store. - Resync tox.ini to resolve amulet test failures. Change-Id: Id0c204f273caa9f0f585dcf505975ba685349ad0 --- .../charmhelpers/contrib/charmsupport/nrpe.py | 32 +++-- .../contrib/hardening/templating.py | 6 +- hooks/charmhelpers/contrib/network/ip.py | 65 +++++++++- hooks/charmhelpers/contrib/openstack/utils.py | 2 +- hooks/charmhelpers/contrib/python/packages.py | 11 +- hooks/charmhelpers/core/host.py | 2 + hooks/charmhelpers/fetch/snap.py | 122 ++++++++++++++++++ hooks/charmhelpers/fetch/ubuntu.py | 80 +++++++----- metadata.yaml | 1 + tests/basic_deployment.py | 16 ++- tests/charmhelpers/contrib/amulet/utils.py | 29 ++--- .../contrib/openstack/amulet/utils.py | 12 +- tests/charmhelpers/core/host.py | 2 + tests/gate-basic-xenial-ocata | 0 tox.ini | 7 +- 15 files changed, 313 insertions(+), 74 deletions(-) create mode 100644 hooks/charmhelpers/fetch/snap.py mode change 100644 => 100755 tests/gate-basic-xenial-ocata diff --git a/hooks/charmhelpers/contrib/charmsupport/nrpe.py b/hooks/charmhelpers/contrib/charmsupport/nrpe.py index 1410512..9646b83 100644 --- a/hooks/charmhelpers/contrib/charmsupport/nrpe.py +++ b/hooks/charmhelpers/contrib/charmsupport/nrpe.py @@ -227,6 +227,7 @@ class NRPE(object): nagios_logdir = '/var/log/nagios' nagios_exportdir = '/var/lib/nagios/export' nrpe_confdir = '/etc/nagios/nrpe.d' + homedir = '/var/lib/nagios' # home dir provided by nagios-nrpe-server def __init__(self, hostname=None, primary=True): super(NRPE, self).__init__() @@ -338,13 +339,14 @@ def get_nagios_unit_name(relation_name='nrpe-external-master'): return unit -def add_init_service_checks(nrpe, services, unit_name): +def add_init_service_checks(nrpe, services, unit_name, immediate_check=True): """ Add checks for each service in list :param NRPE nrpe: NRPE object to add check to :param list services: List of services to check :param str unit_name: Unit name to use in check description + :param bool immediate_check: For sysv init, run the service check immediately """ for svc in services: # Don't add a check for these services from neutron-gateway @@ -368,21 +370,31 @@ def add_init_service_checks(nrpe, services, unit_name): ) elif os.path.exists(sysv_init): cronpath = '/etc/cron.d/nagios-service-check-%s' % svc - cron_file = ('*/5 * * * * root ' - '/usr/local/lib/nagios/plugins/check_exit_status.pl ' - '-s /etc/init.d/%s status > ' - '/var/lib/nagios/service-check-%s.txt\n' % (svc, - svc) - ) + checkpath = '%s/service-check-%s.txt' % (nrpe.homedir, svc) + croncmd = ( + '/usr/local/lib/nagios/plugins/check_exit_status.pl ' + '-s /etc/init.d/%s status' % svc + ) + cron_file = '*/5 * * * * root %s > %s\n' % (croncmd, checkpath) f = open(cronpath, 'w') f.write(cron_file) f.close() nrpe.add_check( shortname=svc, - description='process check {%s}' % unit_name, - check_cmd='check_status_file.py -f ' - '/var/lib/nagios/service-check-%s.txt' % svc, + description='service check {%s}' % unit_name, + check_cmd='check_status_file.py -f %s' % checkpath, ) + # if /var/lib/nagios doesn't exist open(checkpath, 'w') will fail + # (LP: #1670223). + if immediate_check and os.path.isdir(nrpe.homedir): + f = open(checkpath, 'w') + subprocess.call( + croncmd.split(), + stdout=f, + stderr=subprocess.STDOUT + ) + f.close() + os.chmod(checkpath, 0o644) def copy_nrpe_checks(): diff --git a/hooks/charmhelpers/contrib/hardening/templating.py b/hooks/charmhelpers/contrib/hardening/templating.py index 2174c64..5b6765f 100644 --- a/hooks/charmhelpers/contrib/hardening/templating.py +++ b/hooks/charmhelpers/contrib/hardening/templating.py @@ -13,6 +13,7 @@ # limitations under the License. import os +import six from charmhelpers.core.hookenv import ( log, @@ -26,7 +27,10 @@ except ImportError: from charmhelpers.fetch import apt_install from charmhelpers.fetch import apt_update apt_update(fatal=True) - apt_install('python-jinja2', fatal=True) + if six.PY2: + apt_install('python-jinja2', fatal=True) + else: + apt_install('python3-jinja2', fatal=True) from jinja2 import FileSystemLoader, Environment diff --git a/hooks/charmhelpers/contrib/network/ip.py b/hooks/charmhelpers/contrib/network/ip.py index e141fc1..54c76a7 100644 --- a/hooks/charmhelpers/contrib/network/ip.py +++ b/hooks/charmhelpers/contrib/network/ip.py @@ -20,25 +20,37 @@ import socket from functools import partial -from charmhelpers.core.hookenv import unit_get from charmhelpers.fetch import apt_install, apt_update from charmhelpers.core.hookenv import ( + config, log, + network_get_primary_address, + unit_get, WARNING, ) +from charmhelpers.core.host import ( + lsb_release, +) + try: import netifaces except ImportError: apt_update(fatal=True) - apt_install('python-netifaces', fatal=True) + if six.PY2: + apt_install('python-netifaces', fatal=True) + else: + apt_install('python3-netifaces', fatal=True) import netifaces try: import netaddr except ImportError: apt_update(fatal=True) - apt_install('python-netaddr', fatal=True) + if six.PY2: + apt_install('python-netaddr', fatal=True) + else: + apt_install('python3-netaddr', fatal=True) import netaddr @@ -414,7 +426,10 @@ def ns_query(address): try: import dns.resolver except ImportError: - apt_install('python-dnspython', fatal=True) + if six.PY2: + apt_install('python-dnspython', fatal=True) + else: + apt_install('python3-dnspython', fatal=True) import dns.resolver if isinstance(address, dns.name.Name): @@ -462,7 +477,10 @@ def get_hostname(address, fqdn=True): try: import dns.reversename except ImportError: - apt_install("python-dnspython", fatal=True) + if six.PY2: + apt_install("python-dnspython", fatal=True) + else: + apt_install("python3-dnspython", fatal=True) import dns.reversename rev = dns.reversename.from_address(address) @@ -499,3 +517,40 @@ def port_has_listener(address, port): cmd = ['nc', '-z', address, str(port)] result = subprocess.call(cmd) return not(bool(result)) + + +def assert_charm_supports_ipv6(): + """Check whether we are able to support charms ipv6.""" + if lsb_release()['DISTRIB_CODENAME'].lower() < "trusty": + raise Exception("IPv6 is not supported in the charms for Ubuntu " + "versions less than Trusty 14.04") + + +def get_relation_ip(interface, config_override=None): + """Return this unit's IP for the given relation. + + Allow for an arbitrary interface to use with network-get to select an IP. + Handle all address selection options including configuration parameter + override and IPv6. + + Usage: get_relation_ip('amqp', config_override='access-network') + + @param interface: string name of the relation. + @param config_override: string name of the config option for network + override. Supports legacy network override configuration parameters. + @raises Exception if prefer-ipv6 is configured but IPv6 unsupported. + @returns IPv6 or IPv4 address + """ + + fallback = get_host_ip(unit_get('private-address')) + if config('prefer-ipv6'): + assert_charm_supports_ipv6() + return get_ipv6_addr()[0] + elif config_override and config(config_override): + return get_address_in_network(config(config_override), + fallback) + else: + try: + return network_get_primary_address(interface) + except NotImplementedError: + return fallback diff --git a/hooks/charmhelpers/contrib/openstack/utils.py b/hooks/charmhelpers/contrib/openstack/utils.py index 80219d6..7e8ecff 100644 --- a/hooks/charmhelpers/contrib/openstack/utils.py +++ b/hooks/charmhelpers/contrib/openstack/utils.py @@ -153,7 +153,7 @@ SWIFT_CODENAMES = OrderedDict([ ('newton', ['2.8.0', '2.9.0', '2.10.0']), ('ocata', - ['2.11.0', '2.12.0']), + ['2.11.0', '2.12.0', '2.13.0']), ]) # >= Liberty version->codename mapping diff --git a/hooks/charmhelpers/contrib/python/packages.py b/hooks/charmhelpers/contrib/python/packages.py index e29bd1b..6e95028 100644 --- a/hooks/charmhelpers/contrib/python/packages.py +++ b/hooks/charmhelpers/contrib/python/packages.py @@ -16,6 +16,7 @@ # limitations under the License. import os +import six import subprocess import sys @@ -39,7 +40,10 @@ def pip_execute(*args, **kwargs): from pip import main as _pip_execute except ImportError: apt_update() - apt_install('python-pip') + if six.PY2: + apt_install('python-pip') + else: + apt_install('python3-pip') from pip import main as _pip_execute _pip_execute(*args, **kwargs) finally: @@ -136,7 +140,10 @@ def pip_list(): def pip_create_virtualenv(path=None): """Create an isolated Python environment.""" - apt_install('python-virtualenv') + if six.PY2: + apt_install('python-virtualenv') + else: + apt_install('python3-virtualenv') if path: venv_path = path diff --git a/hooks/charmhelpers/core/host.py b/hooks/charmhelpers/core/host.py index edbb72f..05edfa5 100644 --- a/hooks/charmhelpers/core/host.py +++ b/hooks/charmhelpers/core/host.py @@ -306,6 +306,8 @@ SYSTEMD_SYSTEM = '/run/systemd/system' def init_is_systemd(): """Return True if the host system uses systemd, False otherwise.""" + if lsb_release()['DISTRIB_CODENAME'] == 'trusty': + return False return os.path.isdir(SYSTEMD_SYSTEM) diff --git a/hooks/charmhelpers/fetch/snap.py b/hooks/charmhelpers/fetch/snap.py new file mode 100644 index 0000000..23c707b --- /dev/null +++ b/hooks/charmhelpers/fetch/snap.py @@ -0,0 +1,122 @@ +# Copyright 2014-2017 Canonical Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Charm helpers snap for classic charms. + +If writing reactive charms, use the snap layer: +https://lists.ubuntu.com/archives/snapcraft/2016-September/001114.html +""" +import subprocess +from os import environ +from time import sleep +from charmhelpers.core.hookenv import log + +__author__ = 'Joseph Borg ' + +SNAP_NO_LOCK = 1 # The return code for "couldn't acquire lock" in Snap (hopefully this will be improved). +SNAP_NO_LOCK_RETRY_DELAY = 10 # Wait X seconds between Snap lock checks. +SNAP_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times. + + +class CouldNotAcquireLockException(Exception): + pass + + +def _snap_exec(commands): + """ + Execute snap commands. + + :param commands: List commands + :return: Integer exit code + """ + assert type(commands) == list + + retry_count = 0 + return_code = None + + while return_code is None or return_code == SNAP_NO_LOCK: + try: + return_code = subprocess.check_call(['snap'] + commands, env=environ) + except subprocess.CalledProcessError as e: + retry_count += + 1 + if retry_count > SNAP_NO_LOCK_RETRY_COUNT: + raise CouldNotAcquireLockException('Could not aquire lock after %s attempts' % SNAP_NO_LOCK_RETRY_COUNT) + return_code = e.returncode + log('Snap failed to acquire lock, trying again in %s seconds.' % SNAP_NO_LOCK_RETRY_DELAY, level='WARN') + sleep(SNAP_NO_LOCK_RETRY_DELAY) + + return return_code + + +def snap_install(packages, *flags): + """ + Install a snap package. + + :param packages: String or List String package name + :param flags: List String flags to pass to install command + :return: Integer return code from snap + """ + if type(packages) is not list: + packages = [packages] + + flags = list(flags) + + message = 'Installing snap(s) "%s"' % ', '.join(packages) + if flags: + message += ' with option(s) "%s"' % ', '.join(flags) + + log(message, level='INFO') + return _snap_exec(['install'] + flags + packages) + + +def snap_remove(packages, *flags): + """ + Remove a snap package. + + :param packages: String or List String package name + :param flags: List String flags to pass to remove command + :return: Integer return code from snap + """ + if type(packages) is not list: + packages = [packages] + + flags = list(flags) + + message = 'Removing snap(s) "%s"' % ', '.join(packages) + if flags: + message += ' with options "%s"' % ', '.join(flags) + + log(message, level='INFO') + return _snap_exec(['remove'] + flags + packages) + + +def snap_refresh(packages, *flags): + """ + Refresh / Update snap package. + + :param packages: String or List String package name + :param flags: List String flags to pass to refresh command + :return: Integer return code from snap + """ + if type(packages) is not list: + packages = [packages] + + flags = list(flags) + + message = 'Refreshing snap(s) "%s"' % ', '.join(packages) + if flags: + message += ' with options "%s"' % ', '.join(flags) + + log(message, level='INFO') + return _snap_exec(['refresh'] + flags + packages) diff --git a/hooks/charmhelpers/fetch/ubuntu.py b/hooks/charmhelpers/fetch/ubuntu.py index 39b9b80..82ac80f 100644 --- a/hooks/charmhelpers/fetch/ubuntu.py +++ b/hooks/charmhelpers/fetch/ubuntu.py @@ -116,8 +116,8 @@ CLOUD_ARCHIVE_POCKETS = { } APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT. -APT_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks. -APT_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times. +CMD_RETRY_DELAY = 10 # Wait 10 seconds between command retries. +CMD_RETRY_COUNT = 30 # Retry a failing fatal command X times. def filter_installed_packages(packages): @@ -249,7 +249,8 @@ def add_source(source, key=None): source.startswith('http') or source.startswith('deb ') or source.startswith('cloud-archive:')): - subprocess.check_call(['add-apt-repository', '--yes', source]) + cmd = ['add-apt-repository', '--yes', source] + _run_with_retries(cmd) elif source.startswith('cloud:'): install(filter_installed_packages(['ubuntu-cloud-keyring']), fatal=True) @@ -286,41 +287,60 @@ def add_source(source, key=None): key]) -def _run_apt_command(cmd, fatal=False): - """Run an APT command. - - Checks the output and retries if the fatal flag is set - to True. +def _run_with_retries(cmd, max_retries=CMD_RETRY_COUNT, retry_exitcodes=(1,), + retry_message="", cmd_env=None): + """Run a command and retry until success or max_retries is reached. :param: cmd: str: The apt command to run. + :param: max_retries: int: The number of retries to attempt on a fatal + command. Defaults to CMD_RETRY_COUNT. + :param: retry_exitcodes: tuple: Optional additional exit codes to retry. + Defaults to retry on exit code 1. + :param: retry_message: str: Optional log prefix emitted during retries. + :param: cmd_env: dict: Environment variables to add to the command run. + """ + + env = os.environ.copy() + if cmd_env: + env.update(cmd_env) + + if not retry_message: + retry_message = "Failed executing '{}'".format(" ".join(cmd)) + retry_message += ". Will retry in {} seconds".format(CMD_RETRY_DELAY) + + retry_count = 0 + result = None + + retry_results = (None,) + retry_exitcodes + while result in retry_results: + try: + result = subprocess.check_call(cmd, env=env) + except subprocess.CalledProcessError as e: + retry_count = retry_count + 1 + if retry_count > max_retries: + raise + result = e.returncode + log(retry_message) + time.sleep(CMD_RETRY_DELAY) + + +def _run_apt_command(cmd, fatal=False): + """Run an apt command with optional retries. + :param: fatal: bool: Whether the command's output should be checked and retried. """ - env = os.environ.copy() - - if 'DEBIAN_FRONTEND' not in env: - env['DEBIAN_FRONTEND'] = 'noninteractive' + # Provide DEBIAN_FRONTEND=noninteractive if not present in the environment. + cmd_env = { + 'DEBIAN_FRONTEND': os.environ.get('DEBIAN_FRONTEND', 'noninteractive')} if fatal: - retry_count = 0 - result = None - - # If the command is considered "fatal", we need to retry if the apt - # lock was not acquired. - - while result is None or result == APT_NO_LOCK: - try: - result = subprocess.check_call(cmd, env=env) - except subprocess.CalledProcessError as e: - retry_count = retry_count + 1 - if retry_count > APT_NO_LOCK_RETRY_COUNT: - raise - result = e.returncode - log("Couldn't acquire DPKG lock. Will retry in {} seconds." - "".format(APT_NO_LOCK_RETRY_DELAY)) - time.sleep(APT_NO_LOCK_RETRY_DELAY) - + _run_with_retries( + cmd, cmd_env=cmd_env, retry_exitcodes=(1, APT_NO_LOCK,), + retry_message="Couldn't acquire DPKG lock") else: + env = os.environ.copy() + env.update(cmd_env) subprocess.call(cmd, env=env) diff --git a/metadata.yaml b/metadata.yaml index df238c6..eb47512 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -11,6 +11,7 @@ tags: - misc series: - xenial + - zesty - trusty - yakkety peers: diff --git a/tests/basic_deployment.py b/tests/basic_deployment.py index d40c158..65da3ab 100644 --- a/tests/basic_deployment.py +++ b/tests/basic_deployment.py @@ -223,11 +223,13 @@ class CephBasicDeployment(OpenStackAmuletDeployment): self.keystone_sentry: ['keystone'], self.glance_sentry: ['glance-registry', 'glance-api'], - self.cinder_sentry: ['cinder-api', - 'cinder-scheduler', + self.cinder_sentry: ['cinder-scheduler', 'cinder-volume'], } + if self._get_openstack_release() < self.xenial_ocata: + services[self.cinder_sentry].append('cinder-api') + if self._get_openstack_release() < self.xenial_mitaka: # For upstart systems only. Ceph services under systemd # are checked by process name instead. @@ -380,11 +382,19 @@ class CephBasicDeployment(OpenStackAmuletDeployment): u.log.debug('Checking cinder (rbd) config file data...') unit = self.cinder_sentry conf = '/etc/cinder/cinder.conf' + + # NOTE(jamespage): Deal with section config for >= ocata. + if self._get_openstack_release() >= self.xenial_ocata: + section_key = 'CEPH' + else: + section_key = 'DEFAULT' + expected = { - 'DEFAULT': { + section_key: { 'volume_driver': 'cinder.volume.drivers.rbd.RBDDriver' } } + for section, pairs in expected.iteritems(): ret = u.validate_config_data(unit, conf, section, pairs) if ret: diff --git a/tests/charmhelpers/contrib/amulet/utils.py b/tests/charmhelpers/contrib/amulet/utils.py index f9e4c3a..8a6b764 100644 --- a/tests/charmhelpers/contrib/amulet/utils.py +++ b/tests/charmhelpers/contrib/amulet/utils.py @@ -785,37 +785,30 @@ class AmuletUtils(object): generating test messages which need to be unique-ish.""" return '[{}-{}]'.format(uuid.uuid4(), time.time()) -# amulet juju action helpers: + # amulet juju action helpers: def run_action(self, unit_sentry, action, _check_output=subprocess.check_output, params=None): - """Run the named action on a given unit sentry. + """Translate to amulet's built in run_action(). Deprecated. + + Run the named action on a given unit sentry. params a dict of parameters to use - _check_output parameter is used for dependency injection. + _check_output parameter is no longer used @return action_id. """ - unit_id = unit_sentry.info["unit_name"] - command = ["juju", "action", "do", "--format=json", unit_id, action] - if params is not None: - for key, value in params.iteritems(): - command.append("{}={}".format(key, value)) - self.log.info("Running command: %s\n" % " ".join(command)) - output = _check_output(command, universal_newlines=True) - data = json.loads(output) - action_id = data[u'Action queued with id'] - return action_id + self.log.warn('charmhelpers.contrib.amulet.utils.run_action has been ' + 'deprecated for amulet.run_action') + return unit_sentry.run_action(action, action_args=params) def wait_on_action(self, action_id, _check_output=subprocess.check_output): """Wait for a given action, returning if it completed or not. - _check_output parameter is used for dependency injection. + action_id a string action uuid + _check_output parameter is no longer used """ - command = ["juju", "action", "fetch", "--format=json", "--wait=0", - action_id] - output = _check_output(command, universal_newlines=True) - data = json.loads(output) + data = amulet.actions.get_action_output(action_id, full_output=True) return data.get(u"status") == "completed" def status_get(self, unit): diff --git a/tests/charmhelpers/contrib/openstack/amulet/utils.py b/tests/charmhelpers/contrib/openstack/amulet/utils.py index 401c032..1f4cf42 100644 --- a/tests/charmhelpers/contrib/openstack/amulet/utils.py +++ b/tests/charmhelpers/contrib/openstack/amulet/utils.py @@ -32,6 +32,7 @@ from keystoneclient.v3 import client as keystone_client_v3 from novaclient import exceptions import novaclient.client as nova_client +import novaclient import pika import swiftclient @@ -434,9 +435,14 @@ class OpenStackAmuletUtils(AmuletUtils): self.log.debug('Authenticating nova user ({})...'.format(user)) ep = keystone.service_catalog.url_for(service_type='identity', endpoint_type='publicURL') - return nova_client.Client(NOVA_CLIENT_VERSION, - username=user, api_key=password, - project_id=tenant, auth_url=ep) + if novaclient.__version__[0] >= "7": + return nova_client.Client(NOVA_CLIENT_VERSION, + username=user, password=password, + project_name=tenant, auth_url=ep) + else: + return nova_client.Client(NOVA_CLIENT_VERSION, + username=user, api_key=password, + project_id=tenant, auth_url=ep) def authenticate_swift_user(self, keystone, user, password, tenant): """Authenticates a regular user with swift api.""" diff --git a/tests/charmhelpers/core/host.py b/tests/charmhelpers/core/host.py index edbb72f..05edfa5 100644 --- a/tests/charmhelpers/core/host.py +++ b/tests/charmhelpers/core/host.py @@ -306,6 +306,8 @@ SYSTEMD_SYSTEM = '/run/systemd/system' def init_is_systemd(): """Return True if the host system uses systemd, False otherwise.""" + if lsb_release()['DISTRIB_CODENAME'] == 'trusty': + return False return os.path.isdir(SYSTEMD_SYSTEM) diff --git a/tests/gate-basic-xenial-ocata b/tests/gate-basic-xenial-ocata old mode 100644 new mode 100755 diff --git a/tox.ini b/tox.ini index d8d8d03..6f1aeac 100644 --- a/tox.ini +++ b/tox.ini @@ -14,13 +14,18 @@ install_command = pip install --allow-unverified python-apt {opts} {packages} commands = ostestr {posargs} whitelist_externals = juju -passenv = HOME TERM AMULET_* +passenv = HOME TERM AMULET_* CS_API_URL [testenv:py27] basepython = python2.7 deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt +[testenv:py35] +basepython = python3.5 +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt + [testenv:pep8] basepython = python2.7 deps = -r{toxinidir}/requirements.txt