Rework charm to use cinder sub plugin

This commit is contained in:
Luciano Lo Giudice
2022-04-18 17:42:51 -03:00
parent 5f323a7ecb
commit db316aa466
11 changed files with 134 additions and 212 deletions

View File

@@ -1 +1,7 @@
git+https://github.com/canonical/charmcraft.git@0.10.2#egg=charmcraft
# NOTES(lourot):
# * We don't install charmcraft via pip anymore because it anyway spins up a
# container and scp the system's charmcraft snap inside it. So the charmcraft
# snap is necessary on the system anyway.
# * `tox -e build` successfully validated with charmcraft 1.2.1
cffi==1.14.6; python_version < '3.6' # cffi 1.15.0 drops support for py35.

32
charmcraft.yaml Normal file
View File

@@ -0,0 +1,32 @@
type: charm
parts:
charm:
after:
- update-certificates
charm-python-packages:
# NOTE(lourot): see
# * https://github.com/canonical/charmcraft/issues/551
# * https://github.com/canonical/charmcraft/issues/632
- setuptools < 58
build-packages:
- git
update-certificates:
plugin: nil
# See https://github.com/canonical/charmcraft/issues/658
override-build: |
apt update
apt install -y ca-certificates
update-ca-certificates
bases:
- build-on:
- name: ubuntu
channel: "20.04"
architectures:
- amd64
run-on:
- name: ubuntu
channel: "20.04"
architectures: [amd64, s390x, ppc64el, arm64]
- name: ubuntu
channel: "22.04"
architectures: [amd64, s390x, ppc64el, arm64]

View File

@@ -27,7 +27,7 @@ options:
raise an exception.
hpe3par-iscsi-ips:
type: string
default: ''
default:
description: |
Comma-separated list of IP:PORT to be used for the iscsi connection.
hpe3par-iscsi-chap-enabled:
@@ -69,39 +69,39 @@ options:
type: string
description: |
Password for SAN controller for SSH access to the array
default: ''
default:
hpe3par-username:
type: string
description: |
3PAR username with the 'edit' role
default: ''
default:
hpe3par-password:
type: string
description: |
3PAR password for the user specified in hpe3par_username
default: ''
default:
hpe3par-api-url:
type: string
description: |
3PAR WS API Server URL
default: ''
default:
hpe3par-cpg:
type: string
description: |
3PAR CPG to use for volume creation
default: ''
default:
hpe3par_cpg_snap:
type: string
default: ''
default:
description: |
Sets the CPG name for the snapshot.
If empty, use the userCPG will be used.
hpe3par_target_nsp:
type: string
default: ''
default:
description: |
volume-backend-name:
type: string
description: |
Name to present to cinder, leave blank to use charm (or alias) name
default: ''
default:

View File

@@ -1,7 +1,7 @@
Format: http://dep.debian.net/deps/dep5/
Files: *
Copyright: Copyright 2015-2022, Canonical Ltd., All Rights Reserved.
Copyright: Copyright 2021-2022, Canonical Ltd., All Rights Reserved.
License: Apache License 2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@@ -3,8 +3,9 @@
- charm-unit-jobs
check:
jobs:
- bionic-queens
- focal-ussuri
- focal-yoga
- jammy-yoga
vars:
needs_charm_build: true
build_type: charmcraft

13
rename.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
echo "renaming ${charm}_*.charm to ${charm}.charm"
echo -n "pwd: "
pwd
ls -al
echo "Removing bad downloaded charm maybe?"
if [[ -e "${charm}.charm" ]];
then
rm "${charm}.charm"
fi
echo "Renaming charm here."
mv ${charm}_*.charm ${charm}.charm

View File

