charm-ovn-central/src/lib/charm/openstack/ovn_central.py

1024 lines
41 KiB
Python

# Copyright 2019 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 collections
import operator
import os
import subprocess
import time
import charmhelpers.core as ch_core
import charmhelpers.contrib.charmsupport.nrpe as nrpe
import charmhelpers.contrib.network.ovs.ovn as ch_ovn
import charmhelpers.contrib.network.ovs.ovsdb as ch_ovsdb
from charmhelpers.contrib.network import ufw as ch_ufw
import charmhelpers.contrib.openstack.deferred_events as deferred_events
import charmhelpers.fetch as ch_fetch
import charms.reactive as reactive
import charms_openstack.adapters
import charms_openstack.charm
from charms.layer import snap
# Release selection need to happen here for correct determination during
# bus discovery and action exection
charms_openstack.charm.use_defaults('charm.default-select-release')
PEER_RELATION = 'ovsdb-peer'
CERT_RELATION = 'certificates'
# NOTE(fnordahl): We should split the ``OVNConfigurationAdapter`` in
# ``layer-ovn`` into common and chassis specific parts so we can re-use the
# common parts here.
class OVNCentralConfigurationAdapter(
charms_openstack.adapters.ConfigurationAdapter):
"""Provide a configuration adapter for OVN Central."""
@property
def ovn_key(self):
return os.path.join(self.charm_instance.ovn_sysconfdir(), 'key_host')
@property
def ovn_cert(self):
return os.path.join(self.charm_instance.ovn_sysconfdir(), 'cert_host')
@property
def ovn_ca_cert(self):
return os.path.join(self.charm_instance.ovn_sysconfdir(),
'{}.crt'.format(self.charm_instance.name))
@property
def is_charm_leader(self):
return reactive.is_flag_set('leadership.is_leader')
@property
def _ovn_source(self):
if (not self.ovn_source
and reactive.is_flag_set('leadership.set.install_stamp')
and ch_core.host.lsb_release()['DISTRIB_CODENAME'] == 'focal'):
return 'cloud:focal-ovn-22.03'
return self.ovn_source
@property
def ovn_exporter_snap_channel(self):
"""Validate a provided snap channel and return it
Any prefix is ignored ('0.10' in '0.10/stable' for example). If
a config value is empty it means that the snap does not need to
be installed.
"""
channel = self.ovn_exporter_channel
if not channel:
return None
channel_suffix = channel.split('/')[-1]
if channel_suffix not in ('stable', 'candidate', 'beta', 'edge'):
return 'stable'
return channel_suffix
class BaseOVNCentralCharm(charms_openstack.charm.OpenStackCharm):
abstract_class = True
# Note that we currently do not support pivoting between release specific
# charm classes in the OVN charms. We still need this set to ensure the
# default methods are happy.
#
# Also see docstring in the `upgrade_if_available` method.
package_codenames = {
'ovn-central': collections.OrderedDict([
('2', 'train'),
('20', 'ussuri'),
]),
}
name = 'ovn-central'
packages = ['ovn-central']
services = ['ovn-central']
nrpe_check_services = []
release_pkg = 'ovn-central'
configuration_class = OVNCentralConfigurationAdapter
required_relations = [PEER_RELATION, CERT_RELATION]
python_version = 3
source_config_key = 'source'
min_election_timer = 1
max_election_timer = 60
def __init__(self, **kwargs):
"""Override class init to populate restart map with instance method."""
self.restart_map = {
'/etc/default/ovn-central': self.services,
os.path.join(self.ovn_sysconfdir(),
'ovn-northd-db-params.conf'): ['ovn-northd'],
}
super().__init__(**kwargs)
def restart_on_change(self):
"""Restart the services in the self.restart_map{} attribute if any of
the files identified by the keys changes for the wrapped call.
Usage:
with restart_on_change(restart_map, ...):
do_stuff_that_might_trigger_a_restart()
...
"""
return ch_core.host.restart_on_change(
self.full_restart_map,
stopstart=True,
restart_functions=getattr(self, 'restart_functions', None),
can_restart_now_f=deferred_events.check_and_record_restart_request,
post_svc_restart_f=deferred_events.process_svc_restart)
@property
def deferable_services(self):
"""Services which should be stopped from restarting.
All services from self.services are deferable. But the charm may
install a package which install a service that the charm does not add
to its restart_map. In that case it will be missing from
self.services. However one of the jobs of deferred events is to ensure
that packages updates outside of charms also do not restart services.
To ensure there is a complete list take the services from self.services
and also add in a known list of networking services.
NOTE: It does not matter if one of the services in the list is not
installed on the system.
"""
svcs = self.services[:]
svcs.extend(['ovn-ovsdb-server-nb', 'ovn-ovsdb-server-nb',
'ovn-northd', 'ovn-central'])
return list(set(svcs))
def configure_ovn_source(self):
"""Configure the OVN overlay archive."""
if self.options.ovn_source:
# The end user has added configuration which may require full
# processing including key extraction.
self.configure_source(config_key='ovn-source')
elif self.options._ovn_source:
# The end user has not added configuration and we want to use the
# runtime determined default value.
#
# We cannot use the default `configure_source` method here as it
# attempts to access charm config directly.
ch_fetch.add_source(self.options._ovn_source)
ch_fetch.apt_update(fatal=True)
def configure_sources(self):
"""Configure package sources for OVN charms.
The principal charms provide both a `source` and a `ovn-source`
configuration option, and the subordinate charms only provide the
`ovn-source` configuration option.
The `source` configuration option is tied into the charms.openstack
`source_config_key` class variable and is inteded to be used with a
full UCA archive. The default methods and functions will apply special
meaning to the name used for further processing, and as such the
`source` configuration option is not suitable for use with an overlay
archive.
The `ovn-source` configuration option is intended to be used with a
slim overlay archive containing only OVN and its dependencies.
The two configuration options can be used simultaneously, and the
underlying charm-helpers code will write the configuration out into
separate files depending on the value of the options.
Ref: https://github.com/juju/charm-helpers/commit/982319b136b
"""
self.configure_ovn_source()
if self.source_config_key:
self.configure_source()
def install(self, service_masks=None):
"""Extend the default install method.
Mask services before initial installation.
This is done to prevent extraneous standalone DB initialization and
subsequent upgrade to clustered DB when configuration is rendered.
We need to manually create the symlink as the package is not installed
yet and subsequently systemctl(1) has no knowledge of it.
We also configure source before installing as OpenvSwitch and OVN
packages are distributed as part of the UCA.
"""
# NOTE(fnordahl): The actual masks are provided by the release specific
# classes.
service_masks = service_masks or []
for service_file in service_masks:
abs_path_svc = os.path.join('/etc/systemd/system', service_file)
if not os.path.islink(abs_path_svc):
os.symlink('/dev/null', abs_path_svc)
self.configure_sources()
super().install()
def upgrade_charm(self):
"""Extend the default upgrade_charm method."""
super().upgrade_charm()
# Ensure that `config.changed.ovn-source` flag is not set on charm
# upgrade. When upgrading from an older charm, this flag will be
# set even though the config has not changed.
reactive.clear_flag('config.changed.ovn-source')
def ovn_upgrade_available(self, package=None, snap=None):
"""Determine whether an OVN upgrade is available.
Make use of the installed package version and the package version
available in the apt cache to determine availability of new version.
"""
self.configure_sources()
cur_vers = self.get_package_version(self.release_pkg,
apt_cache_sufficient=False)
avail_vers = self.get_package_version(self.release_pkg,
apt_cache_sufficient=True)
ch_fetch.apt_pkg.init()
return ch_fetch.apt_pkg.version_compare(avail_vers, cur_vers) == 1
def upgrade_if_available(self, interfaces_list):
"""Upgrade OVN if an upgrade is available.
At present there is no need to pivot to a release specific charm class
when upgrading OVN. As such we override the default method to keep
this simpler, given OVN versions are not fully represented in the
OpenStack version machinery that the default method relies on.
:param interfaces_list: List of instances of interface classes
:returns: None
"""
if self.ovn_upgrade_available(self.release_pkg):
ch_core.hookenv.status_set('maintenance', 'Rolling upgrade')
self.do_openstack_pkg_upgrade(upgrade_openstack=False)
self.render_with_interfaces(interfaces_list)
def configure_deferred_restarts(self):
if 'enable-auto-restarts' in ch_core.hookenv.config().keys():
deferred_events.configure_deferred_restarts(
self.deferable_services)
# Reactive charms execute perm missing.
os.chmod(
'/var/lib/charm/{}/policy-rc.d'.format(
ch_core.hookenv.service_name()),
0o755)
def states_to_check(self, required_relations=None):
"""Override parent method to add custom messaging.
Note that this method will only override the messaging for certain
relations, any relations we don't know about will get the default
treatment from the parent method.
:param required_relations: Override `required_relations` class instance
variable.
:type required_relations: Optional[List[str]]
:returns: Map of relation name to flags to check presence of
accompanied by status and message.
:rtype: collections.OrderedDict[str, List[Tuple[str, str, str]]]
"""
# Retrieve default state map
states_to_check = super().states_to_check(
required_relations=required_relations)
# The parent method will always return a OrderedDict
if PEER_RELATION in states_to_check:
# for the peer relation we want default messaging for all states
# but connected.
states_to_check[PEER_RELATION] = [
('{}.connected'.format(PEER_RELATION),
'blocked',
'Charm requires peers to operate, add more units. A minimum '
'of 3 is required for HA')
] + [
states for states in states_to_check[PEER_RELATION]
if 'connected' not in states[0]
]
if CERT_RELATION in states_to_check:
# for the certificates relation we want to replace all messaging
states_to_check[CERT_RELATION] = [
# the certificates relation has no connected state
('{}.available'.format(CERT_RELATION),
'blocked',
"'{}' missing".format(CERT_RELATION)),
# we cannot proceed until Vault have provided server
# certificates
('{}.server.certs.available'.format(CERT_RELATION),
'waiting',
"'{}' awaiting server certificate data"
.format(CERT_RELATION)),
]
return states_to_check
@staticmethod
def ovn_sysconfdir():
return '/etc/ovn'
@staticmethod
def ovn_rundir():
return '/var/run/ovn'
def _default_port_list(self, *_):
"""Return list of ports the payload listens to.
The api_ports class attribute can not be used as it does not allow
one service to listen to multiple ports.
:returns: port numbers the payload listens to.
:rtype: List[int]
"""
# NOTE(fnordahl): the port check does not appear to cope with
# ports bound to a specific interface LP: #1843434
return [6641, 6642]
def ports_to_check(self, *_):
"""Return list of ports to check the payload listens too.
The api_ports class attribute can not be used as it does not allow
one service to listen to multiple ports.
:returns: ports numbers the payload listens to.
:rtype List[int]
"""
return self._default_port_list()
def validate_config(self):
"""Validate configuration and inform user of any issues.
:returns: Tuple with status and message describing configuration issue.
:rtype: Tuple[Optional[str],Optional[str]]
"""
tgt_timer = self.config['ovsdb-server-election-timer']
if (tgt_timer > self.max_election_timer or
tgt_timer < self.min_election_timer):
return (
'blocked',
"Invalid configuration: 'ovsdb-server-election-timer' must be "
"> {} < {}."
.format(self.min_election_timer, self.max_election_timer))
return None, None
def custom_assess_status_last_check(self):
"""Customize charm status output.
Checks and notifies for invalid config and adds clustered DB status to
status message.
:returns: Tuple with workload status and message.
:rtype: Tuple[Optional[str],Optional[str]]
"""
invalid_config = self.validate_config()
if invalid_config != (None, None):
return invalid_config
cluster_str = self.cluster_status_message()
if cluster_str:
return ('active', 'Unit is ready ({})'.format(cluster_str))
return None, None
def enable_services(self):
"""Enable services.
:returns: True on success, False on failure.
:rtype: bool
"""
if self.check_if_paused() != (None, None):
return False
for service in self.services:
ch_core.host.service_resume(service)
return True
def cluster_status(self, db):
"""OVN version agnostic cluster_status helper.
:param db: Database to operate on
:type db: str
:returns: Object describing the cluster status or None
:rtype: Optional[ch_ovn.OVNClusterStatus]
"""
try:
# The charm will attempt to retrieve cluster status before OVN
# is clustered and while units are paused, so we need to handle
# errors from this call gracefully.
return ch_ovn.cluster_status(db, rundir=self.ovn_rundir(),
use_ovs_appctl=(
self.release == 'train'))
except (ValueError, subprocess.CalledProcessError) as e:
ch_core.hookenv.log('Unable to get cluster status, ovsdb-server '
'not ready yet?: {}'.format(e),
level=ch_core.hookenv.DEBUG)
return
def cluster_status_message(self):
"""Get cluster status message suitable for use as workload message.
:returns: Textual representation of local unit db and northd status.
:rtype: str
"""
db_leader = []
for db in ('ovnnb_db', 'ovnsb_db',):
status = self.cluster_status(db)
if status and status.is_cluster_leader:
db_leader.append(db)
msg = []
if db_leader:
msg.append('leader: {}'.format(', '.join(db_leader)))
if self.is_northd_active():
msg.append('northd: active')
return ' '.join(msg)
def is_northd_active(self):
"""OVN version agnostic is_northd_active helper.
:returns: True if northd is active, False if not, None if not supported
:rtype: Optional[bool]
"""
if self.release != 'train':
return ch_ovn.is_northd_active()
def run(self, *args):
"""Fork off a proc and run commands, collect output and return code.
:param args: Arguments
:type args: Union
:returns: subprocess.CompletedProcess object
:rtype: subprocess.CompletedProcess
:raises: subprocess.CalledProcessError
"""
cp = subprocess.run(
args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=True,
universal_newlines=True)
ch_core.hookenv.log(cp, level=ch_core.hookenv.INFO)
def join_cluster(self, db_file, schema_name, local_conn, remote_conn):
"""Maybe create a OVSDB file with remote peer connection information.
This function will return immediately if the database file already
exists.
Because of a shortcoming in the ``ovn-ctl`` script used to start the
OVN databases we call to ``ovsdb-tool join-cluster`` ourself.
That will create a database file on disk with the required information
and the ``ovn-ctl`` script will not touch it.
The ``ovn-ctl`` ``db-nb-cluster-remote-addr`` and
``db-sb-cluster-remote-addr`` configuration options only take one
remote and one must be provided for correct startup, but the values in
the on-disk database file will be used by the ``ovsdb-server`` process.
:param db_file: Full path to OVSDB file
:type db_file: str
:param schema_name: OVSDB Schema [OVN_Northbound, OVN_Southbound]
:type schema_name: str
:param local_conn: Connection string for local unit
:type local_conn: Union[str, ...]
:param remote_conn: Connection string for remote unit(s)
:type remote_conn: Union[str, ...]
:raises: subprocess.CalledProcessError
"""
if self.release == 'train':
absolute_path = os.path.join('/var/lib/openvswitch', db_file)
else:
absolute_path = os.path.join('/var/lib/ovn', db_file)
if os.path.exists(absolute_path):
ch_core.hookenv.log('OVN database "{}" exists on disk, not '
'creating a new one joining cluster',
level=ch_core.hookenv.DEBUG)
return
cmd = ['ovsdb-tool', 'join-cluster', absolute_path, schema_name]
cmd.extend(list(local_conn))
cmd.extend(list(remote_conn))
ch_core.hookenv.log(cmd, level=ch_core.hookenv.INFO)
self.run(*cmd)
def configure_tls(self, certificates_interface=None):
"""Override default handler prepare certs per OVNs taste.
:param certificates_interface: Certificates interface if present
:type certificates_interface: Optional[reactive.Endpoint]
:raises: subprocess.CalledProcessError
"""
tls_objects = self.get_certs_and_keys(
certificates_interface=certificates_interface)
for tls_object in tls_objects:
with open(
self.options.ovn_ca_cert, 'w') as crt:
chain = tls_object.get('chain')
if chain:
crt.write(tls_object['ca'] + os.linesep + chain)
else:
crt.write(tls_object['ca'])
self.configure_cert(self.ovn_sysconfdir(),
tls_object['cert'],
tls_object['key'],
cn='host')
break
def configure_ovn_listener(self, db, port_map):
"""Create or update OVN listener configuration.
:param db: Database to operate on, 'nb' or 'sb'
:type db: str
:param port_map: Dictionary with port number and associated settings
:type port_map: Dict[int,Dict[str,str]]
:raises: ValueError
"""
if db not in ('nb', 'sb'):
raise ValueError
# NOTE: There is one individual OVSDB cluster leader for each
# of the OVSDB databases and throughout a deployment lifetime
# they are not necessarilly the same as the charm leader.
#
# However, at bootstrap time the OVSDB cluster leaders will
# coincide with the charm leader.
status = self.cluster_status('ovn{}_db'.format(db))
if status and status.is_cluster_leader:
ch_core.hookenv.log('is_cluster_leader {}'.format(db),
level=ch_core.hookenv.DEBUG)
connections = ch_ovsdb.SimpleOVSDB(
'ovn-{}ctl'.format(db)).connection
for port, settings in port_map.items():
ch_core.hookenv.log('port {} {}'.format(port, settings),
level=ch_core.hookenv.DEBUG)
# discover and create any non-existing listeners first
for connection in connections.find(
'target="pssl:{}"'.format(port)):
break
else:
ch_core.hookenv.log('create port {}'.format(port),
level=ch_core.hookenv.DEBUG)
# NOTE(fnordahl) the listener configuration is written to
# the database and used by all units, so we cannot bind to
# specific space/address here. We might consider not
# using listener configuration from DB, but that is
# currently not supported by ``ovn-ctl`` script.
self.run('ovn-{}ctl'.format(db),
'--',
'--id=@connection',
'create', 'connection',
'target="pssl:{}"'.format(port),
'--',
'add', '{}_Global'.format(db.upper()),
'.', 'connections', '@connection')
# set/update connection settings
for connection in connections.find(
'target="pssl:{}"'.format(port)):
for k, v in settings.items():
ch_core.hookenv.log(
'set {} {} {}'
.format(str(connection['_uuid']), k, v),
level=ch_core.hookenv.DEBUG)
connections.set(str(connection['_uuid']), k, v)
def configure_ovsdb_election_timer(self, db, tgt_timer):
"""Set the OVSDB cluster Raft election timer.
Note that the OVSDB Server will refuse to decrease or increase this
value more than 2x the current value, however we should let the end
user of the charm set this to whatever they want. Paper over the
reality by iteratively decreasing / increasing the value in a safe
pace.
:param db: Database to operate on, 'nb' or 'sb'
:type db: str
:param tgt_timer: Target value for election timer in seconds
:type tgt_timer: int
:raises: ValueError
"""
if db not in ('nb', 'sb'):
raise ValueError
if (tgt_timer > self.max_election_timer or
tgt_timer < self.min_election_timer):
# Invalid target timer, log error as well as inform user through
# workload status+message, please refer to
# `custom_assess_status_last_check` for implementation detail.
ch_core.hookenv.log('Attempt to set election timer to invalid '
'value: {} (min {}, max {})'
.format(
tgt_timer,
self.min_election_timer,
self.max_election_timer),
level=ch_core.hookenv.ERROR)
return
# OVN uses ms as unit for the election timer
tgt_timer = tgt_timer * 1000
ovn_db = 'ovn{}_db'.format(db)
ovn_schema = 'OVN_Northbound' if db == 'nb' else 'OVN_Southbound'
status = self.cluster_status(ovn_db)
if status and status.is_cluster_leader:
ch_core.hookenv.log('is_cluster_leader {}'.format(db),
level=ch_core.hookenv.DEBUG)
cur_timer = status.election_timer
if tgt_timer == cur_timer:
ch_core.hookenv.log('Election timer already set to target '
'value: {} == {}'
.format(tgt_timer, cur_timer),
level=ch_core.hookenv.DEBUG)
return
# to be able to reuse the change loop to both increase and decrease
# the timer we assign the operators used to variables
if tgt_timer > cur_timer:
# when increasing timer, we will multiply the value
change_op = operator.mul
# when increasing timer, we want the smaller between target
# value and current value multiplied with 2
change_select = min
else:
# when decreasing timer, we will divide the value and do not
# want fractional values
change_op = operator.floordiv
# when decreasing timer, we want the larger of target value and
# current value divided by 2
change_select = max
while status and status.is_cluster_leader and (
status.election_timer != tgt_timer):
# election timer decrease/increase cannot be more than 2x
# current value per iteration
change_timer = change_select(
change_op(cur_timer, 2), tgt_timer)
ch_core.hookenv.status_set(
'maintenance',
'change {} election timer {}ms -> {}ms'
.format(ovn_schema, cur_timer, change_timer))
ch_ovn.ovn_appctl(
ovn_db, (
'cluster/change-election-timer',
ovn_schema,
str(change_timer),
),
rundir=self.ovn_rundir(),
use_ovs_appctl=(self.release == 'train'))
# wait for an election window to pass before changing the value
# again
time.sleep((cur_timer + change_timer) / 1000)
cur_timer = change_timer
status = self.cluster_status(ovn_db)
def configure_ovn(self, nb_port, sb_port, sb_admin_port):
"""Create or update OVN listener configuration.
:param nb_port: Port for Northbound DB listener
:type nb_port: int
:param sb_port: Port for Southbound DB listener
:type sb_port: int
:param sb_admin_port: Port for cluster private Southbound DB listener
:type sb_admin_port: int
"""
inactivity_probe = int(
self.config['ovsdb-server-inactivity-probe']) * 1000
self.configure_ovn_listener(
'nb', {
nb_port: {
'inactivity_probe': inactivity_probe,
},
})
self.configure_ovn_listener(
'sb', {
sb_port: {
'role': 'ovn-controller',
'inactivity_probe': inactivity_probe,
},
})
self.configure_ovn_listener(
'sb', {
sb_admin_port: {
'inactivity_probe': inactivity_probe,
},
})
election_timer = self.config['ovsdb-server-election-timer']
self.configure_ovsdb_election_timer('nb', election_timer)
self.configure_ovsdb_election_timer('sb', election_timer)
@staticmethod
def initialize_firewall():
"""Initialize firewall.
Note that this function is disruptive to active connections and should
only be called when necessary.
"""
# set default allow
ch_ufw.enable()
ch_ufw.default_policy('allow', 'incoming')
ch_ufw.default_policy('allow', 'outgoing')
ch_ufw.default_policy('allow', 'routed')
def configure_firewall(self, port_addr_map):
"""Configure firewall.
Lock down access to ports not protected by OVN RBAC.
:param port_addr_map: Map of ports to addresses to allow.
:type port_addr_map: Dict[Tuple[int, ...], Optional[Iterator]]
:param allowed_hosts: Hosts allowed to connect.
:type allowed_hosts: Iterator
"""
ufw_comment = 'charm-' + self.name
# reject connection to protected ports
for port in set().union(*port_addr_map.keys()):
ch_ufw.modify_access(src=None, dst='any', port=port,
proto='tcp', action='reject',
comment=ufw_comment)
# allow connections from provided addresses
allowed_addrs = {}
for ports, addrs in port_addr_map.items():
# store List copy of addrs to iterate over it multiple times
_addrs = list(addrs or [])
for port in ports:
for addr in _addrs:
ch_ufw.modify_access(addr, port=port, proto='tcp',
action='allow', prepend=True,
comment=ufw_comment)
allowed_addrs[addr] = 1
# delete any rules managed by us that do not match provided addresses
delete_rules = []
for num, rule in ch_ufw.status():
if 'comment' in rule and rule['comment'] == ufw_comment:
if (rule['action'] == 'allow in' and
rule['from'] not in allowed_addrs):
delete_rules.append(num)
for rule in sorted(delete_rules, reverse=True):
ch_ufw.modify_access(None, dst=None, action='delete', index=rule)
def render_nrpe(self):
"""Configure Nagios NRPE checks."""
hostname = nrpe.get_nagios_hostname()
current_unit = nrpe.get_nagios_unit_name()
charm_nrpe = nrpe.NRPE(hostname=hostname)
nrpe.add_init_service_checks(
charm_nrpe, self.nrpe_check_services, current_unit)
charm_nrpe.write()
def custom_assess_status_check(self):
"""Report deferred events in charm status message."""
state = None
message = None
deferred_events.check_restart_timestamps()
events = collections.defaultdict(set)
for e in deferred_events.get_deferred_events():
events[e.action].add(e.service)
for action, svcs in events.items():
svc_msg = "Services queued for {}: {}".format(
action, ', '.join(sorted(svcs)))
state = 'active'
if message:
message = "{}. {}".format(message, svc_msg)
else:
message = svc_msg
deferred_hooks = deferred_events.get_deferred_hooks()
if deferred_hooks:
state = 'active'
svc_msg = "Hooks skipped due to disabled auto restarts: {}".format(
', '.join(sorted(deferred_hooks)))
if message:
message = "{}. {}".format(message, svc_msg)
else:
message = svc_msg
return state, message
def assess_exporter(self):
is_installed = snap.is_installed('prometheus-ovn-exporter')
channel = None
channel = self.options.ovn_exporter_snap_channel
if channel is None:
if is_installed:
snap.remove('prometheus-ovn-exporter')
return
if is_installed:
snap.refresh('prometheus-ovn-exporter', channel=channel,
devmode=True)
else:
snap.install('prometheus-ovn-exporter', channel=channel,
devmode=True)
@staticmethod
def leave_cluster():
"""Run commands to remove servers running on this unit from cluster.
In case the commands fail, an ERROR message will be logged.
:return: None
:rtype: None
"""
try:
ch_core.hookenv.log(
"Removing self from Southbound cluster",
ch_core.hookenv.INFO
)
ch_ovn.ovn_appctl("ovnsb_db", ("cluster/leave", "OVN_Southbound"))
except subprocess.CalledProcessError:
ch_core.hookenv.log(
"Failed to leave Southbound cluster. You can use "
"'cluster-kick' juju action on remaining units to "
"remove lingering cluster members.",
ch_core.hookenv.ERROR
)
try:
ch_core.hookenv.log(
"Removing self from Northbound cluster",
ch_core.hookenv.INFO
)
ch_ovn.ovn_appctl("ovnnb_db", ("cluster/leave", "OVN_Northbound"))
except subprocess.CalledProcessError:
ch_core.hookenv.log(
"Failed to leave Northbound cluster. You can use "
"'cluster-kick' juju action on remaining units to "
"remove lingering cluster members.",
ch_core.hookenv.ERROR
)
@staticmethod
def is_server_in_cluster(server_ip, cluster_status):
"""Parse cluster status and find if server with given IP is part of it.
:param server_ip: IP of a server to search.
:type server_ip: str
:param cluster_status: Cluster status to parse.
:type cluster_status: ch_ovn.OVNClusterStatus
:return: True if server is part of the cluster. Otherwise, False.
:rtype: bool
"""
remote_unit_url = "ssl:{}:".format(server_ip)
return any(
list(server)[1].startswith(remote_unit_url)
for server in cluster_status.servers
)
def wait_for_server_leave(self, server_ip, timeout=30):
"""Wait for servers with specified IP to leave SB and NB clusters.
:param server_ip: IP of the server that should no longer be part of
the clusters.
:type server_ip: str
:param timeout: How many seconds should this function wait for the
servers to leave. The timeout should be an increment of 5.
:return: True if servers from selected unit departed within the
timeout window. Otherwise, it returns False.
:rtype: bool
"""
tick = 5
timer = 0
unit_in_sb_cluster = unit_in_nb_cluster = True
servers_left = False
wait_sb_msg = "Waiting for {} to leave Southbound cluster".format(
server_ip
)
wait_nb_msg = "Waiting for {} to leave Northbound cluster".format(
server_ip
)
while timer < timeout:
if unit_in_sb_cluster:
ch_core.hookenv.log(wait_sb_msg, ch_core.hookenv.INFO)
unit_in_sb_cluster = self.is_server_in_cluster(
server_ip,
self.cluster_status("ovnsb_db")
)
if unit_in_nb_cluster:
ch_core.hookenv.log(wait_nb_msg, ch_core.hookenv.INFO)
unit_in_nb_cluster = self.is_server_in_cluster(
server_ip,
self.cluster_status("ovnnb_db")
)
if not unit_in_sb_cluster and not unit_in_nb_cluster:
servers_left = True
ch_core.hookenv.log(
"{} servers left Northbound and Southbound OVN "
"clusters.".format(server_ip),
ch_core.hookenv.INFO
)
break
time.sleep(tick)
timer += tick
return servers_left
class TrainOVNCentralCharm(BaseOVNCentralCharm):
# OpenvSwitch and OVN is distributed as part of the Ubuntu Cloud Archive
# Pockets get their name from OpenStack releases
release = 'train'
# NOTE(fnordahl) we have to replace the package sysv init script with
# systemd service files, this should be removed from the charm when the
# systemd service files committed to Focal can be backported to the Train
# UCA.
#
# The issue that triggered this change is that to be able to pass the
# correct command line arguments to ``ovn-nortrhd`` we need to create
# a ``/etc/openvswitch/ovn-northd-db-params.conf`` which has the side
# effect of profoundly changing the behaviour of the ``ovn-ctl`` tool
# that the ``ovn-central`` init script makes use of.
#
# https://github.com/ovn-org/ovn/blob/dc0e10c068c20c4e59c9c86ecee26baf8ed50e90/utilities/ovn-ctl#L323
def __init__(self, **kwargs):
"""Override class init to adjust restart_map for Train.
NOTE(fnordahl): the restart_map functionality in charms.openstack
combines the process of writing a charm template to disk and
restarting a service whenever the target file changes.
In this instance we are only interested in getting the files written
to disk. The restart operation will be taken care of when
``/etc/default/ovn-central`` as defined in ``BaseOVNCentralCharm``.
"""
super().__init__(**kwargs)
self.restart_map.update({
'/lib/systemd/system/ovn-central.service': [],
'/lib/systemd/system/ovn-northd.service': [],
'/lib/systemd/system/ovn-nb-ovsdb.service': [],
'/lib/systemd/system/ovn-sb-ovsdb.service': [],
})
self.nrpe_check_services = [
'ovn-northd',
'ovn-nb-ovsdb',
'ovn-sb-ovsdb',
]
def install(self):
"""Override charm install method.
NOTE(fnordahl) At Train, the OVN central components is packaged with
a dependency on openvswitch-switch, but it does not need the switch
or stock ovsdb running.
"""
service_masks = [
'openvswitch-switch.service',
'ovs-vswitchd.service',
'ovsdb-server.service',
'ovn-central.service',
]
super().install(service_masks=service_masks)
@staticmethod
def ovn_sysconfdir():
return '/etc/openvswitch'
@staticmethod
def ovn_rundir():
return '/var/run/openvswitch'
class UssuriOVNCentralCharm(BaseOVNCentralCharm):
# OpenvSwitch and OVN is distributed as part of the Ubuntu Cloud Archive
# Pockets get their name from OpenStack releases
release = 'ussuri'
def __init__(self, **kwargs):
"""Override class init to adjust service map for Ussuri."""
super().__init__(**kwargs)
# We need to list the OVN ovsdb-server services explicitly so they get
# unmasked on render of ``ovn-central``.
self.services.extend([
'ovn-ovsdb-server-nb',
'ovn-ovsdb-server-sb',
])
self.nrpe_check_services = [
'ovn-northd',
'ovn-ovsdb-server-nb',
'ovn-ovsdb-server-sb',
]
def install(self):
"""Override charm install method."""
# This is done to prevent extraneous standalone DB initialization and
# subsequent upgrade to clustered DB when configuration is rendered.
service_masks = [
'ovn-central.service',
'ovn-ovsdb-server-nb.service',
'ovn-ovsdb-server-sb.service',
]
super().install(service_masks=service_masks)
class WallabyOVNCentralCharm(UssuriOVNCentralCharm):
# OpenvSwitch and OVN is distributed as part of the Ubuntu Cloud Archive
# Pockets get their name from OpenStack releases
release = 'wallaby'
packages = ['ovn-central', 'openstack-release']