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. raise an exception.
hpe3par-iscsi-ips: hpe3par-iscsi-ips:
type: string type: string
default: '' default:
description: | description: |
Comma-separated list of IP:PORT to be used for the iscsi connection. Comma-separated list of IP:PORT to be used for the iscsi connection.
hpe3par-iscsi-chap-enabled: hpe3par-iscsi-chap-enabled:
@@ -69,39 +69,39 @@ options:
type: string type: string
description: | description: |
Password for SAN controller for SSH access to the array Password for SAN controller for SSH access to the array
default: '' default:
hpe3par-username: hpe3par-username:
type: string type: string
description: | description: |
3PAR username with the 'edit' role 3PAR username with the 'edit' role
default: '' default:
hpe3par-password: hpe3par-password:
type: string type: string
description: | description: |
3PAR password for the user specified in hpe3par_username 3PAR password for the user specified in hpe3par_username
default: '' default:
hpe3par-api-url: hpe3par-api-url:
type: string type: string
description: | description: |
3PAR WS API Server URL 3PAR WS API Server URL
default: '' default:
hpe3par-cpg: hpe3par-cpg:
type: string type: string
description: | description: |
3PAR CPG to use for volume creation 3PAR CPG to use for volume creation
default: '' default:
hpe3par_cpg_snap: hpe3par_cpg_snap:
type: string type: string
default: '' default:
description: | description: |
Sets the CPG name for the snapshot. Sets the CPG name for the snapshot.
If empty, use the userCPG will be used. If empty, use the userCPG will be used.
hpe3par_target_nsp: hpe3par_target_nsp:
type: string type: string
default: '' default:
description: | description: |
volume-backend-name: volume-backend-name:
type: string type: string
description: | description: |
Name to present to cinder, leave blank to use charm (or alias) name 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/ Format: http://dep.debian.net/deps/dep5/
Files: * Files: *
Copyright: Copyright 2015-2022, Canonical Ltd., All Rights Reserved. Copyright: Copyright 2021-2022, Canonical Ltd., All Rights Reserved.
License: Apache License 2.0 License: Apache License 2.0
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@@ -3,8 +3,9 @@
- charm-unit-jobs - charm-unit-jobs
check: check:
jobs: jobs:
- bionic-queens
- focal-ussuri - focal-ussuri
- focal-yoga
- jammy-yoga
vars: vars:
needs_charm_build: true needs_charm_build: true
build_type: charmcraft 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.framework import StoredState
from ops.model import MaintenanceStatus, ActiveStatus, BlockedStatus from ops.model import MaintenanceStatus, ActiveStatus, BlockedStatus
from ops_openstack.plugins.classes import CinderStoragePluginCharm
from charmhelpers.fetch.ubuntu import apt_install from charmhelpers.fetch.ubuntu import apt_install
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -35,23 +37,25 @@ REQUIRED_OPTS = [
'hpe3par-password', 'hpe3par-password',
'san-ip', 'san-ip',
'san-login', 'san-login',
'san-password'] 'san-password',
]
# Based on initialize-iscsi-ports() in # Based on initialize-iscsi-ports() in
# https://github.com/openstack/cinder/blob/master/cinder/ \ # https://github.com/openstack/cinder/blob/master/cinder/ \
# volume/drivers/hpe/hpe-3par-iscsi.py # volume/drivers/hpe/hpe-3par-iscsi.py
REQUIRED_OPTS_ISCSI = [ REQUIRED_OPTS_ISCSI = ['hpe3par-iscsi-ips']
'hpe3par-iscsi-ips']
class InvalidConfigError(Exception): class InvalidConfigError(Exception):
"""Exception raised on invalid configurations.""" """Exception raised on invalid configurations."""
def __init__(self, msg): pass
self.msg = msg
def __str__(self):
return self.msg class MissingConfigError(Exception):
"""Exception raised on missing 3PAR config parameter."""
pass
def CinderThreeParContext(charm_config, service): def CinderThreeParContext(charm_config, service):
@@ -67,10 +71,7 @@ def CinderThreeParContext(charm_config, service):
""" """
ctxt = [] ctxt = []
for key in charm_config.keys(): for key in charm_config.keys():
if key == 'volume-backend-name': ctxt.append((key.replace('-', '_'), charm_config[key]))
ctxt.append((key, service))
else:
ctxt.append((key.replace('-', '_'), charm_config[key]))
if charm_config['driver-type'] == 'fc': if charm_config['driver-type'] == 'fc':
ctxt.append(( ctxt.append((
'volume_driver', 'volume_driver',
@@ -81,118 +82,49 @@ def CinderThreeParContext(charm_config, service):
'cinder.volume.drivers.hpe.hpe_3par_iscsi.HPE3PARISCSIDriver')) 'cinder.volume.drivers.hpe.hpe_3par_iscsi.HPE3PARISCSIDriver'))
else: else:
raise InvalidConfigError('Invalid config (driver-type)') raise InvalidConfigError('Invalid config (driver-type)')
return { return ctxt
"cinder": {
"/etc/cinder/cinder.conf": {
"sections": {
service: ctxt
}
}
}
}
class CharmCinderThreeParCharm(CharmBase): class CharmCinderThreeParCharm(CinderStoragePluginCharm):
"""Charm the Cinder HPE 3PAR driver.""" """Charm the Cinder HPE 3PAR driver."""
_stored = StoredState() MANDATORY_CONFIG = REQUIRED_OPTS
PACKAGES = ['python3-3parclient', 'sysfsutils']
def __init__(self, *args): def __init__(self, *args, **kwargs):
super().__init__(*args) super().__init__(*args, **kwargs)
self.framework.observe( self._stored.is_started = True
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 _rel_get_remote_units(self, rel_name): def on_config(self, event):
"""Get relations remote units""" prev_status = self.unit.status
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]
try: try:
ctx = CinderThreeParContext(charm_config, svc_name) super().on_config(event)
except InvalidConfigError as e: except InvalidConfigError as e:
self.unit.status = BlockedStatus(str(e)) self.unit.status = BlockedStatus(str(e))
return finally:
self.unit.status = prev_status
for u in self._rel_get_remote_units('storage-backend'): def _on_config(self, event):
r.data[self.unit]['backend_name'] = \ # The list of mandatory config options isn't static for this
charm_config['volume-backend-name'] or svc_name # charm, so we need to manually adjust here.
r.data[self.unit]['subordinate_configuration'] = json.dumps(ctx) if self.framework.model.config.get('driver-type') == 'iscsi':
self.unit.status = ActiveStatus("Unit is ready") self.MANDATORY_CONFIG = REQUIRED_OPTS + REQUIRED_OPTS_ISCSI
else:
self.MANDATORY_CONFIG = REQUIRED_OPTS
def _on_render_storage_backend(self, event): super()._on_config(event)
"""Render the current configuration"""
def cinder_configuration(self, charm_config):
svc_name = self.framework.model.app.name 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 # According to the HPE 3par driver code, expiration and retention can
# be left unset and won't be configured: # be left unset and won't be configured:
# https://github.com/openstack/cinder/blob/stable/ussuri/cinder/ \ # https://github.com/openstack/cinder/blob/stable/ussuri/cinder/ \
# volume/drivers/hpe/hpe_3par_common.py#L2834 # volume/drivers/hpe/hpe_3par_common.py#L2834
for opt in ("hpe3par-snapshot-retention", for opt in ('hpe3par-snapshot-retention',
"hpe3par-snapshot-expiration"): 'hpe3par-snapshot-expiration'):
# Setting as < 0 will remove the given option from the request.
if charm_config.get(opt, -1) < 0: if charm_config.get(opt, -1) < 0:
charm_config.pop(opt, None) charm_config.pop(opt, None)
required_opts = REQUIRED_OPTS return CinderThreeParContext(charm_config, svc_name)
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
if __name__ == "__main__": 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: configure:
- zaza.openstack.charm_tests.keystone.setup.add_demo_user - zaza.openstack.charm_tests.keystone.setup.add_demo_user
gate_bundles: gate_bundles:
- bionic-queens
- focal-ussuri - focal-ussuri
- focal-yoga
- jammy-yoga
smoke_bundles: smoke_bundles:
- bionic-queens
- focal-ussuri - focal-ussuri
- focal-yoga
- jammy-yoga
dev_bundles: dev_bundles:
- bionic-queens
- focal-ussuri - focal-ussuri
- focal-yoga
- jammy-yoga

39
tox.ini
View File

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

View File

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