@@ -22,6 +22,8 @@ from ops.main import main
from ops.framework import StoredState
from ops.model import MaintenanceStatus, ActiveStatus, BlockedStatus
from ops_openstack.plugins.classes import CinderStoragePluginCharm
from charmhelpers.fetch.ubuntu import apt_install
logger = logging.getLogger(__name__)
@@ -35,23 +37,25 @@ REQUIRED_OPTS = [
'hpe3par-password',
'san-ip',
'san-login',
'san-password']
'san-password',
]
# Based on initialize-iscsi-ports() in
# https://github.com/openstack/cinder/blob/master/cinder/ \
# volume/drivers/hpe/hpe-3par-iscsi.py
REQUIRED_OPTS_ISCSI = [
'hpe3par-iscsi-ips']
REQUIRED_OPTS_ISCSI = ['hpe3par-iscsi-ips']
class InvalidConfigError(Exception):
"""Exception raised on invalid configurations."""
def __init__(self, msg):
self.msg = msg
pass
def __str__(self):
return self.msg
class MissingConfigError(Exception):
"""Exception raised on missing 3PAR config parameter."""
pass
def CinderThreeParContext(charm_config, service):
@@ -67,10 +71,7 @@ def CinderThreeParContext(charm_config, service):
"""
ctxt = []
for key in charm_config.keys():
if key == 'volume-backend-name':
ctxt.append((key, service))
else:
ctxt.append((key.replace('-', '_'), charm_config[key]))
ctxt.append((key.replace('-', '_'), charm_config[key]))
if charm_config['driver-type'] == 'fc':
ctxt.append((
'volume_driver',
@@ -81,118 +82,49 @@ def CinderThreeParContext(charm_config, service):
'cinder.volume.drivers.hpe.hpe_3par_iscsi.HPE3PARISCSIDriver'))
else:
raise InvalidConfigError('Invalid config (driver-type)')
return {
"cinder": {
"/etc/cinder/cinder.conf": {
"sections": {
service: ctxt
}
}
}
}
return ctxt
class CharmCinderThreeParCharm(CharmBase):
class CharmCinderThreeParCharm(CinderStoragePluginCharm):
"""Charm the Cinder HPE 3PAR driver."""
_stored = StoredState()
MANDATORY_CONFIG = REQUIRED_OPTS
PACKAGES = ['python3-3parclient', 'sysfsutils']
def __init__(self, *args):
super().__init__(*args)
self.framework.observe(
self.on.install,
self._on_install)
self.framework.observe(
self.on.config_changed,
self._on_config_changed_or_upgrade)
self.framework.observe(
self.on.upgrade_charm,
self._on_config_changed_or_upgrade)
self.framework.observe(
self.on.storage_backend_relation_joined,
self._on_render_storage_backend)
self.framework.observe(
self.on.storage_backend_relation_changed,
self._on_render_storage_backend)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._stored.is_started = True
def _rel_get_remote_units(self, rel_name):
"""Get relations remote units"""
return self.framework.model.get_relation(rel_name).units
def _on_install(self, _):
"""Install packages"""
self.unit.status = MaintenanceStatus(
"Installing packages")
# os_brick lib needs systool from sysfsutils to be able to retrieve
# the data from FC links:
# https://github.com/openstack/os-brick/blob/ \
# 1b2e2295421615847d86508dcd487ec51fa45f25/ \
# os_brick/initiator/linuxfc.py#L151
apt_install(['python3-3parclient',
'sysfsutils'])
self.unit.status = ActiveStatus("Unit is ready")
def _on_config_changed_or_upgrade(self, event):
"""Update on changed config or charm upgrade"""
svc_name = self.framework.model.app.name
# Copying to a new dict as charm_config will be edited according to
# the settings
charm_config = dict(self.framework.model.config)
if not self.check_config(charm_config):
# The config checks failed, drop this event as the operator
# needs to intervene manually
return
r = self.framework.model.relations.get('storage-backend')[0]
def on_config(self, event):
prev_status = self.unit.status
try:
ctx = CinderThreeParContext(charm_config, svc_name)
super().on_config(event)
except InvalidConfigError as e:
self.unit.status = BlockedStatus(str(e))
return
finally:
self.unit.status = prev_status
for u in self._rel_get_remote_units('storage-backend'):
r.data[self.unit]['backend_name'] = \
charm_config['volume-backend-name'] or svc_name
r.data[self.unit]['subordinate_configuration'] = json.dumps(ctx)
self.unit.status = ActiveStatus("Unit is ready")
def _on_config(self, event):
# The list of mandatory config options isn't static for this
# charm, so we need to manually adjust here.
if self.framework.model.config.get('driver-type') == 'iscsi':
self.MANDATORY_CONFIG = REQUIRED_OPTS + REQUIRED_OPTS_ISCSI
else:
self.MANDATORY_CONFIG = REQUIRED_OPTS
def _on_render_storage_backend(self, event):
"""Render the current configuration"""
super()._on_config(event)
def cinder_configuration(self, charm_config):
svc_name = self.framework.model.app.name
charm_config = self.framework.model.config
data = event.relation.data
data[self.unit]['backend_name'] = \
charm_config['volume-backend-name'] or svc_name
try:
ctx = CinderThreeParContext(charm_config, svc_name)
except InvalidConfigError as e:
self.unit.status = BlockedStatus(str(e))
return
data[self.unit]['subordinate_configuration'] = json.dumps(ctx)
def check_config(self, charm_config):
"""Check whether required options are set."""
# According to the HPE 3par driver code, expiration and retention can
# be left unset and won't be configured:
# https://github.com/openstack/cinder/blob/stable/ussuri/cinder/ \
# volume/drivers/hpe/hpe_3par_common.py#L2834
for opt in ("hpe3par-snapshot-retention",
"hpe3par-snapshot-expiration"):
# Setting as < 0 will remove the given option from the request.
for opt in ('hpe3par-snapshot-retention',
'hpe3par-snapshot-expiration'):
if charm_config.get(opt, -1) < 0:
charm_config.pop(opt, None)
required_opts = REQUIRED_OPTS
charm_config = self.framework.model.config
if charm_config['driver-type'] == 'iscsi':
required_opts += REQUIRED_OPTS_ISCSI
missing_opts = set(required_opts) - set(charm_config.keys())
if missing_opts:
self.unit.status = BlockedStatus(
'Missing options: {}'.format(','.join(missing_opts)))
return False
else:
self.unit.status = MaintenanceStatus("Sharing configs with Cinder")
return True
return CinderThreeParContext(charm_config, svc_name)
if __name__ == "__main__":

View File

@@ -1,49 +0,0 @@
series: bionic
comment:
- 'machines section to decide order of deployment. database sooner = faster'
machines:
'0':
constraints: mem=3072M
'1':
'2':
constraints: mem=4G root-disk=16G
'3':
local_overlay_enabled: false
relations:
- - keystone:shared-db
- mysql:shared-db
- - cinder:shared-db
- mysql:shared-db
- - cinder:identity-service
- keystone:identity-service
- - cinder:amqp
- rabbitmq-server:amqp
- - cinder:storage-backend
- cinder-three-par:storage-backend
applications:
mysql:
charm: cs:~openstack-charmers-next/percona-cluster
num_units: 1
to:
- '0'
keystone:
charm: cs:~openstack-charmers-next/keystone
num_units: 1
to:
- '1'
cinder:
charm: cs:~openstack-charmers-next/cinder
num_units: 1
options:
block-device: /dev/vdb
overwrite: "true"
ephemeral-unmount: /mnt
to:
- '2'
cinder-three-par:
charm: ../../cinder-three-par.charm
rabbitmq-server:
charm: cs:~openstack-charmers-next/rabbitmq-server
num_units: 1
to:
- '3'

View File

@@ -4,11 +4,14 @@ tests:
configure:
- zaza.openstack.charm_tests.keystone.setup.add_demo_user
gate_bundles:
- bionic-queens
- focal-ussuri
- focal-yoga
- jammy-yoga
smoke_bundles:
- bionic-queens
- focal-ussuri
- focal-yoga
- jammy-yoga
dev_bundles:
- bionic-queens
- focal-ussuri
- focal-yoga
- jammy-yoga

39
tox.ini
View File

@@ -1,5 +1,4 @@
# Operator charm (with zaza): tox.ini
[tox]
envlist = pep8,py3
skipsdist = True
@@ -15,11 +14,14 @@ skip_missing_interpreters = False
# * It is also necessary to pin virtualenv as a newer virtualenv would still
# lead to fetching the latest pip in the func* tox targets, see
# https://stackoverflow.com/a/38133283
# * It is necessary to declare setuptools as a dependency otherwise tox will
# fail very early at not being able to load it. The version pinning is in
# line with `pip.sh`.
requires = pip < 20.3
virtualenv < 20.0
setuptools < 50.0.0
# NOTE: https://wiki.canonical.com/engineering/OpenStack/InstallLatestToxOnOsci
minversion = 3.2.0
[testenv]
setenv = VIRTUAL_ENV={envdir}
PYTHONHASHSEED=0
@@ -27,44 +29,47 @@ setenv = VIRTUAL_ENV={envdir}
install_command =
pip install {opts} {packages}
commands = stestr run --slowest {posargs}
whitelist_externals =
allowlist_externals =
git
add-to-archive.py
bash
charmcraft
rename.sh
passenv = HOME TERM CS_* OS_* TEST_*
deps = -r{toxinidir}/test-requirements.txt
[testenv:py35]
basepython = python3.5
# python3.5 is irrelevant on a focal+ charm.
commands = /bin/true
[testenv:py36]
basepython = python3.6
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
[testenv:py37]
basepython = python3.7
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
[testenv:py38]
basepython = python3.8
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
[testenv:py39]
basepython = python3.9
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
[testenv:py310]
basepython = python3.10
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
[testenv:py3]
basepython = python3
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
[testenv:pep8]
basepython = python3
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands = flake8 {posargs} src unit_tests tests
[testenv:cover]
# Technique based heavily upon
# https://github.com/openstack/nova/blob/master/tox.ini
@@ -81,7 +86,6 @@ commands =
coverage html -d cover
coverage xml -o cover/coverage.xml
coverage report
[coverage:run]
branch = True
concurrency = multiprocessing
@@ -92,42 +96,37 @@ omit =
.tox/*
*/charmhelpers/*
unit_tests/*
[testenv:venv]
basepython = python3
commands = {posargs}
[testenv:build]
basepython = python3
deps = -r{toxinidir}/build-requirements.txt
commands =
charmcraft build
charmcraft clean
charmcraft -v build
{toxinidir}/rename.sh
[testenv:func-noop]
basepython = python3
commands =
functest-run-suite --help
[testenv:func]
basepython = python3
commands =
functest-run-suite --keep-model
[testenv:func-smoke]
basepython = python3
commands =
functest-run-suite --keep-model --smoke
[testenv:func-dev]
basepython = python3
commands =
functest-run-suite --keep-model --dev
[testenv:func-target]
basepython = python3
commands =
functest-run-suite --keep-model --bundle {posargs}
[flake8]
# Ignore E902 because the unit_tests directory is missing in the built charm.
ignore = E402,E226,E902

View File

@@ -16,7 +16,7 @@ import unittest
import json
import copy
from ops.model import Relation, BlockedStatus
from ops.model import Relation, BlockedStatus, ActiveStatus
from ops.testing import Harness
from src.charm import CharmCinderThreeParCharm
@@ -29,7 +29,6 @@ TEST_3PAR_CONFIG = {
['driver_type', 'fc'],
['use_multipath_image_xfer', False],
['enforce_multipath_for_image_xfer', False],
['hpe3par_iscsi_ips', ''],
['hpe3par_iscsi_chap_enabled', True],
['hpe3par_snapshot_expiration', 72],
['hpe3par_snapshot_retention', 48],
@@ -37,14 +36,7 @@ TEST_3PAR_CONFIG = {
['reserved_percentage', 15],
['san_ip', '0.0.0.0'],
['san_login', 'some-login'],
['san_password', ''],
['hpe3par_username', ''],
['hpe3par_password', ''],
['hpe3par_api_url', ''],
['hpe3par_cpg', ''],
['hpe3par_cpg_snap', ''],
['hpe3par_target_nsp', ''],
['volume-backend-name', 'cinder-three-par'],
['volume_backend_name', 'cinder-three-par'],
['volume_driver',
'cinder.volume.drivers.hpe.hpe_3par_fc.HPE3PARFCDriver']
]
@@ -62,20 +54,14 @@ TEST_3PAR_CONFIG_CHANGED = {
['driver_type', 'fc'],
['use_multipath_image_xfer', False],
['enforce_multipath_for_image_xfer', False],
['hpe3par_iscsi_ips', ''],
['hpe3par_iscsi_chap_enabled', True],
['max_over_subscription_ratio', 20.0],
['reserved_percentage', 15],
['san_ip', '1.2.3.4'],
['san_login', 'login'],
['san_password', 'pwd'],
['hpe3par_username', ''],
['hpe3par_password', ''],
['hpe3par_api_url', 'test.url'],
['hpe3par_cpg', ''],
['hpe3par_cpg_snap', ''],
['hpe3par_target_nsp', ''],
['volume-backend-name', 'cinder-three-par'],
['volume_backend_name', 'cinder-three-par'],
['volume_driver',
'cinder.volume.drivers.hpe.hpe_3par_fc.HPE3PARFCDriver']
]
@@ -127,13 +113,12 @@ class TestCharm(unittest.TestCase):
def test_blocked_status(self):
self.harness.update_config(unset=["san-ip", "san-login"])
self.harness.charm.on.update_status.emit()
self.assertEqual(
self.harness.charm.unit.status.message,
'Missing options: san-login,san-ip')
self.assertIsInstance(
self.harness.charm.unit.status,
BlockedStatus)
message = self.harness.charm.unit.status.message
self.assertIn('san-login', message)
self.assertIn('san-ip', message)
def test_blocked_unset_retention_expiration(self):
self.harness.update_config({
@@ -167,5 +152,5 @@ class TestCharm(unittest.TestCase):
def test_invalid_config_driver_type(self):
self.harness.update_config({'driver-type': '???'})
self.assertIsInstance(self.harness.charm.unit.status,
BlockedStatus)
self.assertFalse(isinstance(self.harness.charm.unit.status,
ActiveStatus))