From 2ab26e69f58eef9855fe706872cda48d3578aef4 Mon Sep 17 00:00:00 2001 From: Nikolay Vinogradov Date: Sat, 15 Oct 2022 03:45:57 +0300 Subject: [PATCH] Merged branch refactor onto main - implemented basic charm functionality and configuration - unit-tests - pep8 --- README.md | 15 +- actions.yaml | 9 + config.yaml | 52 ++-- metadata.yaml | 13 +- requirements.txt | 2 +- src/charm.py | 293 ++++++++++++++++-- templates/multipath.conf.j2 | 28 ++ tests/bundles/focal-ussuri.yaml | 6 +- tests/tests.py | 15 +- unit_tests/infinibox-sample.txt | 77 +++++ unit_tests/test_cinder_infinidat2_charm.py | 51 ---- unit_tests/test_infinidat_tools.py | 327 +++++++++++++++++++++ 12 files changed, 764 insertions(+), 124 deletions(-) create mode 100644 actions.yaml create mode 100644 templates/multipath.conf.j2 create mode 100644 unit_tests/infinibox-sample.txt delete mode 100644 unit_tests/test_cinder_infinidat2_charm.py create mode 100644 unit_tests/test_infinidat_tools.py diff --git a/README.md b/README.md index 4e8de44..4abb0e1 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,16 @@ -infinidat2 Storage Backend for Cinder -------------------------------- +Infinidat Tools charm +--------------------- Overview ======== -This charm provides a infinidat2 storage backend for use with the Cinder -charm. +This charm provides configuration and tools for principal charms, such as cinder and nova-compute charms. -To use: +To use as a nova-compute subordinate: - juju deploy cinder - juju deploy cinder-infinidat2 - juju add-relation cinder-infinidat2 cinder + juju deploy nova + juju deploy infinidat-tools + juju add-relation infinidat-tools:storage-backend nova:storage-backend Configuration ============= diff --git a/actions.yaml b/actions.yaml new file mode 100644 index 0000000..37a8847 --- /dev/null +++ b/actions.yaml @@ -0,0 +1,9 @@ +run-infinidat-settings-check: + description: | + Manually trigger environment re-validation with infinihost tool. + params: + auto-fix: + type: string + description: | + If true, infinihost updates environment multipath, + iscsi and lvm configuration to recommended defaults. diff --git a/config.yaml b/config.yaml index 4dd2e1d..365f67b 100644 --- a/config.yaml +++ b/config.yaml @@ -1,38 +1,42 @@ options: - driver-source: + install_sources: type: string - default: + default: "deb https://repo.infinidat.com/packages/main-stable/apt/linux-ubuntu {distrib_codename} main" description: | Optional configuration to support use of additional sources such as: - ppa:myteam/ppa - cloud:trusty-proposed/kilo - http://my.archive.com/ubuntu main The last option should be used in conjunction with the key configuration - option. - driver-key: + option. See https://repo.infinidat.com/home/main-stable for details. + The charm also supports templating of the distribution codename via + automatic expansion of {distrib_codename} depending on the host system + install_keys: type: string - default: + default: | + -----BEGIN PGP PUBLIC KEY BLOCK----- + Version: GnuPG v1.4.11 (GNU/Linux) + + mQENBFESDRIBCADMR7MQMbH4GdCQqfrOMt35MhBwwH4wv9kb1WRSTxa0CmuzYaBB + 1nJ0nLaMAwHsEr9CytPWDpMngm/3nt+4F2hJcsOEkQkqeJ31gScJewM+AOUV3DEl + qOeXXYLcP+jUY6pPjlZpOw0p7moUQPXHn+7amVrk7cXGQ8O3B+5a5wjN86LT2hlX + DlBlV5bX/DYluiPUbvQLOknmwO53KpaeDeZc4a8iIOCYWu2ntuAMddBkTps0El5n + JJZMTf6os2ZzngWMZRMDiVJgqVRi2b+8SgFQlQy0cAmne/mpgPrRq0ZMX3DokGG5 + hnIg1mF82laTxd+9qtiOxupzJqf8mncQHdaTABEBAAG0IWFwcF9yZXBvIChDb21t + ZW50KSA8bm9AZW1haWwuY29tPokBOAQTAQIAIgUCURINEgIbLwYLCQgHAwIGFQgC + CQoLBBYCAwECHgECF4AACgkQem2D/j05RYSrcggAsCc4KppV/SZX5XI/CWFXIAXw + +HaNsh2EwYKf9DhtoGbTOuwePvrPGcgFYM3Tu+m+rziPnnFl0bs0xwQyNEVQ9yDw + t465pSgmXwEHbBkoISV1e4WYtZAsnTNne9ieJ49Ob/WY4w3AkdPRK/41UP5Ct6lR + HHRXrSWJYHVq5Rh6BakRuMJyJLz/KvcJAaPkA4U6VrPD7PFtSecMTaONPjGCcomq + b7q84G5ZfeJWb742PWBTS8fJdC+Jd4y5fFdJS9fQwIo52Ff9In2QBpJt5Wdc02SI + fvQnuh37D2P8OcIfMxMfoFXpAMWjrMYc5veyQY1GXD/EOkfjjLne6qWPLfNojA== + =w5Os + -----END PGP PUBLIC KEY BLOCK----- description: | Key ID to import to the apt keyring to support use with arbitary source configuration from outside of Launchpad archives or PPA's. - use-multipath: - type: boolean - default: True - description: | - Whether to use a multipath connection for iSCSI or FC in Cinder - volume service. Enabling multipath for VMs is managed by the - "use-multipath" option in the nova-compute charm. - protocol: - type: string - default: - description: | - SAN protocol to use. Choose between iscsi or fc. - volume-backend-name: + lvm_global_filter: type: string description: | - Volume backend name for the backend. The default value is the - application name in the Juju model, e.g. "cinder-mybackend" - if it's deployed as `juju deploy cinder-infinidat2 cinder-mybackend`. - A common backend name can be set to multiple backends with the - same characters so that those can be treated as a single virtual - backend associated with a single volume type. + Value for global_filter to specify in lvm.conf + default: "[ \"a|^/dev/sd.*|\", \"a|^/dev/vd.*|\", \"r|.*|\" ]" diff --git a/metadata.yaml b/metadata.yaml index fca0be2..72f55d0 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -1,10 +1,11 @@ -name: cinder-infinidat2 -summary: infinidat2 integration for OpenStack Block Storage +name: infinidat-tools +summary: Inifnidat integration tools for OpenStack Block Storage maintainer: OpenStack Charmers description: | - Cinder is the block storage service for the Openstack project. - . - This charm provides a infinidat2 backend for Cinder + This charm is intended to be used as a subordinate charm to any machine which will access Infinidat Infinibox storage. + Infinidat host powertools is installed and is used to configure the machine (akin to infinihost settings check --auto-fix). + Compatible with bare-metal and KVM machines. + NOT compatible with lxd containers as they can't run multipathd on bionic. tags: - openstack - storage @@ -15,7 +16,7 @@ series: subordinate: true provides: storage-backend: - interface: cinder-backend + interface: storage-backend scope: container requires: juju-info: diff --git a/requirements.txt b/requirements.txt index 86315ca..bd51e1a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ ops -git+https://opendev.org/openstack/charm-ops-openstack#egg=ops_openstack +git+https://github.com/nikolayvinogradov/charm-ops-openstack@nickv#egg=ops_openstack diff --git a/src/charm.py b/src/charm.py index a0f1580..a6ae0a1 100755 --- a/src/charm.py +++ b/src/charm.py @@ -1,6 +1,6 @@ #! /usr/bin/env python3 -# Copyright 2021 Canonical Ltd +# Copyright 2022 Canonical Ltd # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,46 +14,291 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os +import logging +import subprocess +import re +import tempfile +from pathlib import Path -from ops_openstack.plugins.classes import CinderStoragePluginCharm -from ops_openstack.core import charm_class, get_charm_class_for_release +from ops_openstack.core import OSBaseCharm from ops.main import main +from ops.model import ( + ActiveStatus, + BlockedStatus, +) -class CinderCharmBase(CinderStoragePluginCharm): +from charmhelpers.core.host import ( + lsb_release, + service_restart, +) - PACKAGES = ['cinder-common'] - MANDATORY_CONFIG = ['protocol'] +from charmhelpers.fetch import ( + apt_install, + apt_update, + add_source, +) + +from charmhelpers.fetch.archiveurl import ArchiveUrlFetchHandler + +logger = logging.getLogger(__name__) + +INFINIHOST_RESULTS_DIR = '/home/ubuntu/infinihost-results' +MULTIPATH_CONF = '/etc/multipath.conf' +DEFAULT_REPO_KEY_URL = 'https://repo.infinidat.com/packages/gpg.key' + + +class InfinidatToolsCharm(OSBaseCharm): + + # multipath-tools package should be installed by nova-compute + PACKAGES = [ + 'host-power-tools', + 'scsitools', + 'multipath-tools-boot' + ] + + MANDATORY_CONFIG = ['install_sources'] # Overriden from the parent. May be set depending on the charm's properties + + RESTART_MAP = { + MULTIPATH_CONF: ['multipathd'] + } + + DEFAULT_REPO_BASEURL = \ + 'https://repo.infinidat.com/packages/main-stable/apt/linux-ubuntu' + stateless = True active_active = True def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def cinder_configuration(self, config): - # Return the configuration to be set by the principal. - backend_name = config.get('volume-backend-name', - self.framework.model.app.name) - volume_driver = '' - options = [ - ('volume_driver', volume_driver), - ('volume_backend_name', backend_name), + self.framework.observe(self.on.start, self.on_start) + + self.framework.observe(self.on.run_infinidat_settings_check_action, + self.on_run_infinidat_settings_check_action) + + def _run_infinihost_check(self, auto_fix=True, stdout=subprocess.PIPE): + """ + host-power-tool utility checks environment for + the recommended settings and if anything is missing + (and --auto-fix is provided) then it: + - generates default multipath.conf with recommended defaults + - excludes local disks from multipath configuration + - registers itself as a udev handler + - regenerates initramfs, so that multipath configuration + is applied to it as well, in case root fs is on SAN + """ + p = None + + cmd = [ + 'infinihost', + 'settings', + 'check' ] - if config.get('use-multipath'): - options.extend([ - ('use_multipath_for_image_xfer', True), - ('enforce_multipath_for_image_xfer', True) - ]) + if auto_fix: + cmd.append('--auto-fix') - return options + logging.debug("Executing: {0}".format(' '.join(cmd))) + try: + # FIXME: Without this there is a dependency conflict: + # pkg_resources in charm's venv pythonpath takes precedence over + # the system-wide one, and because of the version difference, + # it breaks infinihost with "ImportError: cannot import name 'six'" + env = os.environ.copy() + if 'PYTHONPATH' in env: + env.pop('PYTHONPATH') -@charm_class -class Cinderinfinidat2Charm(CinderCharmBase): - release = 'ussuri' + p = subprocess.Popen(cmd, shell=False, stdout=stdout, env=env) + except FileNotFoundError: + logging.fatal( + "Failed to run 'infinihost': is host-power-tools installed?") + raise + + if stdout == subprocess.PIPE: + stdout, _ = p.communicate() + logger.debug("infinihost output: {0}".format(stdout)) + + code = p.wait() + + logger.info('infinihost exit code: {0}' + .format(code)) + + return code + + def _update_multipath_conf(self, restart=True): + + replacements = ( + # Ensure that multipathd config file has skip_kpartx set to yes + # (causes issues with volumes detaching if set to no) + (re.compile( + r'(skip_kpartx[ \t]+).*$', re.MULTILINE), 'yes'), + (re.compile( + r'(user_friendly_names[ \t]+).*$', re.MULTILINE), 'no'), + ) + + with open(MULTIPATH_CONF, 'r') as f: + data = f.read() + for pat, new_value in replacements: + data = re.sub( + pat, + lambda m: '{0}"{1}"'.format(str(m.group(1)), new_value), + data + ) + + logger.info("Updating multipath.conf") + with open(MULTIPATH_CONF, 'w') as f: + f.write(data) + + logger.info("Restarting multipathd") + if restart: + service_restart('multipathd') + + def _set_lvm_conf_global_filter(self, lvm_global_filter, + lvm_conf='/etc/lvm/lvm.conf'): + # Ensure we have a lvm.conf filter in place to stop lvm groups + # present on instance/VM being discovered by the host + + logging.info('Setting lvm.conf global_filter') + + with open(lvm_conf, 'r') as file: + d = file.read() + + p = re.compile(r'(# global_filter = \[[^\n]*\n)') + + p_split = p.split(d, maxsplit=1) + if len(p_split) == 3: + the_rest = p_split.pop(2) + + the_next_line, nl, the_rest = the_rest.partition('\n') + tmp = [] + if lvm_global_filter: + tmp.append('global_filter = {0} # __infinidat_tools__' + .format(lvm_global_filter)) + + if '__infinidat_tools__' not in the_next_line: + tmp.append(the_next_line + nl + the_rest) + else: + tmp.append(the_rest) + + p_split.append('\n'.join(tmp)) + + with open(lvm_conf, 'w') as file: + file.write(''.join(p_split)) + else: + logging.fatal( + 'Error while modifying lvm.conf: ' + 'expected pattern not found' + ) + self.unit.status = BlockedStatus('Failed to update lvm.conf') + return + + def on_start(self, event): + self._stored.is_started = True + + def _regenerate_initrd(self): + # Regenerate initrd + try: + logging.info('Regenerating initrd') + subprocess.check_call("update-initramfs -u -k all", shell=True) + except subprocess.CalledProcessError as e: + logging.fatal('Error regenerating initrd: {0}'.format(e)) + self.unit.status = BlockedStatus('error regenerating initrd') + return + + def on_install(self, event): + logging.info('Preparing Infinidat tools package installation') + + # initial installation, this is not called after reboot + try: + self.install_pkgs() + code = self._run_infinihost_check(auto_fix=True) + self._update_multipath_conf() + self._regenerate_initrd() + except Exception as e: + logger.fatal("Failed to install packages: {0}".format(str(e))) + # something failed, attempt rerunning the hook later + self.unit.status = BlockedStatus("Installation failed") + event.defer() + return + + self._set_lvm_conf_global_filter( + self.config.get('lvm_global_filter')) + + if code != 0: + self.unit.status = ActiveStatus( + "review infinihost settings status") + else: + self.unit.status = ActiveStatus() + + def _get_default_repo_key(self): + url_fetcher = ArchiveUrlFetchHandler() + with tempfile.NamedTemporaryFile() as outfile: + outfile.close() + dest_path = os.path.join(tempfile.gettempdir(), outfile.name) + + url_fetcher.download(DEFAULT_REPO_KEY_URL, + dest_path) + with open(dest_path) as f: + return f.read() + + def install_pkgs(self): + install_keys = self.model.config.get('install_keys') + if not install_keys: + install_keys = self._get_default_repo_key() + + # we implement $codename expansion here + # see the default value for 'source' in config.yaml + if self.model.config.get('install_sources'): + distrib_codename = lsb_release()['DISTRIB_CODENAME'].lower() + add_source( + self.model.config['install_sources'] + .format(distrib_codename=distrib_codename), + self.model.config.get('install_keys')) + apt_update(fatal=True) + apt_install(self.PACKAGES, fatal=True) + + def on_config(self, event): + self._set_lvm_conf_global_filter( + self.config.get('lvm_global_filter')) + + self.unit.status = ActiveStatus() + + def on_run_infinidat_settings_check_action(self, event): + Path(INFINIHOST_RESULTS_DIR).mkdir(parents=True, exist_ok=True) + + if event.params.get('auto-fix'): + auto_fix = True + else: + auto_fix = False + + with tempfile.NamedTemporaryFile(mode="w+b", prefix='infinihost-out-', + dir=INFINIHOST_RESULTS_DIR, + delete=False) as outfile: + try: + event.log("Running 'infinihost settings check'") + code = self._run_infinihost_check(auto_fix=auto_fix, + stdout=outfile) + Path(outfile.name).chmod(0o644) + except subprocess.CalledProcessError as e: + msg = "Failed to run infinihost: {0}".format(str(e)) + logger.fatal(msg) + event.fail(msg) + + if auto_fix: + event.log("--auto-fix is enabled, updating multipath.conf") + self._update_multipath_conf() + + event.set_results({ + "result": + "exit code={0}\n" + "see 'juju ssh -m {1} {2} cat {3}' for more details" + .format(code, self.unit.name, self.model.name, + os.path.join(INFINIHOST_RESULTS_DIR, outfile.name))}) if __name__ == '__main__': - main(get_charm_class_for_release()) + main(InfinidatToolsCharm) diff --git a/templates/multipath.conf.j2 b/templates/multipath.conf.j2 new file mode 100644 index 0000000..ecff2a1 --- /dev/null +++ b/templates/multipath.conf.j2 @@ -0,0 +1,28 @@ +defaults { + max_fds 8192 + queue_without_daemon no + user_friendly_names no + skip_kpartx yes + find_multipaths yes +} +devices { + device { + vendor "NFINIDAT" + product "InfiniBox.*" + path_grouping_policy "group_by_prio" + path_checker "tur" + features "0" + hardware_handler "1 alua" + prio "alua" + rr_weight "priorities" + no_path_retry "queue" + rr_min_io 1 + rr_min_io_rq 1 + flush_on_last_del "yes" + fast_io_fail_tmo 15 + dev_loss_tmo "infinity" + path_selector "round-robin 0" + failback immediate + detect_prio yes + } +} diff --git a/tests/bundles/focal-ussuri.yaml b/tests/bundles/focal-ussuri.yaml index 7eee982..018401b 100644 --- a/tests/bundles/focal-ussuri.yaml +++ b/tests/bundles/focal-ussuri.yaml @@ -32,8 +32,8 @@ applications: openstack-origin: cloud:focal-ussuri to: - '2' - cinder-infinidat2: - charm: ../../cinder-infinidat2.charm + infinidat-tools: + charm: ../../infinidat-tools.charm # options: rabbitmq-server: charm: ch:rabbitmq-server @@ -47,4 +47,4 @@ relations: - [ cinder:shared-db, mysql:shared-db ] - [ cinder:identity-service, keystone:identity-service ] - [ cinder:amqp, rabbitmq-server:amqp ] -- [ cinder:storage-backend, cinder-infinidat2:storage-backend ] \ No newline at end of file +- [ cinder:juju-info, infinidat-tools:juju-info ] diff --git a/tests/tests.py b/tests/tests.py index 481d92f..d759ca9 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -14,17 +14,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Encapsulate cinder-infinidat2 testing.""" +"""Encapsulate infinidat-tools testing.""" from zaza.openstack.charm_tests.cinder_backend.tests import CinderBackendTest -class Cinderinfinidat2Test(CinderBackendTest): - """Encapsulate infinidat2 tests.""" +class InfinidatToolsTest(CinderBackendTest): + """Encapsulate infinidat-tools tests.""" - backend_name = 'infinidat2' + backend_name = 'cinder-infinidat' expected_config_content = { - 'infinidat2': { - 'volume-backend-name': ['infinidat2'], - }} + 'infinidat-tools': { + 'volume-backend-name': ['cinder-infinidat'], + } + } diff --git a/unit_tests/infinibox-sample.txt b/unit_tests/infinibox-sample.txt new file mode 100644 index 0000000..637b018 --- /dev/null +++ b/unit_tests/infinibox-sample.txt @@ -0,0 +1,77 @@ +SCSI: Checking that sg3-utils is installed ... ok +SCSI: Checking that scsitools is installed ... ok +SCSI: Checking that parted is installed ... ok +Multipath: Checking that multipath-tools is installed ... ok +Multipath: Checking that multipath-tools-boot is installed ... ok +Multipath: Checking that multipath-tools automatically loads on boot ... ok +Multipath: Checking that kernel module multipath is loaded. ... ok +Multipath: Checking that multipath-tools is running. ... fail applying fix fail +Multipath: Checking global parameters ... skip +Multipath: Checking InfiniBox Device Settings ... skip +Multipath: Checking for InfiniBox device exclusions ... skip +Multipath: Checking for local device exclusions ... skip +Performance: Checking that a udev rule exists for setting the 'noop' I/O scheduler for InfiniBox scsi-disks and 'none' for all InfiniBox dm-disks ... ok +Performance: Checking that the current I/O scheduler for all InfiniBox scsi-disks is 'noop' and for all InfiniBox dm-disks is 'none' ... ok +FC HBA: Checking Fibre Channel host bus adapters ... skip +FC HBA: Checking versions of FC HBA drivers ... skip +FC HBA: Checking FC HBA driver attributes ... ok +Boot From SAN: Checking if PowerTools has made some changes that require a new initrd image in boot-from-san environments ... skip +Devices: Checking for volumes that don't have a multipath device ... ok +Devices: Checking that all reported InfiniBox LUNs have a block device ... ok +Devices: Checking the path count for all InfiniBox MPIO devices ... ok +Connectivity: Checking if initiator ports connected to InfiniBox are not connected to other target ports ... skip +Connectivity: Checking that each InfiniBox is connected through more than one initiator ... skip +Connectivity: Checking that the host is connected to at least one Node of each InfiniBox ... skip +Connectivity: Checking that the host does not have more block devices than the recommended limit ... skip +Connectivity: Checking that the host does not have more volumes than the recommended number ... ok +=============================================================================== +Fail Multipath: Checking that multipath-tools is running. +REASON: FAILURE: service multipath-tools is not running +INFO: For more information, see https://support.infinidat.com/hc/articles/202404141 +------------------------------------------------------------------------------- +Skip Multipath: Checking global parameters +REASON: SKIP: Multipath Daemon must be running for this test to pass +INFO: For more information, see https://support.infinidat.com/hc/articles/202319232 +------------------------------------------------------------------------------- +Skip Multipath: Checking InfiniBox Device Settings +REASON: SKIP: Multipath Daemon must be running for this test to pass +INFO: For more information, see https://support.infinidat.com/hc/articles/202404151 +------------------------------------------------------------------------------- +Skip Multipath: Checking for InfiniBox device exclusions +REASON: SKIP: Multipath Daemon must be running for this test to pass +INFO: For more information, see https://support.infinidat.com/hc/articles/202319242 +------------------------------------------------------------------------------- +Skip Multipath: Checking for local device exclusions +REASON: SKIP: Multipath Daemon must be running for this test to pass +INFO: For more information, see https://support.infinidat.com/hc/articles/202404331 +------------------------------------------------------------------------------- +Skip FC HBA: Checking Fibre Channel host bus adapters +REASON: SKIP: no fiberchannel ports detected +INFO: For more information, see https://support.infinidat.com/hc/articles/202404231 +------------------------------------------------------------------------------- +Skip FC HBA: Checking versions of FC HBA drivers +REASON: SKIP: no fiberchannel ports detected +INFO: For more information, see https://support.infinidat.com/hc/articles/202319342 +------------------------------------------------------------------------------- +Skip Boot From SAN: Checking if PowerTools has made some changes that require a new initrd image in boot-from-san environments +REASON: SKIP: no boot-related changes were performed +INFO: For more information, see https://support.infinidat.com/hc/articles/202621702 +------------------------------------------------------------------------------- +Skip Connectivity: Checking if initiator ports connected to InfiniBox are not connected to other target ports +REASON: SKIP: no fiberchannel ports connected to InfiniBox +INFO: For more information, see https://support.infinidat.com/hc/articles/202319352 +------------------------------------------------------------------------------- +Skip Connectivity: Checking that each InfiniBox is connected through more than one initiator +REASON: SKIP: no fiberchannel ports connected to InfiniBox +INFO: For more information, see https://support.infinidat.com/hc/articles/202404261 +------------------------------------------------------------------------------- +Skip Connectivity: Checking that the host is connected to at least one Node of each InfiniBox +REASON: SKIP: no fiberchannel ports connected to a physical port of InfiniBox +INFO: For more information, see https://support.infinidat.com/hc/articles/202404271 +------------------------------------------------------------------------------- +Skip Connectivity: Checking that the host does not have more block devices than the recommended limit +REASON: SKIP: no initiator ports connected to InfiniBox +INFO: For more information, see https://support.infinidat.com/hc/articles/202319172 +------------------------------------------------------------------------------- +(failures=1, skips=11) +This host is NOT ready to work with the InfiniBox diff --git a/unit_tests/test_cinder_infinidat2_charm.py b/unit_tests/test_cinder_infinidat2_charm.py deleted file mode 100644 index c35e1dd..0000000 --- a/unit_tests/test_cinder_infinidat2_charm.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2016 Canonical Ltd -# -# 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. - -import unittest -from src.charm import CinderCharmBase -from ops.model import ActiveStatus -from ops.testing import Harness - - -class TestCinderinfinidat2Charm(unittest.TestCase): - - def setUp(self): - self.harness = Harness(CinderCharmBase) - self.addCleanup(self.harness.cleanup) - self.harness.begin() - self.harness.set_leader(True) - backend = self.harness.add_relation('storage-backend', 'cinder') - self.harness.update_config({'volume-backend-name': 'test'}) - self.harness.add_relation_unit(backend, 'cinder/0') - - def test_cinder_base(self): - self.assertEqual( - self.harness.framework.model.app.name, - 'cinder-infinidat2') - # Test that charm is active upon installation. - self.harness.update_config({}) - self.assertTrue(isinstance( - self.harness.model.unit.status, ActiveStatus)) - - def test_multipath_config(self): - self.harness.update_config({'use-multipath': True}) - conf = dict(self.harness.charm.cinder_configuration( - dict(self.harness.model.config))) - self.assertEqual(conf['volume_backend_name'], 'test') - self.assertTrue(conf.get('use_multipath_for_image_xfer')) - self.assertTrue(conf.get('enforce_multipath_for_image_xfer')) - - def test_cinder_configuration(self): - # Add check here that configuration is as expected. - pass diff --git a/unit_tests/test_infinidat_tools.py b/unit_tests/test_infinidat_tools.py new file mode 100644 index 0000000..18c5fd4 --- /dev/null +++ b/unit_tests/test_infinidat_tools.py @@ -0,0 +1,327 @@ +# Copyright 2016 Canonical Ltd +# +# 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. + +import subprocess +import unittest +from io import StringIO +from unittest import mock +from src.charm import InfinidatToolsCharm +from ops.testing import Harness + +from ops.model import ( + ActiveStatus, + BlockedStatus, +) + +from charmhelpers.core.host_factory.ubuntu import UBUNTU_RELEASES + +SOURCE = "deb https://repo.infinidat.com/packages/main-stable/apt/linux-ubuntu focal main" # noqa: E501 +KEY = """\ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.11 (GNU/Linux) + +mQENBFESDRIBCADMR7MQMbH4GdCQqfrOMt35MhBwwH4wv9kb1WRSTxa0CmuzYaBB +1nJ0nLaMAwHsEr9CytPWDpMngm/3nt+4F2hJcsOEkQkqeJ31gScJewM+AOUV3DEl +qOeXXYLcP+jUY6pPjlZpOw0p7moUQPXHn+7amVrk7cXGQ8O3B+5a5wjN86LT2hlX +DlBlV5bX/DYluiPUbvQLOknmwO53KpaeDeZc4a8iIOCYWu2ntuAMddBkTps0El5n +JJZMTf6os2ZzngWMZRMDiVJgqVRi2b+8SgFQlQy0cAmne/mpgPrRq0ZMX3DokGG5 +hnIg1mF82laTxd+9qtiOxupzJqf8mncQHdaTABEBAAG0IWFwcF9yZXBvIChDb21t +ZW50KSA8bm9AZW1haWwuY29tPokBOAQTAQIAIgUCURINEgIbLwYLCQgHAwIGFQgC +CQoLBBYCAwECHgECF4AACgkQem2D/j05RYSrcggAsCc4KppV/SZX5XI/CWFXIAXw ++HaNsh2EwYKf9DhtoGbTOuwePvrPGcgFYM3Tu+m+rziPnnFl0bs0xwQyNEVQ9yDw +t465pSgmXwEHbBkoISV1e4WYtZAsnTNne9ieJ49Ob/WY4w3AkdPRK/41UP5Ct6lR +HHRXrSWJYHVq5Rh6BakRuMJyJLz/KvcJAaPkA4U6VrPD7PFtSecMTaONPjGCcomq +b7q84G5ZfeJWb742PWBTS8fJdC+Jd4y5fFdJS9fQwIo52Ff9In2QBpJt5Wdc02SI +fvQnuh37D2P8OcIfMxMfoFXpAMWjrMYc5veyQY1GXD/EOkfjjLne6qWPLfNojA== +=w5Os +-----END PGP PUBLIC KEY BLOCK----- +""" + + +class TestInfinidatToolsCharm(unittest.TestCase): + + def setUp(self): + self.harness = Harness(InfinidatToolsCharm) + self.addCleanup(self.harness.cleanup) + self.harness.begin() + self.harness.set_leader(True) + backend = self.harness.add_relation('juju-info', 'nova') + self.harness.add_relation_unit(backend, 'infinidat-tools/0') + + def _get_source(self, codename, pocket, baseurl=None): + if baseurl is None: + baseurl = self.harness.charm.DEFAULT_REPO_BASEURL + return ' '.join(( + 'deb', + baseurl, + codename, + pocket)) + + @mock.patch('src.charm.add_source') + @mock.patch('src.charm.apt_update') + @mock.patch('src.charm.apt_install') + @mock.patch('src.charm.lsb_release') + @mock.patch('src.charm.InfinidatToolsCharm._get_default_repo_key') + @mock.patch('src.charm.InfinidatToolsCharm._set_lvm_conf_global_filter') + @mock.patch('src.charm.InfinidatToolsCharm._run_infinihost_check') + @mock.patch('src.charm.InfinidatToolsCharm._update_multipath_conf') + @mock.patch('src.charm.InfinidatToolsCharm._regenerate_initrd') + def test_repo_management(self, _regenerate_initrd, _update_multipath_conf, + _run_infinihost_check, + _set_lvm_conf_global_filter, + _get_default_repo_key, lsb_release, apt_install, + apt_update, add_source): + + dynamic_source = self._get_source('{distrib_codename}', 'main') + + # generate test data for both 'source' values that need substituion + # and for the static ones + + test_data = [] + + for release in UBUNTU_RELEASES: + static_source = self._get_source(release, 'main') + test_data.append( + (dynamic_source, release, + self._get_source(release, 'main')), + ) + test_data.append( + (static_source, release, static_source), + ) + + for i in test_data: + # distro codename the charm runs on + lsb_release.return_value = {'DISTRIB_CODENAME': i[1]} + + # configure to use specific repo version + self.harness.update_config({ + 'install_sources': i[0], + 'install_keys': KEY, + }) + + self.harness.charm.on.install.emit() + + # make sure the repo management calls were correct + add_source.assert_called_with(i[2], KEY) + apt_install.assert_called_with(self.harness.charm.PACKAGES, + fatal=True) + + @mock.patch('subprocess.Popen') + @mock.patch('subprocess.Popen.communicate') + @mock.patch('subprocess.Popen.wait') + def test_infinibox_settings_check(self, pwait, pcomm, popen): + """ + Test basic functionality of _run_infinihost: + 1. Check that method properly handles absense of infinihost + 2. Check subprocess.PIPE + 3. Check redirect to file + 4. Check return code 0 1 2 + """ + with open('unit_tests/infinibox-sample.txt') as f: + data = f.read() + + pcomm.return_value = (data, '') + pwait.return_value = 2 + + class p: + def __init__(self): + self.communicate = pcomm + self.wait = pwait + + popen.return_value = p() + + code = self.harness.charm.\ + _run_infinihost_check(auto_fix=False, stdout=subprocess.PIPE) + pwait.assert_called_with() + self.assertEqual(code, 2) + + # test redirection to a file + f = '123' + self.harness.charm._run_infinihost_check(auto_fix=True, stdout=f) + popen.assert_called_with([ + 'infinihost', 'settings', 'check', '--auto-fix'], + stdout=f, shell=mock.ANY, env=mock.ANY) + + # make sure the missing infinihost is handled with re-raise + popen.side_effect = FileNotFoundError( + "[Errno 2] No such file or directory: 'infinihost'" + ) + self.assertRaises(FileNotFoundError, + self.harness.charm._run_infinihost_check) + + @mock.patch('src.charm.add_source') + @mock.patch('src.charm.apt_update') + @mock.patch('src.charm.apt_install') + @mock.patch('src.charm.lsb_release') + @mock.patch('src.charm.InfinidatToolsCharm._get_default_repo_key') + @mock.patch('src.charm.InfinidatToolsCharm._set_lvm_conf_global_filter') + @mock.patch('src.charm.InfinidatToolsCharm._run_infinihost_check') + @mock.patch('src.charm.InfinidatToolsCharm._update_multipath_conf') + @mock.patch('src.charm.InfinidatToolsCharm._regenerate_initrd') + def test_default_gpg_key(self, _regenerate_initrd, _update_multipath_conf, + _run_infinihost_check, + _set_lvm_conf_global_filter, + _get_default_repo_key, lsb_release, + apt_install, apt_update, add_source): + _get_default_repo_key.return_value = KEY + self.harness.update_config({ + 'install_sources': self._get_source('focal', 'main') + }) + self.harness.charm.on.install.emit() + add_source.assert_called_with(self._get_source('focal', 'main'), KEY) + + def test_multipath_config_patching(self): + pass + + @mock.patch('src.charm.InfinidatToolsCharm._set_lvm_conf_global_filter') + def test_on_config_changed(self, _set_lvm_conf_global_filter): + """ + Make sure that LVM configuration update is called + in on_config() handler + """ + self.harness.update_config({ + 'install_sources': self._get_source('focal', 'main') + }) + _set_lvm_conf_global_filter.assert_called_with( + self.harness.model.config.get('lvm_global_filter')) + + self.assertTrue(isinstance( + self.harness.model.unit.status, ActiveStatus + )) + + @mock.patch('src.charm.add_source') + @mock.patch('src.charm.apt_update') + @mock.patch('src.charm.apt_install') + @mock.patch('src.charm.lsb_release') + @mock.patch('src.charm.InfinidatToolsCharm._get_default_repo_key') + @mock.patch('src.charm.InfinidatToolsCharm._set_lvm_conf_global_filter') + @mock.patch('src.charm.InfinidatToolsCharm._run_infinihost_check') + @mock.patch('src.charm.InfinidatToolsCharm._update_multipath_conf') + @mock.patch('src.charm.InfinidatToolsCharm._regenerate_initrd') + def test_on_install_handler(self, _regenerate_initrd, + _update_multipath_conf, + _run_infinihost_check, + _set_lvm_conf_global_filter, + _get_default_repo_key, lsb_release, + apt_install, apt_update, add_source): + """ + Make sure on_install() handler does all necessary stuff: + 1. Calls infinihost with autofix + 2. Updates multipath config + 3. Sets lvm_global_filter + 4. Sets status to Active if ok + 5. Sets status to Blocked if it fails + """ + self.harness.update_config({ + 'install_sources': self._get_source('focal', 'main') + }) + self.harness.charm.on.install.emit() + + _run_infinihost_check.assert_called_with(auto_fix=True) + _update_multipath_conf.assert_called_with() + _set_lvm_conf_global_filter.assert_called_with( + self.harness.model.config.get('lvm_global_filter')) + + self.assertTrue(isinstance( + self.harness.model.unit.status, ActiveStatus + )) + _run_infinihost_check.side_effect = FileNotFoundError( + "[Errno 2] No such file or directory: 'infinihost'" + ) + + self.harness.charm.on.install.emit() + + self.assertTrue(isinstance( + self.harness.model.unit.status, BlockedStatus + )) + + @mock.patch('builtins.open') + def test_lvm_config_patching(self, _open): + + tpl = r"""# Configuration option devices/global_filter. +# Limit the block devices that are used by LVM system components. +# Because devices/filter may be overridden from the command line, it is +# not suitable for system-wide device filtering, e.g. udev. +# Use global_filter to hide devices from these LVM system components. +# The syntax is the same as devices/filter. Devices rejected by +# global_filter are not opened by LVM. +# This configuration option has an automatic default value. +# global_filter = [ "a|.*|" ]{0} +# other contents +""" + input_data = tpl.format('') + + value = '[ "a|^/dev/sd.*|", "a|^/dev/vd.*|", "r|.*|" ]' + updated_data = tpl.format( + '\nglobal_filter = ' + value + + ' # __infinidat_tools__' + ) + + self.maxDiff = 1000 + updated_data2 = tpl.format( + '\nglobal_filter = ' + value + + ' 2 # __infinidat_tools__' + ) + + class fake_fh: + def __init__(self, expected): + self.buf = StringIO(expected) + self.newbuf = StringIO('') + self.readlines = self.buf.readlines + self.read = self.buf.read + self.write = self.newbuf.write + + def __enter__(self, *args, **kwargs): + return self + + def __exit__(self, *args, **kwargs): + pass + + d = fake_fh(input_data) + _open.return_value = d + + self.harness.charm._set_lvm_conf_global_filter(value) + + self.assertEqual(d.newbuf.getvalue(), updated_data) + + d = fake_fh(d.newbuf.getvalue()) + _open.return_value = d + + self.harness.charm._set_lvm_conf_global_filter(value + ' 2') + + self.assertEqual(d.newbuf.getvalue(), updated_data2) + + d = fake_fh(updated_data) + _open.return_value = d + self.harness.charm._set_lvm_conf_global_filter('') + + self.assertEqual(d.newbuf.getvalue(), input_data) + + d = fake_fh(input_data) + _open.return_value = d + self.harness.charm._set_lvm_conf_global_filter('') + + self.assertEqual(d.newbuf.getvalue(), input_data) + + def check_contents(self): + pass + + def test_action_outfile_created(self): + pass + + def test_action_no_autofix(self): + pass + + def test_action_autofix(self): + pass