Merge "Rewrite update status machinery with the ops framework"

This commit is contained in:
Zuul 2022-10-07 14:45:06 +00:00 committed by Gerrit Code Review
commit 11b7a7340b
10 changed files with 485 additions and 390 deletions

View File

@ -44,13 +44,12 @@ from charmhelpers.core.hookenv import (
leader_set, leader_get,
is_leader,
remote_unit,
Hooks, UnregisteredHookError,
Hooks,
service_name,
relations_of_type,
relations,
status_set,
local_unit,
application_version_set)
)
from charmhelpers.core.host import (
service_pause,
mkdir,
@ -61,14 +60,12 @@ from charmhelpers.fetch import (
apt_install,
filter_installed_packages,
add_source,
get_upstream_version,
)
from charmhelpers.contrib.openstack.alternatives import install_alternative
from charmhelpers.contrib.openstack.utils import (
clear_unit_paused,
clear_unit_upgrading,
get_os_codename_install_source,
is_unit_upgrading_set,
set_unit_paused,
set_unit_upgrading,
)
@ -82,19 +79,15 @@ from charmhelpers.core.templating import render
from charmhelpers.contrib.storage.linux.ceph import (
CephBrokerRq,
CephConfContext,
OSD_SETTING_EXCEPTIONS,
enable_pg_autoscale,
get_osd_settings,
send_osd_settings,
)
from utils import (
add_rbd_mirror_features,
assert_charm_supports_ipv6,
get_cluster_addr,
get_networks,
get_public_addr,
get_rbd_features,
has_rbd_mirrors,
get_ceph_osd_releases,
execute_post_osd_upgrade_steps,
mgr_disable_module,
@ -1291,94 +1284,6 @@ def is_unsupported_cmr(unit_name):
return unsupported
def assess_status(charm=None):
'''Assess status of current unit'''
application_version_set(get_upstream_version(VERSION_PACKAGE))
if not config('permit-insecure-cmr'):
units = [unit
for rtype in relations()
for relid in relation_ids(reltype=rtype)
for unit in related_units(relid=relid)
if is_cmr_unit(unit)]
if units:
status_set("blocked", "Unsupported CMR relation")
return
if is_unit_upgrading_set():
status_set("blocked",
"Ready for do-release-upgrade and reboot. "
"Set complete when finished.")
return
# Check that the no-bootstrap config option is set in conjunction with
# having the bootstrap-source relation established
if not config('no-bootstrap') and is_relation_made('bootstrap-source'):
status_set('blocked', 'Cannot join the bootstrap-source relation when '
'no-bootstrap is False')
return
moncount = int(config('monitor-count'))
units = get_peer_units()
# not enough peers and mon_count > 1
if len(units.keys()) < moncount:
status_set('blocked', 'Insufficient peer units to bootstrap'
' cluster (require {})'.format(moncount))
return
# mon_count > 1, peers, but no ceph-public-address
ready = sum(1 for unit_ready in units.values() if unit_ready)
if ready < moncount:
status_set('waiting', 'Peer units detected, waiting for addresses')
return
configured_rbd_features = config('default-rbd-features')
if has_rbd_mirrors() and configured_rbd_features:
if add_rbd_mirror_features(
configured_rbd_features) != configured_rbd_features:
# The configured RBD features bitmap does not contain the features
# required for RBD Mirroring
status_set('blocked', 'Configuration mismatch: RBD Mirroring '
'enabled but incorrect value set for '
'``default-rbd-features``')
return
try:
get_osd_settings('client')
except OSD_SETTING_EXCEPTIONS as e:
status_set('blocked', str(e))
return
if charm is not None and charm.metrics_endpoint.assess_alert_rule_errors():
return
# active - bootstrapped + quorum status check
if ceph.is_bootstrapped() and ceph.is_quorum():
expected_osd_count = config('expected-osd-count') or 3
if sufficient_osds(expected_osd_count):
status_set('active', 'Unit is ready and clustered')
elif not relation_ids('osd'):
status_set('blocked', 'Missing relation: OSD')
else:
status_set(
'waiting',
'Monitor bootstrapped but waiting for number of'
' OSDs to reach expected-osd-count ({})'
.format(expected_osd_count)
)
else:
# Unit should be running and clustered, but no quorum
# TODO: should this be blocked or waiting?
status_set('blocked', 'Unit not clustered (no quorum)')
# If there's a pending lock for this unit,
# can i get the lock?
# reboot the ceph-mon process
@hooks.hook('update-status')
@harden()
def update_status():
log('Updating status.')
@hooks.hook('pre-series-upgrade')
def pre_series_upgrade():
log("Running prepare series upgrade hook", "INFO")
@ -1398,18 +1303,3 @@ def post_series_upgrade():
# upgrading states.
clear_unit_paused()
clear_unit_upgrading()
if __name__ == '__main__':
remote_block = False
remote_unit_name = remote_unit()
if remote_unit_name and is_cmr_unit(remote_unit_name):
remote_block = not config('permit-insecure-cmr')
if remote_block:
log("Not running hook, CMR detected and not supported", "ERROR")
else:
try:
hooks.execute(sys.argv)
except UnregisteredHookError as e:
log('Unknown hook {} - skipping.'.format(e))
assess_status()

View File

@ -12,7 +12,6 @@ import pathlib
from typing import Optional, Union, List, TYPE_CHECKING
import ops.model
from ops.model import BlockedStatus
if TYPE_CHECKING:
import charm
@ -81,12 +80,8 @@ class CephMetricsEndpointProvider(prometheus_scrape.MetricsEndpointProvider):
# We're not related to prom, don't care about alert rules
self._charm._stored.alert_rule_errors = None
def assess_alert_rule_errors(self):
if self._charm._stored.alert_rule_errors:
self._charm.unit.status = BlockedStatus(
"invalid alert rules, check unit logs"
)
return True
def have_alert_rule_errors(self):
return bool(self._charm._stored.alert_rule_errors)
def _on_alert_rule_status_changed(self, event):
logger.debug(

88
src/ceph_shared.py Normal file
View File

@ -0,0 +1,88 @@
# Copyright 2022 Canonical Ltd.
# See LICENSE file for licensing details.
"""Shared operator framework code
Provide helpers for querying current status of ceph-mon units
"""
import logging
from typing import Mapping, List, Dict, TYPE_CHECKING
from ops import model, framework
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
import charm
class CephMonInfo(framework.Object):
"""Provide status information about ceph-mon.
Information about
- Relations
- Peer information
- CMR units
"""
def __init__(self, charm: "charm.CephMonCharm"):
super().__init__(charm, "moninfo")
self.charm = charm
@property
def relations(self) -> Mapping[str, List[model.Relation]]:
return self.charm.model.relations
def get_peer_mons(self) -> Dict[model.Unit, model.RelationDataContent]:
"""Retrieve information about ceph-mon peer units."""
return self._get_related_unit_data("mon")
def get_osd_units(self) -> Dict[model.Unit, model.RelationDataContent]:
"""Retrieve information about related osd units."""
return self._get_related_unit_data("osd")
def _get_related_unit_data(
self, reltype: str
) -> Dict[model.Unit, model.RelationDataContent]:
rel_units = [
unit for rel in self.relations[reltype] for unit in rel.units
]
rel_data = {}
for rel in self.relations[reltype]:
for unit in rel_units:
rel_data[unit] = rel.data.get(unit, {})
return rel_data
def remote_units(self) -> List[model.Unit]:
"""Retrieve related CMR units."""
remotes = [
unit
for reltype in self.relations.values()
for rel in reltype
for unit in rel.units
if unit.name.startswith("remote-")
]
return remotes
def sufficient_osds(self, minimum_osds: int = 3) -> bool:
"""
Determine if the minimum number of OSD's have been
bootstrapped into the cluster.
:param expected_osds: The minimum number of OSD's required
:return: boolean indicating whether the required number of
OSD's where detected.
"""
osds = self.get_osd_units()
bootstrapped_osds = sum(
int(osd.get("bootstrapped-osds"))
for osd in osds.values()
if osd.get("bootstrapped-osds")
)
if bootstrapped_osds >= minimum_osds:
return True
return False
def have_osd_relation(self) -> bool:
return bool(self.relations["osd"])

147
src/ceph_status.py Normal file
View File

@ -0,0 +1,147 @@
# Copyright 2022 Canonical Ltd.
# See LICENSE file for licensing details.
"""Provide status checking for the ceph-mon charm"""
import logging
from typing import Union, TYPE_CHECKING
from charmhelpers.core.hookenv import (
application_version_set,
is_relation_made,
)
from charmhelpers.fetch import get_upstream_version
from ops import model
import utils
if TYPE_CHECKING:
import charm
from charmhelpers.contrib.storage.linux import ceph as ch_ceph
import charms_ceph.utils as ceph_utils
import ceph_shared
logger = logging.getLogger(__name__)
VERSION_PACKAGE = "ceph-common"
class StatusAssessor(ceph_shared.CephMonInfo):
"""Status checking for ceph-mon charms
Takes a ceph-mon charm object as a client, registers checking methods for
the charm object and updates status.
"""
def __init__(self, charm: "charm.CephMonCharm"):
super().__init__(charm)
self.framework.observe(
self.framework.on.commit, self.assess_status
)
self.register_checks()
def config(self, key) -> Union[str, int, float, bool, None]:
return self.charm.model.config.get(key)
def check_insecure_cmr(self) -> model.StatusBase:
if not self.config("permit-insecure-cmr") and self.remote_units():
return model.BlockedStatus("Unsupported CMR relation")
return model.ActiveStatus()
def check_bootstrap_source(self) -> model.StatusBase:
if not self.config("no-bootstrap") and is_relation_made(
"bootstrap-source"
):
return model.BlockedStatus(
"Cannot join the bootstrap-source relation when "
"no-bootstrap is False",
)
return model.ActiveStatus()
def check_moncount(self) -> model.StatusBase:
moncount = self.config("monitor-count")
if (
len(self.get_peer_mons()) + 1 < moncount
): # we're including ourselves
return model.BlockedStatus(
"Insufficient peer units to bootstrap"
" cluster (require {})".format(moncount)
)
return model.ActiveStatus()
def check_ready_mons(self) -> model.StatusBase:
moncount = self.config("monitor-count")
mons = self.get_peer_mons()
ready = sum(
1 for mon in mons.values() if mon.get("ceph-public-address")
)
if ready + 1 < moncount: # "this" mon is ready presumably
return model.WaitingStatus(
"Peer units detected, waiting for addresses"
)
return model.ActiveStatus()
def check_rbd_features(self) -> model.StatusBase:
configured_rbd_features = self.config("default-rbd-features")
if utils.has_rbd_mirrors() and configured_rbd_features:
if (
utils.add_rbd_mirror_features(configured_rbd_features)
!= configured_rbd_features
):
# The configured RBD features bitmap does not contain the
# features required for RBD Mirroring
return model.BlockedStatus(
"Configuration mismatch: RBD Mirroring "
"enabled but incorrect value set for "
"``default-rbd-features``",
)
return model.ActiveStatus()
def check_get_osd_settings(self):
try:
ch_ceph.get_osd_settings("client")
except ch_ceph.OSD_SETTING_EXCEPTIONS as e:
return model.BlockedStatus(str(e))
return model.ActiveStatus()
def check_alert_rule_errors(self):
if self.charm.metrics_endpoint.have_alert_rule_errors():
return model.BlockedStatus("invalid alert rules, check unit logs")
return model.ActiveStatus()
def check_expected_osd_count(self):
if ceph_utils.is_bootstrapped() and ceph_utils.is_quorum():
expected_osd_count = self.config("expected-osd-count") or 3
if self.sufficient_osds(expected_osd_count):
return model.ActiveStatus("Unit is ready and clustered")
elif not self.have_osd_relation():
return model.BlockedStatus("Missing relation: OSD")
else:
return model.WaitingStatus(
"Monitor bootstrapped but waiting for number of"
" OSDs to reach expected-osd-count ({})".format(
expected_osd_count
)
)
else:
return model.BlockedStatus("Unit not clustered (no quorum)")
def register_checks(self):
checkers = [
self.check_insecure_cmr,
self.check_bootstrap_source,
self.check_moncount,
self.check_ready_mons,
self.check_rbd_features,
self.check_alert_rule_errors,
self.check_expected_osd_count,
]
for check in checkers:
self.charm.register_status_check(check)
def assess_status(self, _event):
logger.debug("Running assess_status() for %s", self.charm)
application_version_set(get_upstream_version(VERSION_PACKAGE))
self.charm.update_status()

View File

@ -3,6 +3,7 @@ import logging
from ops.main import main
import ceph_status
import charms.operator_libs_linux.v0.apt as apt
import charms.operator_libs_linux.v1.systemd as systemd
@ -40,77 +41,62 @@ class CephMonCharm(ops_openstack.core.OSBaseCharm):
systemd.service_pause('ceph-create-keys')
except systemd.SystemdError:
pass
hooks.assess_status(self)
def on_config(self, event):
hooks.config_changed()
hooks.assess_status(self)
def on_pre_series_upgrade(self, event):
hooks.pre_series_upgrade()
hooks.assess_status(self)
def on_upgrade(self, event):
self.metrics_endpoint.update_alert_rules()
hooks.upgrade_charm()
hooks.assess_status(self)
def on_post_series_upgrade(self, event):
hooks.post_series_upgrade()
hooks.assess_status(self)
# Relations.
def on_mon_relation_joined(self, event):
hooks.mon_relation_joined()
hooks.assess_status(self)
def on_bootstrap_source_relation_changed(self, event):
hooks.bootstrap_source_relation_changed()
hooks.assess_status(self)
def on_prometheus_relation_joined_or_changed(self, event):
hooks.prometheus_relation()
hooks.assess_status(self)
def on_prometheus_relation_departed(self, event):
hooks.prometheus_left()
hooks.assess_status(self)
def on_mon_relation(self, event):
hooks.mon_relation()
hooks.assess_status(self)
def on_osd_relation(self, event):
hooks.osd_relation()
hooks.assess_status(self)
def on_dashboard_relation_joined(self, event):
hooks.dashboard_relation()
hooks.assess_status(self)
def on_radosgw_relation(self, event):
hooks.radosgw_relation()
hooks.assess_status(self)
def on_rbd_mirror_relation(self, event):
hooks.rbd_mirror_relation()
hooks.assess_status(self)
def on_mds_relation(self, event):
hooks.mds_relation_joined()
hooks.assess_status(self)
def on_admin_relation(self, event):
hooks.admin_relation_joined()
hooks.assess_status(self)
def on_client_relation(self, event):
hooks.client_relation()
hooks.assess_status(self)
def on_nrpe_relation(self, event):
hooks.update_nrpe_config()
hooks.assess_status(self)
def on_commit(self, _event):
self.ceph_status.assess_status()
# Actions.
@ -141,12 +127,15 @@ class CephMonCharm(ops_openstack.core.OSBaseCharm):
self._stored.is_started = True
if self.is_blocked_insecure_cmr():
logging.error("Not running hook, CMR detected and not supported")
logging.error(
"Not running hook, CMR detected and not supported")
return
fw = self.framework
self.metrics_endpoint = ceph_metrics.CephMetricsEndpointProvider(self)
self.ceph_status = ceph_status.StatusAssessor(self)
self._observe_action(self.on.change_osd_weight_action,
ops_actions.change_osd_weight.change_osd_weight)
self._observe_action(self.on.copy_pool_action,

View File

@ -122,7 +122,7 @@ class TestCephMetrics(unittest.TestCase):
self.harness.add_resource("alert-rules", "not-a-rule")
self.harness.charm.metrics_endpoint.update_alert_rules()
self.assertTrue(
self.harness.charm.metrics_endpoint.assess_alert_rule_errors()
self.harness.charm.metrics_endpoint.have_alert_rule_errors()
)
@patch("ceph_metrics.ceph_utils.is_bootstrapped", return_value=True)

View File

@ -0,0 +1,55 @@
#!/usr/bin/env python3
# Copyright 2022 Canonical Ltd.
# See LICENSE file for licensing details.
from unittest.mock import patch
import unittest
from ops.testing import Harness
import ceph_shared
import charm
@patch("charm.hooks")
class TestCephShared(unittest.TestCase):
def setUp(self):
super().setUp()
self.harness = Harness(charm.CephMonCharm)
self.addCleanup(self.harness.cleanup)
def test_init(self, _hooks):
self.harness.begin()
ceph_info = ceph_shared.CephMonInfo(self.harness.charm)
self.assertTrue(ceph_info.relations)
def test_get_peer_mons(self, _hooks):
self.harness.begin()
self.harness.set_leader(True)
ceph_info = ceph_shared.CephMonInfo(self.harness.charm)
self.harness.add_relation_unit(
self.harness.add_relation("mon", "ceph-mon"), "ceph-mon/0"
)
peer_mons = ceph_info.get_peer_mons()
self.assertEqual(len(peer_mons), 1)
peer = list(peer_mons.keys())[0]
self.assertEqual(peer.name, "ceph-mon/0")
def test_not_sufficient_osds(self, _hooks):
self.harness.begin()
ceph_info = ceph_shared.CephMonInfo(self.harness.charm)
rel_id = self.harness.add_relation("osd", "ceph-osd")
self.harness.add_relation_unit(rel_id, "ceph-osd/0")
have_enough = ceph_info.sufficient_osds(minimum_osds=77)
self.assertFalse(have_enough)
def test_sufficient_osds(self, _hooks):
self.harness.begin()
ceph_info = ceph_shared.CephMonInfo(self.harness.charm)
rel_id = self.harness.add_relation("osd", "ceph-osd")
self.harness.add_relation_unit(rel_id, "ceph-osd/0")
self.harness.update_relation_data(
rel_id, "ceph-osd/0", {"bootstrapped-osds": "77"}
)
have_enough = ceph_info.sufficient_osds(minimum_osds=77)
self.assertTrue(have_enough)

View File

@ -0,0 +1,120 @@
#!/usr/bin/env python3
# Copyright 2022 Canonical Ltd.
# See LICENSE file for licensing details.
from unittest.mock import patch
import unittest
from ops import model
from ops.testing import Harness
import ceph_status
import charm
from charmhelpers.contrib.storage.linux import ceph as ch_ceph
@patch("charm.hooks")
class TestCephStatus(unittest.TestCase):
def setUp(self):
super().setUp()
self.harness = Harness(charm.CephMonCharm)
self.addCleanup(self.harness.cleanup)
def test_init(self, _hooks):
self.harness.begin()
status = ceph_status.StatusAssessor(self.harness.charm)
self.assertTrue(status.charm.custom_status_checks)
def test_check_insecure_cmr(self, _hooks):
self.harness.begin()
status = ceph_status.StatusAssessor(self.harness.charm)
result = status.check_insecure_cmr()
self.assertIsInstance(result, model.ActiveStatus)
self.harness.add_relation_unit(
self.harness.add_relation("client", "remote"), "remote-foo/0"
)
result = status.check_insecure_cmr()
self.assertIsInstance(result, model.BlockedStatus)
def test_check_moncount(self, _hooks):
self.harness.begin()
status = ceph_status.StatusAssessor(self.harness.charm)
result = status.check_moncount()
self.assertIsInstance(result, model.BlockedStatus)
rel_id = self.harness.add_relation("mon", "ceph-mon")
for n in (0, 1, 2):
self.harness.add_relation_unit(rel_id, "ceph-mon/{}".format(n))
result = status.check_moncount()
self.assertIsInstance(result, model.ActiveStatus)
def test_check_ready_mons(self, _hooks):
self.harness.begin()
status = ceph_status.StatusAssessor(self.harness.charm)
result = status.check_ready_mons()
self.assertIsInstance(result, model.WaitingStatus)
rel_id = self.harness.add_relation("mon", "ceph-mon")
for n in (0, 1, 2):
self.harness.add_relation_unit(rel_id, "ceph-mon/{}".format(n))
self.harness.update_relation_data(
rel_id, "ceph-mon/{}".format(n), {"ceph-public-address": "foo"}
)
result = status.check_ready_mons()
self.assertIsInstance(result, model.ActiveStatus)
@patch("ceph_status.ch_ceph.get_osd_settings")
def test_check_get_osd_settings(self, get_osd_settings, _hooks):
self.harness.begin()
status = ceph_status.StatusAssessor(self.harness.charm)
result = status.check_get_osd_settings()
self.assertIsInstance(result, model.ActiveStatus)
get_osd_settings.side_effect = ch_ceph.OSDSettingConflict(
"testexception"
)
result = status.check_get_osd_settings()
self.assertIsInstance(result, model.BlockedStatus)
def test_check_alert_rule_errors(self, _hooks):
self.harness.begin()
status = ceph_status.StatusAssessor(self.harness.charm)
with patch.object(
self.harness.charm,
"metrics_endpoint",
create=True,
) as metrics_endpoint:
metrics_endpoint.have_alert_rule_errors.return_value = True
result = status.check_alert_rule_errors()
self.assertIsInstance(result, model.BlockedStatus)
metrics_endpoint.have_alert_rule_errors.return_value = False
result = status.check_alert_rule_errors()
self.assertIsInstance(result, model.ActiveStatus)
@patch("ceph_status.ceph_utils")
def test_check_expected_osd_count(self, ceph_utils, _hooks):
self.harness.begin()
status = ceph_status.StatusAssessor(self.harness.charm)
# not bootstrapped
ceph_utils.is_bootstrapped.return_value = False
ceph_utils.is_quorum.return_value = False
result = status.check_expected_osd_count()
self.assertIsInstance(result, model.BlockedStatus)
self.assertEqual(result.message, "Unit not clustered (no quorum)")
# bootstrapped, no osd rel
ceph_utils.is_bootstrapped.return_value = True
ceph_utils.is_quorum.return_value = True
result = status.check_expected_osd_count()
self.assertIsInstance(result, model.BlockedStatus)
self.assertEqual(result.message, "Missing relation: OSD")
# bootstrapped, enough osds
rel_id = self.harness.add_relation("osd", "ceph-osd")
for n in (0, 1, 2):
self.harness.add_relation_unit(rel_id, "ceph-osd/{}".format(n))
self.harness.update_relation_data(
rel_id, "ceph-osd/{}".format(n), {"bootstrapped-osds": "1"}
)
result = status.check_expected_osd_count()
self.assertIsInstance(result, model.ActiveStatus)

62
unit_tests/test_charm.py Normal file
View File

@ -0,0 +1,62 @@
#!/usr/bin/env python3
# Copyright 2022 Canonical Ltd.
# See LICENSE file for licensing details.
from unittest.mock import patch
import unittest
from ops.testing import Harness
import ceph_metrics # noqa: avoid circ. import
import charm
class TestCephCharm(unittest.TestCase):
def setUp(self):
super().setUp()
self.harness = Harness(charm.CephMonCharm)
self.harness.begin()
self.addCleanup(self.harness.cleanup)
def test_init(self):
self.assertTrue(self.harness.charm.framework)
self.assertTrue(self.harness.charm.metrics_endpoint)
self.assertTrue(self.harness.charm.ceph_status)
@patch("charm.hooks")
def test_on_config_changed(self, hooks):
self.harness.update_config({"permit-insecure-cmr": None})
hooks.config_changed.assert_called()
@patch("charm.ops_openstack.core.apt_install")
@patch("charm.ops_openstack.core.apt_update")
@patch("charm.ops_openstack.core.add_source")
@patch("charm.ops_openstack.core.OSBaseCharm.update_status")
@patch("charm.hooks")
@patch("charm.systemd")
@patch("charm.apt")
def test_on_install(
self,
_apt,
_systemd,
_hooks,
_update_status,
_add_source,
apt_update,
apt_install,
):
self.harness.update_config({"permit-insecure-cmr": None})
self.harness.charm.on.install.emit()
apt_install.assert_called_with(
[
"ceph",
"gdisk",
"radosgw",
"lvm2",
"parted",
"smartmontools",
],
fatal=True,
)
apt_update.assert_called()

View File

@ -1,251 +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.mock as mock
import sys
import test_utils
import charmhelpers.contrib.storage.linux.ceph as ch_ceph
# python-apt is not installed as part of test-requirements but is imported by
# some charmhelpers modules so create a fake import.
mock_apt = mock.MagicMock()
sys.modules['apt'] = mock_apt
mock_apt.apt_pkg = mock.MagicMock()
with mock.patch('charmhelpers.contrib.hardening.harden.harden') as mock_dec:
mock_dec.side_effect = (lambda *dargs, **dkwargs: lambda f:
lambda *args, **kwargs: f(*args, **kwargs))
import ceph_hooks as hooks
TO_PATCH = [
'status_set',
'config',
'ceph',
'is_relation_made',
'relations',
'relation_ids',
'relation_get',
'related_units',
'local_unit',
'application_version_set',
'get_upstream_version',
]
NO_PEERS = {
'ceph-mon1': True
}
ENOUGH_PEERS_INCOMPLETE = {
'ceph-mon1': True,
'ceph-mon2': False,
'ceph-mon3': False,
}
ENOUGH_PEERS_COMPLETE = {
'ceph-mon1': True,
'ceph-mon2': True,
'ceph-mon3': True,
}
class ServiceStatusTestCase(test_utils.CharmTestCase):
def setUp(self):
super(ServiceStatusTestCase, self).setUp(hooks, TO_PATCH)
self.config.side_effect = self.test_config.get
self.test_config.set('monitor-count', 3)
self.local_unit.return_value = 'ceph-mon1'
self.get_upstream_version.return_value = '10.2.2'
self.is_relation_made.return_value = False
@mock.patch.object(hooks, 'get_peer_units')
def test_assess_status_no_peers(self, _peer_units):
_peer_units.return_value = NO_PEERS
hooks.assess_status()
self.status_set.assert_called_with('blocked', mock.ANY)
self.application_version_set.assert_called_with('10.2.2')
@mock.patch.object(hooks, 'get_peer_units')
def test_assess_status_peers_incomplete(self, _peer_units):
_peer_units.return_value = ENOUGH_PEERS_INCOMPLETE
hooks.assess_status()
self.status_set.assert_called_with('waiting', mock.ANY)
self.application_version_set.assert_called_with('10.2.2')
@mock.patch.object(hooks, 'get_osd_settings')
@mock.patch.object(hooks, 'has_rbd_mirrors')
@mock.patch.object(hooks, 'sufficient_osds')
@mock.patch.object(hooks, 'get_peer_units')
def test_assess_status_peers_complete_active(self, _peer_units,
_sufficient_osds,
_has_rbd_mirrors,
_get_osd_settings):
_peer_units.return_value = ENOUGH_PEERS_COMPLETE
_sufficient_osds.return_value = True
self.ceph.is_bootstrapped.return_value = True
self.ceph.is_quorum.return_value = True
_has_rbd_mirrors.return_value = False
_get_osd_settings.return_value = {}
hooks.assess_status()
self.status_set.assert_called_with('active', mock.ANY)
self.application_version_set.assert_called_with('10.2.2')
@mock.patch.object(hooks, 'relation_ids')
@mock.patch.object(hooks, 'get_osd_settings')
@mock.patch.object(hooks, 'has_rbd_mirrors')
@mock.patch.object(hooks, 'sufficient_osds')
@mock.patch.object(hooks, 'get_peer_units')
def test_assess_status_no_osd_relation(
self,
_peer_units,
_sufficient_osds,
_has_rbd_mirrors,
_get_osd_settings,
_relation_ids
):
_peer_units.return_value = ENOUGH_PEERS_COMPLETE
_sufficient_osds.return_value = False
_relation_ids.return_value = []
self.ceph.is_bootstrapped.return_value = True
self.ceph.is_quorum.return_value = True
_has_rbd_mirrors.return_value = False
_get_osd_settings.return_value = {}
hooks.assess_status()
self.status_set.assert_called_with('blocked', 'Missing relation: OSD')
self.application_version_set.assert_called_with('10.2.2')
@mock.patch.object(hooks, 'relation_ids')
@mock.patch.object(hooks, 'get_osd_settings')
@mock.patch.object(hooks, 'has_rbd_mirrors')
@mock.patch.object(hooks, 'sufficient_osds')
@mock.patch.object(hooks, 'get_peer_units')
def test_assess_status_osd_relation_but_insufficient_osds(
self,
_peer_units,
_sufficient_osds,
_has_rbd_mirrors,
_get_osd_settings,
_relation_ids
):
_peer_units.return_value = ENOUGH_PEERS_COMPLETE
_sufficient_osds.return_value = False
_relation_ids.return_value = ['osd:1']
self.ceph.is_bootstrapped.return_value = True
self.ceph.is_quorum.return_value = True
_has_rbd_mirrors.return_value = False
_get_osd_settings.return_value = {}
hooks.assess_status()
self.status_set.assert_called_with('waiting', mock.ANY)
self.application_version_set.assert_called_with('10.2.2')
@mock.patch.object(hooks, 'get_osd_settings')
@mock.patch.object(hooks, 'has_rbd_mirrors')
@mock.patch.object(hooks, 'sufficient_osds')
@mock.patch.object(hooks, 'get_peer_units')
def test_assess_status_invalid_osd_settings(self, _peer_units,
_sufficient_osds,
_has_rbd_mirrors,
_get_osd_settings):
_peer_units.return_value = ENOUGH_PEERS_COMPLETE
_sufficient_osds.return_value = True
self.ceph.is_bootstrapped.return_value = True
self.ceph.is_quorum.return_value = True
_has_rbd_mirrors.return_value = False
_get_osd_settings.side_effect = ch_ceph.OSDSettingConflict(
'conflict in setting foo')
hooks.assess_status()
self.status_set.assert_called_with('blocked', mock.ANY)
@mock.patch.object(hooks, 'get_osd_settings')
@mock.patch.object(hooks, 'has_rbd_mirrors')
@mock.patch.object(hooks, 'sufficient_osds')
@mock.patch.object(hooks, 'get_peer_units')
def test_assess_status_peers_complete_down(self, _peer_units,
_sufficient_osds,
_has_rbd_mirrors,
_get_osd_settings):
_peer_units.return_value = ENOUGH_PEERS_COMPLETE
_sufficient_osds.return_value = True
self.ceph.is_bootstrapped.return_value = False
self.ceph.is_quorum.return_value = False
_has_rbd_mirrors.return_value = False
_get_osd_settings.return_value = {}
hooks.assess_status()
self.status_set.assert_called_with('blocked', mock.ANY)
self.application_version_set.assert_called_with('10.2.2')
@mock.patch.object(hooks, 'has_rbd_mirrors')
@mock.patch.object(hooks, 'sufficient_osds')
@mock.patch.object(hooks, 'get_peer_units')
def test_assess_status_rbd_feature_mismatch(self, _peer_units,
_sufficient_osds,
_has_rbd_mirrors):
_peer_units.return_value = ENOUGH_PEERS_COMPLETE
_sufficient_osds.return_value = True
self.ceph.is_bootstrapped.return_value = True
self.ceph.is_quorum.return_value = True
_has_rbd_mirrors.return_value = True
self.test_config.set('default-rbd-features', 61)
hooks.assess_status()
self.status_set.assert_called_once_with('blocked', mock.ANY)
def test_get_peer_units_no_peers(self):
self.relation_ids.return_value = ['mon:1']
self.related_units.return_value = []
self.assertEqual({'ceph-mon1': True},
hooks.get_peer_units())
def test_get_peer_units_peers_incomplete(self):
self.relation_ids.return_value = ['mon:1']
self.related_units.return_value = ['ceph-mon2',
'ceph-mon3']
self.relation_get.return_value = None
self.assertEqual({'ceph-mon1': True,
'ceph-mon2': False,
'ceph-mon3': False},
hooks.get_peer_units())
def test_get_peer_units_peers_complete(self):
self.relation_ids.return_value = ['mon:1']
self.related_units.return_value = ['ceph-mon2',
'ceph-mon3']
self.relation_get.side_effect = ['ceph-mon2',
'ceph-mon3']
self.assertEqual({'ceph-mon1': True,
'ceph-mon2': True,
'ceph-mon3': True},
hooks.get_peer_units())
def test_no_bootstrap_not_set(self):
self.is_relation_made.return_value = True
hooks.assess_status()
self.status_set.assert_called_with('blocked', mock.ANY)
self.application_version_set.assert_called_with('10.2.2')
def test_cmr_remote_unit(self):
self.test_config.set('permit-insecure-cmr', False)
self.relations.return_value = ['client']
self.relation_ids.return_value = ['client:1']
self.related_units.return_value = ['remote-1']
hooks.assess_status()
self.status_set.assert_called_with(
'blocked',
'Unsupported CMR relation')
self.status_set.reset_mock()
self.test_config.set('permit-insecure-cmr', True)
hooks.assess_status()
self.assertFalse(
mock.call('blocked', 'Unsupported CMR relation') in
self.status_set.call_args_list)