Merged branch refactor onto main

- implemented basic charm functionality and configuration
- unit-tests
- pep8
This commit is contained in:
Nikolay Vinogradov 2022-10-15 03:45:57 +03:00
parent 00989d8d79
commit 2ab26e69f5
12 changed files with 764 additions and 124 deletions

View File

@ -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
=============

9
actions.yaml Normal file
View File

@ -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.

View File

@ -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|.*|\" ]"

View File

@ -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 <openstack-charmers@lists.ubuntu.com>
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:

View File

@ -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

View File

@ -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)

View File

@ -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
}
}

View File

@ -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 ]
- [ cinder:juju-info, infinidat-tools:juju-info ]

View File

@ -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'],
}
}

View File

@ -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

View File

@ -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

View File

@ -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