You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
342 lines
12 KiB
342 lines
12 KiB
# Copyright 2020 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. |
|
|
|
from ops.charm import CharmBase |
|
from ops.framework import ( |
|
StoredState, |
|
) |
|
|
|
from charmhelpers.fetch import ( |
|
apt_install, |
|
apt_update, |
|
add_source, |
|
) |
|
from ops.model import ( |
|
ActiveStatus, |
|
BlockedStatus, |
|
MaintenanceStatus, |
|
WaitingStatus, |
|
) |
|
import charmhelpers.core.hookenv as hookenv |
|
import charmhelpers.contrib.openstack.utils as os_utils |
|
import logging |
|
|
|
# Stolen from charms.ceph |
|
UCA_CODENAME_MAP = { |
|
'icehouse': 'firefly', |
|
'juno': 'firefly', |
|
'kilo': 'hammer', |
|
'liberty': 'hammer', |
|
'mitaka': 'jewel', |
|
'newton': 'jewel', |
|
'ocata': 'jewel', |
|
'pike': 'luminous', |
|
'queens': 'luminous', |
|
'rocky': 'mimic', |
|
'stein': 'mimic', |
|
'train': 'nautilus', |
|
'ussuri': 'octopus', |
|
'victoria': 'octopus', |
|
'wallaby': 'pacific', |
|
'xena': 'pacific', |
|
'yoga': 'quincy', |
|
} |
|
|
|
|
|
_releases = {} |
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
class OSBaseCharm(CharmBase): |
|
_stored = StoredState() |
|
|
|
PACKAGES = [] |
|
|
|
RESTART_MAP = {} |
|
|
|
REQUIRED_RELATIONS = [] |
|
|
|
MANDATORY_CONFIG = [] |
|
|
|
def __init__(self, framework): |
|
super().__init__(framework) |
|
self.custom_status_checks = [] |
|
self._stored.set_default(is_started=False) |
|
self._stored.set_default(is_paused=False) |
|
self._stored.set_default(series_upgrade=False) |
|
self.framework.observe(self.on.install, self.on_install) |
|
self.framework.observe(self.on.update_status, self.on_update_status) |
|
self.framework.observe(self.on.config_changed, self._on_config) |
|
# A charm may not have pause/resume actions if it does not manage a |
|
# daemon. |
|
try: |
|
self.framework.observe( |
|
self.on.pause_action, |
|
self.on_pause_action) |
|
except AttributeError: |
|
pass |
|
try: |
|
self.framework.observe( |
|
self.on.resume_action, |
|
self.on_resume_action) |
|
except AttributeError: |
|
pass |
|
self.framework.observe(self.on.pre_series_upgrade, |
|
self.on_pre_series_upgrade) |
|
self.framework.observe(self.on.post_series_upgrade, |
|
self.on_post_series_upgrade) |
|
|
|
def install_pkgs(self): |
|
logging.info("Installing packages") |
|
if self.model.config.get('source'): |
|
add_source( |
|
self.model.config['source'], |
|
self.model.config.get('key')) |
|
apt_update(fatal=True) |
|
apt_install(self.PACKAGES, fatal=True) |
|
self.update_status() |
|
|
|
def on_install(self, event): |
|
self.install_pkgs() |
|
|
|
def custom_status_check(self): |
|
raise NotImplementedError |
|
|
|
def register_status_check(self, custom_check): |
|
self.custom_status_checks.append(custom_check) |
|
|
|
def update_status(self): |
|
"""Update the charms status |
|
|
|
A charm, or plugin, can register checks to be run when calculating the |
|
charms status. Each status method should have a unique name. The custom |
|
check should return a StatusBase object. If the check returns an |
|
ActiveStatus object then subsequent checks are run, if it returns |
|
anything else then the charms status is set to the object the check |
|
returned and no subsequent checks are run. If the check returns an |
|
ActiveStatus with a specific message then this message will be |
|
concatenated with the other active status messages. |
|
|
|
Example:: |
|
|
|
class MyCharm(OSBaseCharm): |
|
|
|
def __init__(self, framework): |
|
super().__init__(framework) |
|
super().register_status_check(self.mycharm_check) |
|
|
|
def mycharm_check(self): |
|
if self.model.config['plugin-check-fail'] == 'True': |
|
return BlockedStatus( |
|
'Plugin Custom check failed') |
|
else: |
|
return ActiveStatus() |
|
|
|
""" |
|
logging.info("Updating status") |
|
active_messages = ['Unit is ready'] |
|
for check in self.custom_status_checks: |
|
_result = check() |
|
if isinstance(_result, ActiveStatus): |
|
if _result.message: |
|
active_messages.append(_result.message) |
|
else: |
|
self.unit.status = _result |
|
return |
|
|
|
if self._stored.series_upgrade: |
|
self.unit.status = BlockedStatus( |
|
'Ready for do-release-upgrade and reboot. ' |
|
'Set complete when finished.') |
|
return |
|
|
|
if self._stored.is_paused: |
|
self.unit.status = MaintenanceStatus( |
|
"Paused. Use 'resume' action to resume normal service.") |
|
return |
|
|
|
missing_relations = [] |
|
for relation in self.REQUIRED_RELATIONS: |
|
if not self.model.get_relation(relation): |
|
missing_relations.append(relation) |
|
if missing_relations: |
|
self.unit.status = BlockedStatus( |
|
'Missing relations: {}'.format(', '.join(missing_relations))) |
|
return |
|
|
|
_, services_not_running_msg = os_utils.ows_check_services_running( |
|
self.services(), ports=[]) |
|
if services_not_running_msg is not None: |
|
self.unit.status = BlockedStatus(services_not_running_msg) |
|
return |
|
|
|
if self._stored.is_started: |
|
_unique = [] |
|
# Reverse sort the list so that a shorter message that has the same |
|
# start as a longer message comes first and can then be omitted. |
|
# eg 'Unit is ready' comes after 'Unit is ready and clustered' |
|
# and 'Unit is ready' is dropped. |
|
for msg in sorted(list(set(active_messages)), reverse=True): |
|
dupes = [m for m in _unique if m.startswith(msg)] |
|
if not dupes: |
|
_unique.append(msg) |
|
self.unit.status = ActiveStatus(', '.join(_unique)) |
|
else: |
|
self.unit.status = WaitingStatus('Charm configuration in progress') |
|
|
|
logging.info("Status updated") |
|
|
|
def on_update_status(self, event): |
|
self.update_status() |
|
|
|
def services(self): |
|
_svcs = [] |
|
for svc in self.RESTART_MAP.values(): |
|
_svcs.extend(svc) |
|
return list(set(_svcs)) |
|
|
|
def on_pre_series_upgrade(self, event): |
|
_, messages = os_utils.manage_payload_services( |
|
'pause', |
|
services=self.services(), |
|
charm_func=None) |
|
self._stored.is_paused = True |
|
self._stored.series_upgrade = True |
|
self.update_status() |
|
|
|
def on_post_series_upgrade(self, event): |
|
_, messages = os_utils.manage_payload_services( |
|
'resume', |
|
services=self.services(), |
|
charm_func=None) |
|
self._stored.is_paused = False |
|
self._stored.series_upgrade = False |
|
self.update_status() |
|
|
|
def on_pause_action(self, event): |
|
_, messages = os_utils.manage_payload_services( |
|
'pause', |
|
services=self.services(), |
|
charm_func=None) |
|
self._stored.is_paused = True |
|
self.update_status() |
|
|
|
def on_resume_action(self, event): |
|
_, messages = os_utils.manage_payload_services( |
|
'resume', |
|
services=self.services(), |
|
charm_func=None) |
|
self._stored.is_paused = False |
|
self.update_status() |
|
|
|
def on_config(self, event): |
|
"""Main entry point for configuration changes.""" |
|
pass |
|
|
|
def _on_config(self, event): |
|
missing = [] |
|
config = self.framework.model.config |
|
for param in self.MANDATORY_CONFIG: |
|
if param not in config: |
|
missing.append(param) |
|
if missing: |
|
self.unit.status = BlockedStatus( |
|
'Missing option(s): ' + ','.join(missing)) |
|
return |
|
self.on_config(event) |
|
|
|
|
|
def charm_class(cls): |
|
_releases[cls.release] = {'deb': cls} |
|
|
|
|
|
# Adapted from charms_openstack.charm.core |
|
def get_charm_class(release=None, package_type='deb', all_releases=None, |
|
*args, **kwargs): |
|
"""Get an instance of the charm based on the release (or use the |
|
default if release is None). |
|
|
|
OS releases are in alphabetical order, so it looks for the first release |
|
that is provided if release is None, otherwise it finds the release that is |
|
before or equal to the release passed. |
|
|
|
Note that it passes args and kwargs to the class __init__() method. |
|
|
|
:param release: lc string representing release wanted. |
|
:param package_type: string representing the package type required |
|
:returns: BaseOpenStackCharm() derived class according to cls.releases |
|
""" |
|
if not all_releases: |
|
all_releases = os_utils.OPENSTACK_RELEASES |
|
if len(_releases.keys()) == 0: |
|
raise RuntimeError( |
|
"No derived BaseOpenStackCharm() classes registered") |
|
# Note that this relies on OS releases being in alphabetical order |
|
known_releases = sorted(_releases.keys()) |
|
cls = None |
|
if release is None: |
|
# take the latest version of the charm if no release is passed. |
|
cls = _releases[known_releases[-1]][package_type] |
|
else: |
|
# check that the release is a valid release |
|
if release not in all_releases: |
|
raise RuntimeError( |
|
"Release {} is not a known OpenStack release?".format(release)) |
|
release_index = all_releases.index(release) |
|
if (release_index < |
|
all_releases.index(known_releases[0])): |
|
raise RuntimeError( |
|
"Release {} is not supported by this charm. Earliest support " |
|
"is {} release".format(release, known_releases[0])) |
|
else: |
|
# try to find the release that is supported. |
|
for known_release in reversed(known_releases): |
|
if (release_index >= |
|
all_releases.index(known_release) and |
|
package_type in _releases[known_release]): |
|
cls = _releases[known_release][package_type] |
|
break |
|
if cls is None: |
|
raise RuntimeError("Release {} is not supported".format(release)) |
|
return cls |
|
|
|
|
|
# Adapted from charms_openstack.charm.core |
|
def get_charm_instance(release=None, package_type='deb', all_releases=None, |
|
*args, **kwargs): |
|
return get_charm_class( |
|
release=release, |
|
package_type=package_type, |
|
all_releases=all_releases, |
|
*args, **kwargs)(release=release, *args, **kwargs) |
|
|
|
|
|
def get_charm_class_for_release(): |
|
_origin = None |
|
# There is no charm class to interact with the ops framework yet |
|
# and it is now forbidden to access ops.model._Model so fallback |
|
# to charmhelpers.core.hookenv |
|
config = hookenv.config() |
|
if 'source' in config: |
|
_origin = config['source'] |
|
elif 'openstack-origin' in config: |
|
_origin = config['openstack-origin'] |
|
if not _origin: |
|
_origin = 'distro' |
|
# XXX Make this support openstack and ceph |
|
target_release = os_utils.get_os_codename_install_source(_origin) |
|
# Check for a cepch charm match first: |
|
ceph_release = UCA_CODENAME_MAP[target_release] |
|
releases = sorted(list(set(UCA_CODENAME_MAP.values()))) |
|
return get_charm_class(release=ceph_release, all_releases=releases)
|
|
|