
Ensure that the NB and SB ports are configured for the service rather than the clustering ports. Change-Id: I7c23902195046005ec9d137c2dbf3774331a3f18
449 lines
15 KiB
Python
Executable File
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)
|