James Page f8c994ad42 Correct ports for K8S service patching
Ensure that the NB and SB ports are configured for the service
rather than the clustering ports.

Change-Id: I7c23902195046005ec9d137c2dbf3774331a3f18
2022-09-30 12:18:27 +01:00

449 lines
15 KiB
Python
Executable File

#!/usr/bin/env python3
"""OVN Central Operator Charm.
This charm provide Glance services as part of an OpenStack deployment
"""
import ovn
import ovsdb as ch_ovsdb
import logging
from typing import List, Mapping
import ops.charm
from ops.framework import StoredState
from ops.main import main
import ops_sunbeam.charm as sunbeam_charm
import ops_sunbeam.core as sunbeam_core
import ops_sunbeam.relation_handlers as sunbeam_rhandlers
import ops_sunbeam.config_contexts as sunbeam_ctxts
import ops_sunbeam.ovn.container_handlers as ovn_chandlers
import ops_sunbeam.ovn.config_contexts as ovn_ctxts
import ops_sunbeam.ovn.relation_handlers as ovn_rhandlers
import charms.sunbeam_ovn_central_operator.v0.ovsdb as ovsdb
from charms.observability_libs.v0.kubernetes_service_patch \
import KubernetesServicePatch
logger = logging.getLogger(__name__)
OVN_SB_DB_CONTAINER = "ovn-sb-db-server"
OVN_NB_DB_CONTAINER = "ovn-nb-db-server"
OVN_NORTHD_CONTAINER = "ovn-northd"
OVN_DB_CONTAINERS = [OVN_SB_DB_CONTAINER, OVN_NB_DB_CONTAINER]
class OVNNorthBPebbleHandler(ovn_chandlers.OVNPebbleHandler):
@property
def wrapper_script(self):
return '/root/ovn-northd-wrapper.sh'
@property
def status_command(self):
return '/usr/share/ovn/scripts/ovn-ctl status_northd'
@property
def service_description(self):
return 'OVN Northd'
def default_container_configs(self):
_cc = super().default_container_configs()
_cc.append(
sunbeam_core.ContainerConfigFile(
'/etc/ovn/ovn-northd-db-params.conf',
'root',
'root'))
return _cc
class OVNNorthBDBPebbleHandler(ovn_chandlers.OVNPebbleHandler):
@property
def wrapper_script(self):
return '/root/ovn-nb-db-server-wrapper.sh'
@property
def status_command(self):
# This command always return 0 even if the DB service
# is not running, so adding healthcheck with tcp check
return '/usr/share/ovn/scripts/ovn-ctl status_ovsdb'
@property
def service_description(self):
return 'OVN North Bound DB'
def default_container_configs(self):
_cc = super().default_container_configs()
_cc.append(
sunbeam_core.ContainerConfigFile(
'/root/ovn-nb-cluster-join.sh',
'root',
'root'))
return _cc
def get_healthcheck_layer(self) -> dict:
"""Health check pebble layer.
:returns: pebble health check layer configuration for OVN NB DB
:rtype: dict
"""
return {
"checks": {
"online": {
"override": "replace",
"level": "ready",
"tcp": {
"port": 6641
}
},
}
}
class OVNSouthBDBPebbleHandler(ovn_chandlers.OVNPebbleHandler):
@property
def wrapper_script(self):
return '/root/ovn-sb-db-server-wrapper.sh'
@property
def status_command(self):
# This command always return 0 even if the DB service
# is not running, so adding healthcheck with tcp check
return '/usr/share/ovn/scripts/ovn-ctl status_ovsdb'
@property
def service_description(self):
return 'OVN South Bound DB'
def default_container_configs(self):
_cc = super().default_container_configs()
_cc.append(
sunbeam_core.ContainerConfigFile(
'/root/ovn-sb-cluster-join.sh',
'root',
'root'))
return _cc
def get_healthcheck_layer(self) -> dict:
"""Health check pebble layer.
:returns: pebble health check layer configuration for OVN SB DB
:rtype: dict
"""
return {
"checks": {
"online": {
"override": "replace",
"level": "ready",
"tcp": {
"port": 6642
}
},
}
}
class OVNCentralOperatorCharm(sunbeam_charm.OSBaseOperatorCharm):
"""Charm the service."""
_state = StoredState()
mandatory_relations = {
'certificates',
'peers'
}
def __init__(self, framework):
super().__init__(framework)
self.service_patcher = KubernetesServicePatch(
self,
[
('northbound', 6641),
('southbound', 6642),
]
)
def get_pebble_handlers(self):
pebble_handlers = [
OVNNorthBPebbleHandler(
self,
OVN_NORTHD_CONTAINER,
'ovn-northd',
self.container_configs,
self.template_dir,
self.openstack_release,
self.configure_charm),
OVNSouthBDBPebbleHandler(
self,
OVN_SB_DB_CONTAINER,
'ovn-sb-db-server',
self.container_configs,
self.template_dir,
self.openstack_release,
self.configure_charm),
OVNNorthBDBPebbleHandler(
self,
OVN_NB_DB_CONTAINER,
'ovn-nb-db-server',
self.container_configs,
self.template_dir,
self.openstack_release,
self.configure_charm)]
return pebble_handlers
def get_relation_handlers(self, handlers=None) -> List[
sunbeam_rhandlers.RelationHandler]:
"""Relation handlers for the service."""
handlers = handlers or []
if self.can_add_handler('peers', handlers):
self.peers = ovn_rhandlers.OVNDBClusterPeerHandler(
self,
'peers',
self.configure_charm,
'peers' in self.mandatory_relations)
handlers.append(self.peers)
if self.can_add_handler('ovsdb-cms', handlers):
self.ovsdb_cms = ovn_rhandlers.OVSDBCMSProvidesHandler(
self,
'ovsdb-cms',
self.configure_charm,
'ovsdb-cms' in self.mandatory_relations)
handlers.append(self.ovsdb_cms)
handlers = super().get_relation_handlers(handlers)
return handlers
@property
def config_contexts(self) -> List[sunbeam_ctxts.ConfigContext]:
"""Configuration contexts for the operator."""
contexts = super().config_contexts
contexts.append(
ovn_ctxts.OVNDBConfigContext(self, "ovs_db"))
return contexts
@property
def databases(self) -> Mapping[str, str]:
"""Databases needed to support this charm.
Return empty dict as no mysql databases are
required.
"""
return {}
def ovn_rundir(self):
return '/var/run/ovn'
def get_pebble_executor(self, container_name):
container = self.unit.get_container(
container_name)
def _run_via_pebble(*args):
process = container.exec(list(args), timeout=5*60)
out, warnings = process.wait_output()
if warnings:
for line in warnings.splitlines():
logger.warning('CMD Out: %s', line.strip())
return out
return _run_via_pebble
def cluster_status(self, db, cmd_executor):
"""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 ovn.cluster_status(
db,
rundir=self.ovn_rundir(),
cmd_executor=cmd_executor)
except (ValueError) as e:
logging.error('Unable to get cluster status, ovsdb-server '
'not ready yet?: {}'.format(e))
return
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 == 'nb':
executor = self.get_pebble_executor(OVN_NB_DB_CONTAINER)
elif db == 'sb':
executor = self.get_pebble_executor(OVN_SB_DB_CONTAINER)
status = self.cluster_status(
'ovn{}_db'.format(db),
cmd_executor=executor)
if status and status.is_cluster_leader:
logging.debug(
'configure_ovn_listener is_cluster_leader {}'.format(db))
connections = ch_ovsdb.SimpleOVSDB(
'ovn-{}ctl'.format(db),
cmd_executor=executor).connection
for port, settings in port_map.items():
logging.debug('port {} {}'.format(port, settings))
# discover and create any non-existing listeners first
for connection in connections.find(
'target="pssl:{}"'.format(port)):
logging.debug('Found port {}'.format(port))
break
else:
logging.debug('Create port {}'.format(port))
executor(
'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():
logging.debug(
'set {} {} {}'
.format(str(connection['_uuid']), k, v))
connections.set(str(connection['_uuid']), k, v)
def configure_charm(self, event: ops.framework.EventBase) -> None:
"""Catchall handler to configure charm services.
"""
if not self.unit.is_leader():
if not self.is_leader_ready():
self.unit.status = ops.model.WaitingStatus(
"Waiting for leader to be ready")
return
missing_leader_data = [
k for k in ['nb_cid', 'sb_cid']
if not self.leader_get(k)]
if missing_leader_data:
logging.debug(f"missing {missing_leader_data} from leader")
self.unit.status = ops.model.WaitingStatus(
"Waiting for data from leader")
return
logging.debug(
"Remote leader is ready and has supplied all data needed")
if not self.relation_handlers_ready():
logging.debug("Aborting charm relations not ready")
return
if not all([ph.pebble_ready for ph in self.pebble_handlers]):
logging.debug(
"Aborting configuration, not all pebble handlers are ready")
return
# Render Config in all containers but init should *NOT* start
# the service.
for ph in self.pebble_handlers:
if ph.pebble_ready:
logging.debug(f"Running init for {ph.service_name}")
ph.init_service(self.contexts())
else:
logging.debug(
f"Not running init for {ph.service_name},"
" container not ready")
if self.unit.is_leader():
# Start services in North/South containers on lead unit
logging.debug("Starting services in DB containers")
for ph in self.get_named_pebble_handlers(OVN_DB_CONTAINERS):
ph.start_service()
# Attempt to setup listers etc
self.configure_ovn()
nb_status = self.cluster_status(
'ovnnb_db',
self.get_pebble_executor(OVN_NB_DB_CONTAINER))
sb_status = self.cluster_status(
'ovnsb_db',
self.get_pebble_executor(OVN_SB_DB_CONTAINER))
logging.debug("Telling peers leader is ready and cluster ids")
self.set_leader_ready()
self.leader_set({
'nb_cid': str(nb_status.cluster_id),
'sb_cid': str(sb_status.cluster_id),
})
else:
logging.debug("Attempting to join OVN_Northbound cluster")
container = self.unit.get_container(OVN_NB_DB_CONTAINER)
process = container.exec(
['bash', '/root/ovn-nb-cluster-join.sh'], timeout=5*60)
out, warnings = process.wait_output()
if warnings:
for line in warnings.splitlines():
logger.warning('CMD Out: %s', line.strip())
logging.debug("Attempting to join OVN_Southbound cluster")
container = self.unit.get_container(OVN_SB_DB_CONTAINER)
process = container.exec(
['bash', '/root/ovn-sb-cluster-join.sh'], timeout=5*60)
out, warnings = process.wait_output()
if warnings:
for line in warnings.splitlines():
logger.warning('CMD Out: %s', line.strip())
logging.debug("Starting services in DB containers")
for ph in self.get_named_pebble_handlers(OVN_DB_CONTAINERS):
ph.start_service()
# Attempt to setup listers etc
self.configure_ovn()
# Start ovn-northd service
ph = self.get_named_pebble_handler(OVN_NORTHD_CONTAINER)
ph.start_service()
# Add healthchecks to the plan
# Healthchecks are added after bootstrap process is completed
# to avoid container restarts during bootstraping
for ph in self.pebble_handlers:
ph.add_healthchecks()
self.unit.status = ops.model.ActiveStatus()
def configure_ovn(self):
inactivity_probe = int(
self.config['ovsdb-server-inactivity-probe']) * 1000
self.configure_ovn_listener(
'nb', {
self.ovsdb_cms.db_nb_port: {
'inactivity_probe': inactivity_probe,
},
})
self.configure_ovn_listener(
'sb', {
self.ovsdb_cms.db_sb_port: {
'role': 'ovn-controller',
'inactivity_probe': inactivity_probe,
},
})
self.configure_ovn_listener(
'sb', {
self.ovsdb_cms.db_sb_admin_port: {
'inactivity_probe': inactivity_probe,
},
})
class OVNCentralXenaOperatorCharm(OVNCentralOperatorCharm):
openstack_release = 'xena'
if __name__ == "__main__":
# Note: use_juju_for_storage=True required per
# https://github.com/canonical/operator/issues/506
main(OVNCentralXenaOperatorCharm, use_juju_for_storage=True)