Merge "[nova] add support for nova spice proxy" into main
This commit is contained in:
commit
28f6c50c74
@ -24,7 +24,6 @@ import logging
|
||||
import secrets
|
||||
import socket
|
||||
from typing import (
|
||||
Callable,
|
||||
List,
|
||||
Mapping,
|
||||
Optional,
|
||||
@ -36,9 +35,6 @@ import ops_sunbeam.config_contexts as sunbeam_config_contexts
|
||||
import ops_sunbeam.container_handlers as sunbeam_chandlers
|
||||
import ops_sunbeam.core as sunbeam_core
|
||||
import ops_sunbeam.relation_handlers as sunbeam_rhandlers
|
||||
from ops.charm import (
|
||||
RelationEvent,
|
||||
)
|
||||
from ops.framework import (
|
||||
StoredState,
|
||||
)
|
||||
@ -60,63 +56,6 @@ HEAT_API_PORT = 8004
|
||||
HEAT_API_CFN_PORT = 8000
|
||||
|
||||
|
||||
class TraefikRouteHandler(sunbeam_rhandlers.RelationHandler):
|
||||
"""Base class to handle traefik route relations."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
charm: ops.charm.CharmBase,
|
||||
relation_name: str,
|
||||
callback_f: Callable,
|
||||
mandatory: bool = False,
|
||||
) -> None:
|
||||
"""Run constructor."""
|
||||
super().__init__(charm, relation_name, callback_f, mandatory)
|
||||
|
||||
def setup_event_handler(self) -> ops.framework.Object:
|
||||
"""Configure event handlers for an Ingress relation."""
|
||||
logger.debug("Setting up ingress event handler")
|
||||
from charms.traefik_route_k8s.v0.traefik_route import (
|
||||
TraefikRouteRequirer,
|
||||
)
|
||||
|
||||
interface = TraefikRouteRequirer(
|
||||
self.charm,
|
||||
self.model.get_relation(self.relation_name),
|
||||
self.relation_name,
|
||||
)
|
||||
|
||||
self.framework.observe(interface.on.ready, self._on_ingress_ready)
|
||||
self.framework.observe(
|
||||
self.charm.on[self.relation_name].relation_joined,
|
||||
self._on_traefik_relation_joined,
|
||||
)
|
||||
return interface
|
||||
|
||||
def _on_traefik_relation_joined(self, event: RelationEvent) -> None:
|
||||
"""Handle traefik relation joined event."""
|
||||
# This is passed as None during the init method, so update the
|
||||
# relation attribute in TraefikRouteRequirer
|
||||
self.interface._relation = event.relation
|
||||
|
||||
def _on_ingress_ready(self, event: RelationEvent) -> None:
|
||||
"""Handle ingress relation changed events.
|
||||
|
||||
`event` is an instance of
|
||||
`charms.traefik_k8s.v2.ingress.IngressPerAppReadyEvent`.
|
||||
"""
|
||||
if self.interface.is_ready():
|
||||
self.callback_f(event)
|
||||
|
||||
@property
|
||||
def ready(self) -> bool:
|
||||
"""Whether the handler is ready for use."""
|
||||
if self.charm.unit.is_leader():
|
||||
return bool(self.interface.external_host)
|
||||
else:
|
||||
return self.interface.is_ready()
|
||||
|
||||
|
||||
class HeatAPIPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
|
||||
"""Pebble handler for Heat API container."""
|
||||
|
||||
@ -311,14 +250,14 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
)
|
||||
handlers.append(self.user_id_ops)
|
||||
|
||||
self.traefik_route_public = TraefikRouteHandler(
|
||||
self.traefik_route_public = sunbeam_rhandlers.TraefikRouteHandler(
|
||||
self,
|
||||
"traefik-route-public",
|
||||
self.handle_traefik_ready,
|
||||
"traefik-route-public" in self.mandatory_relations,
|
||||
)
|
||||
handlers.append(self.traefik_route_public)
|
||||
self.traefik_route_internal = TraefikRouteHandler(
|
||||
self.traefik_route_internal = sunbeam_rhandlers.TraefikRouteHandler(
|
||||
self,
|
||||
"traefik-route-internal",
|
||||
self.handle_traefik_ready,
|
||||
|
@ -32,6 +32,8 @@ containers:
|
||||
resource: nova-scheduler-image
|
||||
nova-conductor:
|
||||
resource: nova-conductor-image
|
||||
nova-spiceproxy:
|
||||
resource: nova-spiceproxy-image
|
||||
|
||||
resources:
|
||||
nova-api-image:
|
||||
@ -46,14 +48,18 @@ resources:
|
||||
type: oci-image
|
||||
description: OCI image for OpenStack Nova Conductor
|
||||
upstream-source: ghcr.io/canonical/nova-consolidated:2024.1
|
||||
nova-spiceproxy-image:
|
||||
type: oci-image
|
||||
description: OCI image for OpenStack Nova Spice proxy
|
||||
upstream-source: ghcr.io/canonical/nova-consolidated:2024.1
|
||||
|
||||
requires:
|
||||
ingress-internal:
|
||||
interface: ingress
|
||||
traefik-route-internal:
|
||||
interface: traefik_route
|
||||
optional: true
|
||||
limit: 1
|
||||
ingress-public:
|
||||
interface: ingress
|
||||
traefik-route-public:
|
||||
interface: traefik_route
|
||||
limit: 1
|
||||
database:
|
||||
interface: mysql_client
|
||||
@ -85,7 +91,7 @@ requires:
|
||||
optional: true
|
||||
|
||||
provides:
|
||||
cloud-controller:
|
||||
nova-service:
|
||||
interface: nova
|
||||
|
||||
peers:
|
||||
|
@ -19,6 +19,7 @@ This charm provide Nova services as part of an OpenStack deployment
|
||||
"""
|
||||
|
||||
import logging
|
||||
import socket
|
||||
import uuid
|
||||
from typing import (
|
||||
Callable,
|
||||
@ -33,6 +34,13 @@ import ops_sunbeam.config_contexts as sunbeam_ctxts
|
||||
import ops_sunbeam.container_handlers as sunbeam_chandlers
|
||||
import ops_sunbeam.core as sunbeam_core
|
||||
import ops_sunbeam.relation_handlers as sunbeam_rhandlers
|
||||
from charms.nova_k8s.v0.nova_service import (
|
||||
NovaConfigRequestEvent,
|
||||
NovaServiceProvides,
|
||||
)
|
||||
from ops.charm import (
|
||||
CharmBase,
|
||||
)
|
||||
from ops.main import (
|
||||
main,
|
||||
)
|
||||
@ -45,6 +53,10 @@ logger = logging.getLogger(__name__)
|
||||
NOVA_WSGI_CONTAINER = "nova-api"
|
||||
NOVA_SCHEDULER_CONTAINER = "nova-scheduler"
|
||||
NOVA_CONDUCTOR_CONTAINER = "nova-conductor"
|
||||
NOVA_SPICEPROXY_CONTAINER = "nova-spiceproxy"
|
||||
NOVA_API_INGRESS_NAME = "nova"
|
||||
NOVA_SPICEPROXY_INGRESS_NAME = "nova-spiceproxy"
|
||||
NOVA_SPICEPROXY_INGRESS_PORT = 6182
|
||||
|
||||
|
||||
class WSGINovaMetadataConfigContext(sunbeam_ctxts.ConfigContext):
|
||||
@ -164,6 +176,74 @@ class NovaConductorPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
|
||||
]
|
||||
|
||||
|
||||
class NovaSpiceProxyPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
|
||||
"""Pebble handler for Nova spice proxy."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.enable_service_check = True
|
||||
|
||||
def get_layer(self) -> dict:
|
||||
"""Nova Scheduler service layer.
|
||||
|
||||
:returns: pebble layer configuration for scheduler service
|
||||
:rtype: dict
|
||||
"""
|
||||
return {
|
||||
"summary": "nova spice proxy layer",
|
||||
"description": "pebble configuration for nova services",
|
||||
"services": {
|
||||
"nova-spiceproxy": {
|
||||
"override": "replace",
|
||||
"summary": "Nova Spice Proxy",
|
||||
"command": "nova-spicehtml5proxy",
|
||||
"user": "nova",
|
||||
"group": "nova",
|
||||
},
|
||||
"apache forwarder": {
|
||||
"override": "replace",
|
||||
"summary": "apache",
|
||||
"command": "/usr/sbin/apache2ctl -DFOREGROUND",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def default_container_configs(
|
||||
self,
|
||||
) -> List[sunbeam_core.ContainerConfigFile]:
|
||||
"""Container configurations for handler."""
|
||||
return [
|
||||
sunbeam_core.ContainerConfigFile(
|
||||
"/etc/nova/nova.conf",
|
||||
"root",
|
||||
"nova",
|
||||
0o640,
|
||||
),
|
||||
sunbeam_core.ContainerConfigFile(
|
||||
"/etc/apache2/sites-enabled/nova-spiceproxy-forwarding.conf",
|
||||
self.charm.service_user,
|
||||
self.charm.service_group,
|
||||
0o640,
|
||||
),
|
||||
sunbeam_core.ContainerConfigFile(
|
||||
"/usr/local/share/ca-certificates/ca-bundle.pem",
|
||||
"root",
|
||||
"nova",
|
||||
0o640,
|
||||
),
|
||||
]
|
||||
|
||||
@property
|
||||
def service_ready(self) -> bool:
|
||||
"""Determine whether the service the container provides is running."""
|
||||
if self.enable_service_check:
|
||||
logging.debug("Service checks enabled for nova spice proxy")
|
||||
return super().service_ready
|
||||
else:
|
||||
logging.debug("Service checks disabled for nova spice proxy")
|
||||
return self.pebble_ready
|
||||
|
||||
|
||||
class CloudComputeRequiresHandler(sunbeam_rhandlers.RelationHandler):
|
||||
"""Handles the cloud-compute relation on the requires side."""
|
||||
|
||||
@ -224,6 +304,40 @@ class CloudComputeRequiresHandler(sunbeam_rhandlers.RelationHandler):
|
||||
return True
|
||||
|
||||
|
||||
class NovaServiceProvidesHandler(sunbeam_rhandlers.RelationHandler):
|
||||
"""Handler for nova service relation."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
charm: CharmBase,
|
||||
relation_name: str,
|
||||
callback_f: Callable,
|
||||
):
|
||||
super().__init__(charm, relation_name, callback_f)
|
||||
|
||||
def setup_event_handler(self):
|
||||
"""Configure event handlers for nova service relation."""
|
||||
logger.debug("Setting up Nova service event handler")
|
||||
svc = NovaServiceProvides(
|
||||
self.charm,
|
||||
self.relation_name,
|
||||
)
|
||||
self.framework.observe(
|
||||
svc.on.config_request,
|
||||
self._on_config_request,
|
||||
)
|
||||
return svc
|
||||
|
||||
def _on_config_request(self, event: NovaConfigRequestEvent) -> None:
|
||||
"""Handle Config request event."""
|
||||
self.callback_f(event)
|
||||
|
||||
@property
|
||||
def ready(self) -> bool:
|
||||
"""Report if relation is ready."""
|
||||
return True
|
||||
|
||||
|
||||
class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
"""Charm the service."""
|
||||
|
||||
@ -238,9 +352,31 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
"cell-database",
|
||||
"amqp",
|
||||
"identity-service",
|
||||
"ingress-public",
|
||||
"traefik-route-public",
|
||||
}
|
||||
|
||||
def __init__(self, framework):
|
||||
self.traefik_route_public = None
|
||||
self.traefik_route_internal = None
|
||||
super().__init__(framework)
|
||||
self.framework.observe(
|
||||
self.on.peers_relation_created, self._on_peer_relation_created
|
||||
)
|
||||
self.framework.observe(
|
||||
self.on["peers"].relation_departed, self._on_peer_relation_departed
|
||||
)
|
||||
|
||||
def _on_peer_relation_created(
|
||||
self, event: ops.framework.EventBase
|
||||
) -> None:
|
||||
logger.info("Setting peer unit data")
|
||||
self.peers.set_unit_data({"host": socket.getfqdn()})
|
||||
|
||||
def _on_peer_relation_departed(
|
||||
self, event: ops.framework.EventBase
|
||||
) -> None:
|
||||
self.handle_traefik_ready(event)
|
||||
|
||||
@property
|
||||
def db_sync_cmds(self) -> List[List[str]]:
|
||||
"""DB sync commands for Nova operator."""
|
||||
@ -302,6 +438,50 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
"""Default port for service ingress."""
|
||||
return 8774
|
||||
|
||||
@property
|
||||
def public_url(self) -> str:
|
||||
"""Url for accessing the public endpoint for nova service."""
|
||||
if self.traefik_route_public and self.traefik_route_public.ready:
|
||||
scheme = self.traefik_route_public.interface.scheme
|
||||
external_host = self.traefik_route_public.interface.external_host
|
||||
public_url = (
|
||||
f"{scheme}://{external_host}/{self.model.name}"
|
||||
f"-{NOVA_API_INGRESS_NAME}"
|
||||
)
|
||||
return self.add_explicit_port(public_url)
|
||||
else:
|
||||
return self.add_explicit_port(
|
||||
self.service_url(self.public_ingress_address)
|
||||
)
|
||||
|
||||
@property
|
||||
def internal_url(self) -> str:
|
||||
"""Url for accessing the internal endpoint for nova service."""
|
||||
if self.traefik_route_internal and self.traefik_route_internal.ready:
|
||||
scheme = self.traefik_route_internal.interface.scheme
|
||||
external_host = self.traefik_route_internal.interface.external_host
|
||||
internal_url = (
|
||||
f"{scheme}://{external_host}/{self.model.name}"
|
||||
f"-{NOVA_API_INGRESS_NAME}"
|
||||
)
|
||||
return self.add_explicit_port(internal_url)
|
||||
else:
|
||||
return self.admin_url
|
||||
|
||||
@property
|
||||
def nova_spiceproxy_public_url(self) -> str | None:
|
||||
"""URL for accessing public endpoint for nova spiceproxy service."""
|
||||
if self.traefik_route_public and self.traefik_route_public.ready:
|
||||
scheme = self.traefik_route_public.interface.scheme
|
||||
external_host = self.traefik_route_public.interface.external_host
|
||||
public_url = (
|
||||
f"{scheme}://{external_host}/{self.model.name}"
|
||||
f"-{NOVA_SPICEPROXY_INGRESS_NAME}"
|
||||
)
|
||||
return public_url
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def databases(self) -> Mapping[str, str]:
|
||||
"""Databases needed to support this charm.
|
||||
@ -345,6 +525,14 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
self.template_dir,
|
||||
self.configure_charm,
|
||||
),
|
||||
NovaSpiceProxyPebbleHandler(
|
||||
self,
|
||||
NOVA_SPICEPROXY_CONTAINER,
|
||||
"nova-spiceproxy",
|
||||
[],
|
||||
self.template_dir,
|
||||
self.configure_charm,
|
||||
),
|
||||
]
|
||||
return pebble_handlers
|
||||
|
||||
@ -361,6 +549,32 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
self.register_compute_nodes,
|
||||
)
|
||||
handlers.append(self.compute_nodes)
|
||||
|
||||
if self.can_add_handler("nova-service", handlers):
|
||||
self.config_svc = NovaServiceProvidesHandler(
|
||||
self,
|
||||
"nova-service",
|
||||
self.set_config_from_event,
|
||||
)
|
||||
handlers.append(self.config_svc)
|
||||
|
||||
self.traefik_route_public = sunbeam_rhandlers.TraefikRouteHandler(
|
||||
self,
|
||||
"traefik-route-public",
|
||||
self.handle_traefik_ready,
|
||||
"traefik-route-public" in self.mandatory_relations,
|
||||
[NOVA_API_INGRESS_NAME, NOVA_SPICEPROXY_INGRESS_NAME],
|
||||
)
|
||||
handlers.append(self.traefik_route_public)
|
||||
self.traefik_route_internal = sunbeam_rhandlers.TraefikRouteHandler(
|
||||
self,
|
||||
"traefik-route-internal",
|
||||
self.handle_traefik_ready,
|
||||
"traefik-route-internal" in self.mandatory_relations,
|
||||
[NOVA_API_INGRESS_NAME, NOVA_SPICEPROXY_INGRESS_NAME],
|
||||
)
|
||||
handlers.append(self.traefik_route_internal)
|
||||
|
||||
return handlers
|
||||
|
||||
@property
|
||||
@ -399,6 +613,59 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
]
|
||||
return _cconfigs
|
||||
|
||||
@property
|
||||
def traefik_config(self) -> dict:
|
||||
"""Config to publish to traefik."""
|
||||
model = self.model.name
|
||||
router_cfg = {}
|
||||
# Add routers for both nova-api and nova-spiceproxy
|
||||
for app in NOVA_API_INGRESS_NAME, NOVA_SPICEPROXY_INGRESS_NAME:
|
||||
router_cfg.update(
|
||||
{
|
||||
f"juju-{model}-{app}-router": {
|
||||
"rule": f"PathPrefix(`/{model}-{app}`)",
|
||||
"service": f"juju-{model}-{app}-service",
|
||||
"entryPoints": ["web"],
|
||||
},
|
||||
f"juju-{model}-{app}-router-tls": {
|
||||
"rule": f"PathPrefix(`/{model}-{app}`)",
|
||||
"service": f"juju-{model}-{app}-service",
|
||||
"entryPoints": ["websecure"],
|
||||
"tls": {},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# Get host key value from all units
|
||||
hosts = self.peers.get_all_unit_values(
|
||||
key="host", include_local_unit=True
|
||||
)
|
||||
api_lb_servers = [
|
||||
{"url": f"http://{host}:{self.default_public_ingress_port}"}
|
||||
for host in hosts
|
||||
]
|
||||
spice_lb_servers = [
|
||||
{"url": f"http://{host}:{NOVA_SPICEPROXY_INGRESS_PORT}"}
|
||||
for host in hosts
|
||||
]
|
||||
# Add services for heat-api and heat-api-cfn
|
||||
service_cfg = {
|
||||
f"juju-{model}-{NOVA_API_INGRESS_NAME}-service": {
|
||||
"loadBalancer": {"servers": api_lb_servers},
|
||||
},
|
||||
f"juju-{model}-{NOVA_SPICEPROXY_INGRESS_NAME}-service": {
|
||||
"loadBalancer": {"servers": spice_lb_servers},
|
||||
},
|
||||
}
|
||||
|
||||
config = {
|
||||
"http": {
|
||||
"routers": router_cfg,
|
||||
"services": service_cfg,
|
||||
},
|
||||
}
|
||||
return config
|
||||
|
||||
def get_shared_metadatasecret(self):
|
||||
"""Return the shared metadata secret."""
|
||||
return self.leader_get(self.shared_metadata_secret_key)
|
||||
@ -453,6 +720,42 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
logger.exception("Failed to discover hosts for cell1")
|
||||
raise
|
||||
|
||||
def _update_service_endpoints(self):
|
||||
try:
|
||||
if self.id_svc.update_service_endpoints:
|
||||
logger.info(
|
||||
"Updating service endpoints after ingress relation changed"
|
||||
)
|
||||
self.id_svc.update_service_endpoints(self.service_endpoints)
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
|
||||
def handle_traefik_ready(self, event: ops.framework.EventBase):
|
||||
"""Handle Traefik route ready callback."""
|
||||
if not self.unit.is_leader():
|
||||
logger.debug(
|
||||
"Not a leader unit, not updating traefik route config"
|
||||
)
|
||||
return
|
||||
|
||||
if self.traefik_route_public:
|
||||
logger.debug("Sending traefik config for public interface")
|
||||
self.traefik_route_public.interface.submit_to_traefik(
|
||||
config=self.traefik_config
|
||||
)
|
||||
|
||||
if self.traefik_route_public.ready:
|
||||
self._update_service_endpoints()
|
||||
|
||||
if self.traefik_route_internal:
|
||||
logger.debug("Sending traefik config for internal interface")
|
||||
self.traefik_route_internal.interface.submit_to_traefik(
|
||||
config=self.traefik_config
|
||||
)
|
||||
|
||||
if self.traefik_route_internal.ready:
|
||||
self._update_service_endpoints()
|
||||
|
||||
def get_cell_uuid(self, cell, fatal=True):
|
||||
"""Returns the cell UUID from the name.
|
||||
|
||||
@ -506,6 +809,7 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
"""Callback handler for nova operator configuration."""
|
||||
if not self.peers.ready:
|
||||
return
|
||||
|
||||
metadata_secret = self.get_shared_metadatasecret()
|
||||
if metadata_secret:
|
||||
logger.debug("Found metadata secret in leader DB")
|
||||
@ -513,6 +817,8 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
if self.unit.is_leader():
|
||||
logger.debug("Creating metadata secret")
|
||||
self.set_shared_metadatasecret()
|
||||
self.handle_traefik_ready(event)
|
||||
self.set_config_on_update()
|
||||
else:
|
||||
logger.debug("Metadata secret not ready")
|
||||
return
|
||||
@ -522,6 +828,23 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
NOVA_SCHEDULER_CONTAINER
|
||||
)
|
||||
scheduler_handler.enable_service_check = False
|
||||
|
||||
# Enable apache proxy_http module for nova-spiceproxy apache forwarding
|
||||
nova_spice_handler = self.get_named_pebble_handler(
|
||||
NOVA_SPICEPROXY_CONTAINER
|
||||
)
|
||||
if nova_spice_handler.pebble_ready:
|
||||
nova_spice_handler.execute(
|
||||
["a2enmod", "proxy_http"], exception_on_error=True
|
||||
)
|
||||
nova_spice_handler.execute(
|
||||
["apt-get", "update"], exception_on_error=True
|
||||
)
|
||||
nova_spice_handler.execute(
|
||||
["apt", "install", "spice-html5", "-y"],
|
||||
exception_on_error=True,
|
||||
)
|
||||
|
||||
super().configure_charm(event)
|
||||
if scheduler_handler.pebble_ready:
|
||||
logging.debug("Starting nova scheduler service, pebble ready")
|
||||
@ -534,6 +857,26 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||
"Not starting nova scheduler service, pebble not ready"
|
||||
)
|
||||
|
||||
def set_config_from_event(self, event: ops.framework.EventBase) -> None:
|
||||
"""Set config in relation data."""
|
||||
if self.nova_spiceproxy_public_url:
|
||||
self.config_svc.interface.set_config(
|
||||
relation=event.relation,
|
||||
nova_spiceproxy_url=self.nova_spiceproxy_public_url,
|
||||
)
|
||||
else:
|
||||
logging.debug("Nova spiceproxy not yet set, not sending config")
|
||||
|
||||
def set_config_on_update(self) -> None:
|
||||
"""Set config on relation on update of local data."""
|
||||
if self.nova_spiceproxy_public_url:
|
||||
self.config_svc.interface.set_config(
|
||||
relation=None,
|
||||
nova_spiceproxy_url=self.nova_spiceproxy_public_url,
|
||||
)
|
||||
else:
|
||||
logging.debug("Nova spiceproxy not yet set, not sending config")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(NovaOperatorCharm)
|
||||
|
@ -0,0 +1,15 @@
|
||||
Listen 6182
|
||||
|
||||
<VirtualHost *:6182>
|
||||
ProxyPreserveHost On
|
||||
ProxyRequests Off
|
||||
{% if traefik_route_public and traefik_route_public.nova_spiceproxy_ingress_path -%}
|
||||
ProxyPass {{ traefik_route_public.nova_spiceproxy_ingress_path }} http://localhost:6082/
|
||||
ProxyPassReverse {{ traefik_route_public.nova_spiceproxy_ingress_path }} http://localhost:6082/
|
||||
{% endif -%}
|
||||
ProxyPass / http://localhost:6082/
|
||||
ProxyPassReverse / http://localhost:6082/
|
||||
ErrorLog {{ wsgi_config.error_log }}
|
||||
CustomLog {{ wsgi_config.custom_log }} combined
|
||||
</VirtualHost>
|
||||
|
@ -4,8 +4,8 @@ Listen {{ wsgi_nova_metadata.public_port }}
|
||||
WSGIDaemonProcess nova-api processes=4 threads=1 user={{ wsgi_config.user }} group={{ wsgi_config.group }} \
|
||||
display-name=%{GROUP}
|
||||
WSGIProcessGroup nova-api
|
||||
{% if ingress_public and ingress_public.ingress_path -%}
|
||||
WSGIScriptAlias {{ ingress_public.ingress_path }} {{ wsgi_config.wsgi_public_script }}
|
||||
{% if traefik_route_public and traefik_route_public.nova_ingress_path -%}
|
||||
WSGIScriptAlias {{ traefik_route_public.nova_ingress_path }} {{ wsgi_config.wsgi_public_script }}
|
||||
{% endif -%}
|
||||
WSGIScriptAlias / {{ wsgi_config.wsgi_public_script }}
|
||||
WSGIApplicationGroup %{GLOBAL}
|
||||
|
@ -18,6 +18,9 @@
|
||||
|
||||
import charm
|
||||
import ops_sunbeam.test_utils as test_utils
|
||||
from ops.testing import (
|
||||
Harness,
|
||||
)
|
||||
|
||||
|
||||
class _NovaTestOperatorCharm(charm.NovaOperatorCharm):
|
||||
@ -77,11 +80,34 @@ class TestNovaOperatorCharm(test_utils.CharmTestCase):
|
||||
self.addCleanup(self.harness.cleanup)
|
||||
self.harness.begin()
|
||||
|
||||
def add_complete_ingress_relation(self, harness: Harness) -> None:
|
||||
"""Add complete traefik-route relations."""
|
||||
harness.add_relation(
|
||||
"traefik-route-public",
|
||||
"nova",
|
||||
app_data={"external_host": "dummy-ip", "scheme": "http"},
|
||||
)
|
||||
harness.add_relation(
|
||||
"traefik-route-internal",
|
||||
"nova",
|
||||
app_data={"external_host": "dummy-ip", "scheme": "http"},
|
||||
)
|
||||
|
||||
def add_db_relation(self, harness: Harness, name: str) -> str:
|
||||
"""Add db relation."""
|
||||
rel_id = harness.add_relation(name, "mysql")
|
||||
harness.add_relation_unit(rel_id, "mysql/0")
|
||||
harness.add_relation_unit(rel_id, "mysql/0")
|
||||
harness.update_relation_data(
|
||||
rel_id, "mysql/0", {"ingress-address": "10.0.0.3"}
|
||||
)
|
||||
return rel_id
|
||||
|
||||
def test_pebble_ready_handler(self):
|
||||
"""Test pebble ready handler."""
|
||||
self.assertEqual(self.harness.charm.seen_events, [])
|
||||
test_utils.set_all_pebbles_ready(self.harness)
|
||||
self.assertEqual(len(self.harness.charm.seen_events), 3)
|
||||
self.assertEqual(len(self.harness.charm.seen_events), 4)
|
||||
|
||||
def test_all_relations(self):
|
||||
"""Test all integrations for operator."""
|
||||
@ -89,12 +115,12 @@ class TestNovaOperatorCharm(test_utils.CharmTestCase):
|
||||
test_utils.set_all_pebbles_ready(self.harness)
|
||||
# this adds all the default/common relations
|
||||
test_utils.add_all_relations(self.harness)
|
||||
test_utils.add_complete_ingress_relation(self.harness)
|
||||
self.add_complete_ingress_relation(self.harness)
|
||||
|
||||
# but nova has some extra db relations, so add them manually here
|
||||
rel_id = add_db_relation(self.harness, "api-database")
|
||||
rel_id = self.add_db_relation(self.harness, "api-database")
|
||||
test_utils.add_db_relation_credentials(self.harness, rel_id)
|
||||
rel_id = add_db_relation(self.harness, "cell-database")
|
||||
rel_id = self.add_db_relation(self.harness, "cell-database")
|
||||
test_utils.add_db_relation_credentials(self.harness, rel_id)
|
||||
|
||||
setup_cmds = [
|
||||
@ -122,14 +148,3 @@ class TestNovaOperatorCharm(test_utils.CharmTestCase):
|
||||
]
|
||||
for f in config_files:
|
||||
self.check_file("nova-api", f)
|
||||
|
||||
|
||||
def add_db_relation(harness, name) -> str:
|
||||
"""Add db relation."""
|
||||
rel_id = harness.add_relation(name, "mysql")
|
||||
harness.add_relation_unit(rel_id, "mysql/0")
|
||||
harness.add_relation_unit(rel_id, "mysql/0")
|
||||
harness.update_relation_data(
|
||||
rel_id, "mysql/0", {"ingress-address": "10.0.0.3"}
|
||||
)
|
||||
return rel_id
|
||||
|
@ -26,6 +26,8 @@ requires:
|
||||
receive-ca-cert:
|
||||
interface: certificate_transfer
|
||||
optional: true
|
||||
nova-service:
|
||||
interface: nova
|
||||
|
||||
provides:
|
||||
cos-agent:
|
||||
|
@ -46,6 +46,10 @@ from charms.ceilometer_k8s.v0.ceilometer_service import (
|
||||
from charms.grafana_agent.v0.cos_agent import (
|
||||
COSAgentProvider,
|
||||
)
|
||||
from charms.nova_k8s.v0.nova_service import (
|
||||
NovaConfigChangedEvent,
|
||||
NovaServiceGoneAwayEvent,
|
||||
)
|
||||
from cryptography import (
|
||||
x509,
|
||||
)
|
||||
@ -183,7 +187,12 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm):
|
||||
METADATA_SECRET_KEY = "ovn-metadata-proxy-shared-secret"
|
||||
DEFAULT_SECRET_LENGTH = 32
|
||||
|
||||
mandatory_relations = {"amqp", "identity-credentials", "ovsdb-cms"}
|
||||
mandatory_relations = {
|
||||
"amqp",
|
||||
"identity-credentials",
|
||||
"ovsdb-cms",
|
||||
"nova-service",
|
||||
}
|
||||
|
||||
def __init__(self, framework: ops.framework.Framework) -> None:
|
||||
"""Run constructor."""
|
||||
@ -256,6 +265,16 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm):
|
||||
"ovsdb-cms" in self.mandatory_relations,
|
||||
)
|
||||
handlers.append(self.ovsdb_cms)
|
||||
if self.can_add_handler("nova-service", handlers):
|
||||
self.nova_controller = (
|
||||
sunbeam_rhandlers.NovaServiceRequiresHandler(
|
||||
self,
|
||||
"nova-service",
|
||||
self.handle_nova_controller_events,
|
||||
"nova-service" in self.mandatory_relations,
|
||||
)
|
||||
)
|
||||
handlers.append(self.nova_controller)
|
||||
if self.can_add_handler("ceilometer-service", handlers):
|
||||
self.ceilometer = (
|
||||
sunbeam_rhandlers.CeilometerServiceRequiresHandler(
|
||||
@ -439,39 +458,60 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm):
|
||||
"Data missing: {}".format(e.name)
|
||||
)
|
||||
# Handle optional config contexts
|
||||
try:
|
||||
if contexts.ceph_access.uuid:
|
||||
snap_data.update(
|
||||
{
|
||||
"compute.rbd-user": "nova",
|
||||
"compute.rbd-secret-uuid": contexts.ceph_access.uuid,
|
||||
"compute.rbd-key": contexts.ceph_access.key,
|
||||
}
|
||||
)
|
||||
except AttributeError:
|
||||
# If the relation has been removed it is probably less disruptive to leave the
|
||||
# rbd setting in the snap rather than unsetting them.
|
||||
logger.debug("ceph_access relation not integrated")
|
||||
try:
|
||||
if contexts.ceilometer_service.telemetry_secret:
|
||||
snap_data.update(
|
||||
{
|
||||
"telemetry.enable": self.enable_telemetry,
|
||||
"telemetry.publisher-secret": contexts.ceilometer_service.telemetry_secret,
|
||||
}
|
||||
)
|
||||
else:
|
||||
snap_data.update({"telemetry.enable": self.enable_telemetry})
|
||||
except AttributeError:
|
||||
logger.debug("ceilometer_service relation not integrated")
|
||||
snap_data.update({"telemetry.enable": self.enable_telemetry})
|
||||
|
||||
snap_data.update(self._handle_ceph_access(contexts))
|
||||
snap_data.update(self._handle_ceilometer_service(contexts))
|
||||
snap_data.update(self._handle_nova_service(contexts))
|
||||
snap_data.update(self._handle_receive_ca_cert(contexts))
|
||||
|
||||
self.set_snap_data(snap_data)
|
||||
self.ensure_services_running()
|
||||
self._state.unit_bootstrapped = True
|
||||
|
||||
def _handle_ceph_access(
|
||||
self, contexts: sunbeam_core.OPSCharmContexts
|
||||
) -> dict:
|
||||
try:
|
||||
if contexts.ceph_access.uuid:
|
||||
return {
|
||||
"compute.rbd-user": "nova",
|
||||
"compute.rbd-secret-uuid": contexts.ceph_access.uuid,
|
||||
"compute.rbd-key": contexts.ceph_access.key,
|
||||
}
|
||||
except AttributeError:
|
||||
# If the relation has been removed it is probably less disruptive to leave the
|
||||
# rbd setting in the snap rather than unsetting them.
|
||||
logger.debug("ceph_access relation not integrated")
|
||||
|
||||
return {}
|
||||
|
||||
def _handle_ceilometer_service(
|
||||
self, contexts: sunbeam_core.OPSCharmContexts
|
||||
) -> dict:
|
||||
try:
|
||||
if contexts.ceilometer_service.telemetry_secret:
|
||||
return {
|
||||
"telemetry.enable": self.enable_telemetry,
|
||||
"telemetry.publisher-secret": contexts.ceilometer_service.telemetry_secret,
|
||||
}
|
||||
else:
|
||||
return {"telemetry.enable": self.enable_telemetry}
|
||||
except AttributeError:
|
||||
logger.debug("ceilometer_service relation not integrated")
|
||||
return {"telemetry.enable": self.enable_telemetry}
|
||||
|
||||
def _handle_nova_service(
|
||||
self, contexts: sunbeam_core.OPSCharmContexts
|
||||
) -> dict:
|
||||
try:
|
||||
if contexts.nova_service.nova_spiceproxy_url:
|
||||
return {
|
||||
"compute.nova-spiceproxy-url": contexts.nova_service.nova_spiceproxy_url,
|
||||
}
|
||||
except AttributeError as e:
|
||||
logger.debug(f"Nova service relation not integrated: {str(e)}")
|
||||
|
||||
return {}
|
||||
|
||||
def _handle_receive_ca_cert(
|
||||
self, context: sunbeam_core.OPSCharmContexts
|
||||
) -> dict:
|
||||
@ -493,6 +533,15 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm):
|
||||
self.enable_telemetry = False
|
||||
self.configure_charm(event)
|
||||
|
||||
def handle_nova_controller_events(
|
||||
self, event: ops.framework.EventBase
|
||||
) -> None:
|
||||
"""Handle nova controller events."""
|
||||
if isinstance(event, NovaConfigChangedEvent) or isinstance(
|
||||
event, NovaServiceGoneAwayEvent
|
||||
):
|
||||
self.configure_charm(event)
|
||||
|
||||
def stop_services(self, relation: Optional[Set[str]]) -> None:
|
||||
"""Stop services based on relation goneaway event."""
|
||||
snap_data = {}
|
||||
|
@ -107,8 +107,18 @@ class TestCharm(test_utils.CharmTestCase):
|
||||
self.socket.getfqdn.return_value = "test.local"
|
||||
self.initial_setup()
|
||||
self.harness.set_leader()
|
||||
|
||||
test_utils.add_complete_amqp_relation(self.harness)
|
||||
test_utils.add_complete_identity_credentials_relation(self.harness)
|
||||
# Add nova-service relation
|
||||
self.harness.add_relation(
|
||||
"nova-service",
|
||||
"nova",
|
||||
app_data={
|
||||
"nova-spiceproxy-url": "http://INGRESS_IP/nova-spiceproxy"
|
||||
},
|
||||
)
|
||||
|
||||
hypervisor_snap_mock.ensure.assert_any_call(
|
||||
"latest", channel="essex/stable"
|
||||
)
|
||||
@ -137,6 +147,7 @@ class TestCharm(test_utils.CharmTestCase):
|
||||
"compute.rbd-user": "nova",
|
||||
"compute.rbd-secret-uuid": "ddd",
|
||||
"compute.rbd-key": "eee",
|
||||
"compute.nova-spiceproxy-url": "http://INGRESS_IP/nova-spiceproxy",
|
||||
"credentials.ovn-metadata-proxy-shared-secret": metadata,
|
||||
"identity.admin-role": None,
|
||||
"identity.auth-url": "http://10.153.2.45:80/openstack-keystone",
|
||||
@ -190,6 +201,15 @@ class TestCharm(test_utils.CharmTestCase):
|
||||
app_data={"telemetry-secret": "FAKE_SECRET"},
|
||||
)
|
||||
|
||||
# Add nova-service relation
|
||||
self.harness.add_relation(
|
||||
"nova-service",
|
||||
"nova",
|
||||
app_data={
|
||||
"nova-spiceproxy-url": "http://INGRESS_IP/nova-spiceproxy"
|
||||
},
|
||||
)
|
||||
|
||||
self.get_local_ip_by_default_route.return_value = "10.0.0.10"
|
||||
hypervisor_snap_mock = MagicMock()
|
||||
hypervisor_snap_mock.present = False
|
||||
@ -230,6 +250,7 @@ class TestCharm(test_utils.CharmTestCase):
|
||||
"compute.rbd-user": "nova",
|
||||
"compute.rbd-secret-uuid": "ddd",
|
||||
"compute.rbd-key": "eee",
|
||||
"compute.nova-spiceproxy-url": "http://INGRESS_IP/nova-spiceproxy",
|
||||
"credentials.ovn-metadata-proxy-shared-secret": metadata,
|
||||
"identity.admin-role": None,
|
||||
"identity.auth-url": "http://10.153.2.45:80/openstack-keystone",
|
||||
|
@ -58,6 +58,7 @@ INTERNAL_NEUTRON_LIBS=(
|
||||
|
||||
INTERNAL_NOVA_LIBS=(
|
||||
"keystone_k8s"
|
||||
"nova_k8s"
|
||||
"sunbeam_nova_compute_operator"
|
||||
)
|
||||
|
||||
@ -66,6 +67,7 @@ INTERNAL_OPENSTACK_HYPERVISOR_LIBS=(
|
||||
"ovn_central_k8s"
|
||||
"cinder_ceph_k8s"
|
||||
"ceilometer_k8s"
|
||||
"nova_k8s"
|
||||
)
|
||||
|
||||
INTERNAL_OVN_CENTRAL_LIBS=(
|
||||
@ -343,7 +345,7 @@ declare -A EXTERNAL_LIBS=(
|
||||
[keystone-ldap-k8s]=${NULL_ARRAY[@]}
|
||||
[magnum-k8s]=${EXTERNAL_AODH_LIBS[@]}
|
||||
[neutron-k8s]=${EXTERNAL_NEUTRON_LIBS[@]}
|
||||
[nova-k8s]=${EXTERNAL_AODH_LIBS[@]}
|
||||
[nova-k8s]=${EXTERNAL_HEAT_LIBS[@]}
|
||||
[octavia-k8s]=${EXTERNAL_OCTAVIA_LIBS[@]}
|
||||
[openstack-exporter-k8s]=${EXTERNAL_OPENSTACK_EXPORTER_LIBS[@]}
|
||||
[openstack-hypervisor]=${EXTERNAL_OPENSTACK_HYPERVISOR_LIBS[@]}
|
||||
|
210
libs/internal/lib/charms/nova_k8s/v0/nova_service.py
Normal file
210
libs/internal/lib/charms/nova_k8s/v0/nova_service.py
Normal file
@ -0,0 +1,210 @@
|
||||
"""NovaServiceProvides and Requires module.
|
||||
|
||||
This library contains the Requires and Provides classes for handling
|
||||
the nova_service interface.
|
||||
|
||||
Import `NovaServiceRequires` in your charm, with the charm object and the
|
||||
relation name:
|
||||
- self
|
||||
- "nova_service"
|
||||
|
||||
Two events are also available to respond to:
|
||||
- config_changed
|
||||
- goneaway
|
||||
|
||||
A basic example showing the usage of this relation follows:
|
||||
|
||||
```
|
||||
from charms.nova_k8s.v0.nova_service import (
|
||||
NovaServiceRequires
|
||||
)
|
||||
|
||||
class NovaServiceClientCharm(CharmBase):
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
# NovaService Requires
|
||||
self.nova_service = NovaServiceRequires(
|
||||
self, "nova_service",
|
||||
)
|
||||
self.framework.observe(
|
||||
self.nova_service.on.config_changed,
|
||||
self._on_nova_service_config_changed
|
||||
)
|
||||
self.framework.observe(
|
||||
self.nova_service.on.goneaway,
|
||||
self._on_nova_service_goneaway
|
||||
)
|
||||
|
||||
def _on_nova_service_config_changed(self, event):
|
||||
'''React to the Nova service config changed event.
|
||||
|
||||
This event happens when NovaService relation is added to the
|
||||
model and relation data is changed.
|
||||
'''
|
||||
# Do something with the configuration provided by relation.
|
||||
pass
|
||||
|
||||
def _on_nova_service_goneaway(self, event):
|
||||
'''React to the NovaService goneaway event.
|
||||
|
||||
This event happens when NovaService relation is removed.
|
||||
'''
|
||||
# NovaService Relation has goneaway.
|
||||
pass
|
||||
```
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from ops.charm import (
|
||||
CharmBase,
|
||||
RelationBrokenEvent,
|
||||
RelationChangedEvent,
|
||||
RelationEvent,
|
||||
)
|
||||
from ops.framework import (
|
||||
EventSource,
|
||||
Object,
|
||||
ObjectEvents,
|
||||
)
|
||||
from ops.model import (
|
||||
Relation,
|
||||
)
|
||||
|
||||
# The unique Charmhub library identifier, never change it
|
||||
LIBID = "050da1b56a094b52a08bb9b9ab7504f1"
|
||||
|
||||
# Increment this major API version when introducing breaking changes
|
||||
LIBAPI = 0
|
||||
|
||||
# Increment this PATCH version before using `charmcraft publish-lib` or reset
|
||||
# to 0 if you are raising the major API version
|
||||
LIBPATCH = 1
|
||||
|
||||
|
||||
class NovaConfigRequestEvent(RelationEvent):
|
||||
"""NovaConfigRequest Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NovaServiceProviderEvents(ObjectEvents):
|
||||
"""Events class for `on`."""
|
||||
|
||||
config_request = EventSource(NovaConfigRequestEvent)
|
||||
|
||||
|
||||
class NovaServiceProvides(Object):
|
||||
"""NovaServiceProvides class."""
|
||||
|
||||
on = NovaServiceProviderEvents()
|
||||
|
||||
def __init__(self, charm: CharmBase, relation_name: str):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_nova_service_relation_changed,
|
||||
)
|
||||
|
||||
def _on_nova_service_relation_changed(self, event: RelationChangedEvent):
|
||||
"""Handle NovaService relation changed."""
|
||||
logging.debug("NovaService relation changed")
|
||||
self.on.config_request.emit(event.relation)
|
||||
|
||||
def set_config(
|
||||
self, relation: Relation | None, nova_spiceproxy_url: str
|
||||
) -> None:
|
||||
"""Set nova configuration on the relation."""
|
||||
if not self.charm.unit.is_leader():
|
||||
logging.debug("Not a leader unit, skipping set config")
|
||||
return
|
||||
|
||||
# If relation is not provided send config to all the related
|
||||
# applications. This happens usually when config data is
|
||||
# updated by provider and wants to send the data to all
|
||||
# related applications
|
||||
if relation is None:
|
||||
logging.debug(
|
||||
"Sending config to all related applications of relation"
|
||||
f"{self.relation_name}"
|
||||
)
|
||||
for relation in self.framework.model.relations[self.relation_name]:
|
||||
relation.data[self.charm.app][
|
||||
"nova-spiceproxy-url"
|
||||
] = nova_spiceproxy_url
|
||||
else:
|
||||
logging.debug(
|
||||
f"Sending config on relation {relation.app.name} "
|
||||
f"{relation.name}/{relation.id}"
|
||||
)
|
||||
relation.data[self.charm.app][
|
||||
"nova-spiceproxy-url"
|
||||
] = nova_spiceproxy_url
|
||||
|
||||
|
||||
class NovaConfigChangedEvent(RelationEvent):
|
||||
"""NovaConfigChanged Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NovaServiceGoneAwayEvent(RelationEvent):
|
||||
"""NovaServiceGoneAway Event."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NovaServiceRequirerEvents(ObjectEvents):
|
||||
"""Events class for `on`."""
|
||||
|
||||
config_changed = EventSource(NovaConfigChangedEvent)
|
||||
goneaway = EventSource(NovaServiceGoneAwayEvent)
|
||||
|
||||
|
||||
class NovaServiceRequires(Object):
|
||||
"""NovaServiceRequires class."""
|
||||
|
||||
on = NovaServiceRequirerEvents()
|
||||
|
||||
def __init__(self, charm: CharmBase, relation_name: str):
|
||||
super().__init__(charm, relation_name)
|
||||
self.charm = charm
|
||||
self.relation_name = relation_name
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_changed,
|
||||
self._on_nova_service_relation_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
self.charm.on[relation_name].relation_broken,
|
||||
self._on_nova_service_relation_broken,
|
||||
)
|
||||
|
||||
def _on_nova_service_relation_changed(self, event: RelationChangedEvent):
|
||||
"""Handle NovaService relation changed."""
|
||||
logging.debug("NovaService config data changed")
|
||||
self.on.config_changed.emit(event.relation)
|
||||
|
||||
def _on_nova_service_relation_broken(self, event: RelationBrokenEvent):
|
||||
"""Handle NovaService relation changed."""
|
||||
logging.debug("NovaService on_broken")
|
||||
self.on.goneaway.emit(event.relation)
|
||||
|
||||
@property
|
||||
def _nova_service_rel(self) -> Relation | None:
|
||||
"""The nova service relation."""
|
||||
return self.framework.model.get_relation(self.relation_name)
|
||||
|
||||
def get_remote_app_data(self, key: str) -> str | None:
|
||||
"""Return the value for the given key from remote app data."""
|
||||
if self._nova_service_rel:
|
||||
data = self._nova_service_rel.data[self._nova_service_rel.app]
|
||||
return data.get(key)
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def nova_spiceproxy_url(self) -> str | None:
|
||||
"""Return the nova_spiceproxy url."""
|
||||
return self.get_remote_app_data("nova-spiceproxy-url")
|
@ -1865,3 +1865,144 @@ class CertificateTransferRequiresHandler(RelationHandler):
|
||||
ca_bundle.append(chain_)
|
||||
|
||||
return {"ca_bundle": "\n".join(ca_bundle)}
|
||||
|
||||
|
||||
class TraefikRouteHandler(RelationHandler):
|
||||
"""Base class to handle traefik route relations."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
charm: ops.charm.CharmBase,
|
||||
relation_name: str,
|
||||
callback_f: Callable,
|
||||
mandatory: bool = False,
|
||||
ingress_names: list | None = None,
|
||||
) -> None:
|
||||
"""Run constructor."""
|
||||
super().__init__(charm, relation_name, callback_f, mandatory)
|
||||
self.ingress_names = ingress_names or []
|
||||
|
||||
def setup_event_handler(self) -> ops.framework.Object:
|
||||
"""Configure event handlers for an Ingress relation."""
|
||||
logger.debug("Setting up ingress event handler")
|
||||
from charms.traefik_route_k8s.v0.traefik_route import (
|
||||
TraefikRouteRequirer,
|
||||
)
|
||||
|
||||
interface = TraefikRouteRequirer(
|
||||
self.charm,
|
||||
self.model.get_relation(self.relation_name),
|
||||
self.relation_name,
|
||||
)
|
||||
|
||||
self.framework.observe(interface.on.ready, self._on_ingress_ready)
|
||||
self.framework.observe(
|
||||
self.charm.on[self.relation_name].relation_joined,
|
||||
self._on_traefik_relation_joined,
|
||||
)
|
||||
return interface
|
||||
|
||||
def _on_traefik_relation_joined(
|
||||
self, event: ops.charm.RelationEvent
|
||||
) -> None:
|
||||
"""Handle traefik relation joined event."""
|
||||
# This is passed as None during the init method, so update the
|
||||
# relation attribute in TraefikRouteRequirer
|
||||
self.interface._relation = event.relation
|
||||
|
||||
def _on_ingress_ready(self, event: ops.charm.RelationEvent) -> None:
|
||||
"""Handle ingress relation changed events.
|
||||
|
||||
`event` is an instance of
|
||||
`charms.traefik_k8s.v2.ingress.IngressPerAppReadyEvent`.
|
||||
"""
|
||||
if self.interface.is_ready():
|
||||
self.callback_f(event)
|
||||
|
||||
@property
|
||||
def ready(self) -> bool:
|
||||
"""Whether the handler is ready for use."""
|
||||
if self.charm.unit.is_leader():
|
||||
return bool(self.interface.external_host)
|
||||
else:
|
||||
return self.interface.is_ready()
|
||||
|
||||
def context(self) -> dict:
|
||||
"""Context containing ingress data.
|
||||
|
||||
Returns dictionary of ingress_key: value
|
||||
ingress_key will be <ingress name>_ingress_path (replace - with _ in name)
|
||||
value will be /<model name>-<ingress name>
|
||||
"""
|
||||
return {
|
||||
f"{name.replace('-', '_')}_ingress_path": f"/{self.charm.model.name}-{name}"
|
||||
for name in self.ingress_names
|
||||
}
|
||||
|
||||
|
||||
class NovaServiceRequiresHandler(RelationHandler):
|
||||
"""Handle nova service relation on the requires side."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
charm: ops.charm.CharmBase,
|
||||
relation_name: str,
|
||||
callback_f: Callable,
|
||||
mandatory: bool = False,
|
||||
):
|
||||
"""Create a new nova-service handler.
|
||||
|
||||
Create a new NovaServiceRequiresHandler that handles initial
|
||||
events from the relation and invokes the provided callbacks based on
|
||||
the event raised.
|
||||
|
||||
:param charm: the Charm class the handler is for
|
||||
:type charm: ops.charm.CharmBase
|
||||
:param relation_name: the relation the handler is bound to
|
||||
:type relation_name: str
|
||||
:param callback_f: the function to call when the nodes are connected
|
||||
:type callback_f: Callable
|
||||
:param mandatory: If the relation is mandatory to proceed with
|
||||
configuring charm
|
||||
:type mandatory: bool
|
||||
"""
|
||||
super().__init__(charm, relation_name, callback_f, mandatory)
|
||||
|
||||
def setup_event_handler(self) -> None:
|
||||
"""Configure event handlers for Nova service relation."""
|
||||
import charms.nova_k8s.v0.nova_service as nova_svc
|
||||
|
||||
logger.debug("Setting up Nova service event handler")
|
||||
svc = nova_svc.NovaServiceRequires(
|
||||
self.charm,
|
||||
self.relation_name,
|
||||
)
|
||||
self.framework.observe(
|
||||
svc.on.config_changed,
|
||||
self._on_config_changed,
|
||||
)
|
||||
self.framework.observe(
|
||||
svc.on.goneaway,
|
||||
self._on_goneaway,
|
||||
)
|
||||
return svc
|
||||
|
||||
def _on_config_changed(self, event: ops.framework.EventBase) -> None:
|
||||
"""Handle config_changed event."""
|
||||
logger.debug("Nova service provider config changed event received")
|
||||
self.callback_f(event)
|
||||
|
||||
def _on_goneaway(self, event: ops.framework.EventBase) -> None:
|
||||
"""Handle gone_away event."""
|
||||
logger.debug("Nova service relation is departed/broken")
|
||||
self.callback_f(event)
|
||||
if self.mandatory:
|
||||
self.status.set(BlockedStatus("integration missing"))
|
||||
|
||||
@property
|
||||
def ready(self) -> bool:
|
||||
"""Whether handler is ready for use."""
|
||||
try:
|
||||
return bool(self.interface.nova_spiceproxy_url)
|
||||
except (AttributeError, KeyError):
|
||||
return False
|
||||
|
@ -102,6 +102,7 @@ applications:
|
||||
nova-api-image: ghcr.io/canonical/nova-consolidated:2024.1
|
||||
nova-scheduler-image: ghcr.io/canonical/nova-consolidated:2024.1
|
||||
nova-conductor-image: ghcr.io/canonical/nova-consolidated:2024.1
|
||||
nova-spiceproxy-image: ghcr.io/canonical/nova-consolidated:2024.1
|
||||
placement:
|
||||
{% if placement_k8s is defined and placement_k8s is sameas true -%}
|
||||
charm: ../../../placement-k8s.charm
|
||||
@ -164,8 +165,8 @@ relations:
|
||||
- nova:amqp
|
||||
- - keystone:identity-service
|
||||
- nova:identity-service
|
||||
- - traefik:ingress
|
||||
- nova:ingress-public
|
||||
- - traefik:traefik-route
|
||||
- nova:traefik-route-public
|
||||
- - keystone:send-ca-cert
|
||||
- nova:receive-ca-cert
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user