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 secrets
|
||||||
import socket
|
import socket
|
||||||
from typing import (
|
from typing import (
|
||||||
Callable,
|
|
||||||
List,
|
List,
|
||||||
Mapping,
|
Mapping,
|
||||||
Optional,
|
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.container_handlers as sunbeam_chandlers
|
||||||
import ops_sunbeam.core as sunbeam_core
|
import ops_sunbeam.core as sunbeam_core
|
||||||
import ops_sunbeam.relation_handlers as sunbeam_rhandlers
|
import ops_sunbeam.relation_handlers as sunbeam_rhandlers
|
||||||
from ops.charm import (
|
|
||||||
RelationEvent,
|
|
||||||
)
|
|
||||||
from ops.framework import (
|
from ops.framework import (
|
||||||
StoredState,
|
StoredState,
|
||||||
)
|
)
|
||||||
@ -60,63 +56,6 @@ HEAT_API_PORT = 8004
|
|||||||
HEAT_API_CFN_PORT = 8000
|
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):
|
class HeatAPIPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
|
||||||
"""Pebble handler for Heat API container."""
|
"""Pebble handler for Heat API container."""
|
||||||
|
|
||||||
@ -311,14 +250,14 @@ class HeatOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
|||||||
)
|
)
|
||||||
handlers.append(self.user_id_ops)
|
handlers.append(self.user_id_ops)
|
||||||
|
|
||||||
self.traefik_route_public = TraefikRouteHandler(
|
self.traefik_route_public = sunbeam_rhandlers.TraefikRouteHandler(
|
||||||
self,
|
self,
|
||||||
"traefik-route-public",
|
"traefik-route-public",
|
||||||
self.handle_traefik_ready,
|
self.handle_traefik_ready,
|
||||||
"traefik-route-public" in self.mandatory_relations,
|
"traefik-route-public" in self.mandatory_relations,
|
||||||
)
|
)
|
||||||
handlers.append(self.traefik_route_public)
|
handlers.append(self.traefik_route_public)
|
||||||
self.traefik_route_internal = TraefikRouteHandler(
|
self.traefik_route_internal = sunbeam_rhandlers.TraefikRouteHandler(
|
||||||
self,
|
self,
|
||||||
"traefik-route-internal",
|
"traefik-route-internal",
|
||||||
self.handle_traefik_ready,
|
self.handle_traefik_ready,
|
||||||
|
@ -32,6 +32,8 @@ containers:
|
|||||||
resource: nova-scheduler-image
|
resource: nova-scheduler-image
|
||||||
nova-conductor:
|
nova-conductor:
|
||||||
resource: nova-conductor-image
|
resource: nova-conductor-image
|
||||||
|
nova-spiceproxy:
|
||||||
|
resource: nova-spiceproxy-image
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
nova-api-image:
|
nova-api-image:
|
||||||
@ -46,14 +48,18 @@ resources:
|
|||||||
type: oci-image
|
type: oci-image
|
||||||
description: OCI image for OpenStack Nova Conductor
|
description: OCI image for OpenStack Nova Conductor
|
||||||
upstream-source: ghcr.io/canonical/nova-consolidated:2024.1
|
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:
|
requires:
|
||||||
ingress-internal:
|
traefik-route-internal:
|
||||||
interface: ingress
|
interface: traefik_route
|
||||||
optional: true
|
optional: true
|
||||||
limit: 1
|
limit: 1
|
||||||
ingress-public:
|
traefik-route-public:
|
||||||
interface: ingress
|
interface: traefik_route
|
||||||
limit: 1
|
limit: 1
|
||||||
database:
|
database:
|
||||||
interface: mysql_client
|
interface: mysql_client
|
||||||
@ -85,7 +91,7 @@ requires:
|
|||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
provides:
|
provides:
|
||||||
cloud-controller:
|
nova-service:
|
||||||
interface: nova
|
interface: nova
|
||||||
|
|
||||||
peers:
|
peers:
|
||||||
|
@ -19,6 +19,7 @@ This charm provide Nova services as part of an OpenStack deployment
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import socket
|
||||||
import uuid
|
import uuid
|
||||||
from typing import (
|
from typing import (
|
||||||
Callable,
|
Callable,
|
||||||
@ -33,6 +34,13 @@ import ops_sunbeam.config_contexts as sunbeam_ctxts
|
|||||||
import ops_sunbeam.container_handlers as sunbeam_chandlers
|
import ops_sunbeam.container_handlers as sunbeam_chandlers
|
||||||
import ops_sunbeam.core as sunbeam_core
|
import ops_sunbeam.core as sunbeam_core
|
||||||
import ops_sunbeam.relation_handlers as sunbeam_rhandlers
|
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 (
|
from ops.main import (
|
||||||
main,
|
main,
|
||||||
)
|
)
|
||||||
@ -45,6 +53,10 @@ logger = logging.getLogger(__name__)
|
|||||||
NOVA_WSGI_CONTAINER = "nova-api"
|
NOVA_WSGI_CONTAINER = "nova-api"
|
||||||
NOVA_SCHEDULER_CONTAINER = "nova-scheduler"
|
NOVA_SCHEDULER_CONTAINER = "nova-scheduler"
|
||||||
NOVA_CONDUCTOR_CONTAINER = "nova-conductor"
|
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):
|
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):
|
class CloudComputeRequiresHandler(sunbeam_rhandlers.RelationHandler):
|
||||||
"""Handles the cloud-compute relation on the requires side."""
|
"""Handles the cloud-compute relation on the requires side."""
|
||||||
|
|
||||||
@ -224,6 +304,40 @@ class CloudComputeRequiresHandler(sunbeam_rhandlers.RelationHandler):
|
|||||||
return True
|
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):
|
class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
||||||
"""Charm the service."""
|
"""Charm the service."""
|
||||||
|
|
||||||
@ -238,9 +352,31 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
|||||||
"cell-database",
|
"cell-database",
|
||||||
"amqp",
|
"amqp",
|
||||||
"identity-service",
|
"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
|
@property
|
||||||
def db_sync_cmds(self) -> List[List[str]]:
|
def db_sync_cmds(self) -> List[List[str]]:
|
||||||
"""DB sync commands for Nova operator."""
|
"""DB sync commands for Nova operator."""
|
||||||
@ -302,6 +438,50 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
|||||||
"""Default port for service ingress."""
|
"""Default port for service ingress."""
|
||||||
return 8774
|
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
|
@property
|
||||||
def databases(self) -> Mapping[str, str]:
|
def databases(self) -> Mapping[str, str]:
|
||||||
"""Databases needed to support this charm.
|
"""Databases needed to support this charm.
|
||||||
@ -345,6 +525,14 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
|||||||
self.template_dir,
|
self.template_dir,
|
||||||
self.configure_charm,
|
self.configure_charm,
|
||||||
),
|
),
|
||||||
|
NovaSpiceProxyPebbleHandler(
|
||||||
|
self,
|
||||||
|
NOVA_SPICEPROXY_CONTAINER,
|
||||||
|
"nova-spiceproxy",
|
||||||
|
[],
|
||||||
|
self.template_dir,
|
||||||
|
self.configure_charm,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
return pebble_handlers
|
return pebble_handlers
|
||||||
|
|
||||||
@ -361,6 +549,32 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
|||||||
self.register_compute_nodes,
|
self.register_compute_nodes,
|
||||||
)
|
)
|
||||||
handlers.append(self.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
|
return handlers
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -399,6 +613,59 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
|||||||
]
|
]
|
||||||
return _cconfigs
|
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):
|
def get_shared_metadatasecret(self):
|
||||||
"""Return the shared metadata secret."""
|
"""Return the shared metadata secret."""
|
||||||
return self.leader_get(self.shared_metadata_secret_key)
|
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")
|
logger.exception("Failed to discover hosts for cell1")
|
||||||
raise
|
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):
|
def get_cell_uuid(self, cell, fatal=True):
|
||||||
"""Returns the cell UUID from the name.
|
"""Returns the cell UUID from the name.
|
||||||
|
|
||||||
@ -506,6 +809,7 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
|||||||
"""Callback handler for nova operator configuration."""
|
"""Callback handler for nova operator configuration."""
|
||||||
if not self.peers.ready:
|
if not self.peers.ready:
|
||||||
return
|
return
|
||||||
|
|
||||||
metadata_secret = self.get_shared_metadatasecret()
|
metadata_secret = self.get_shared_metadatasecret()
|
||||||
if metadata_secret:
|
if metadata_secret:
|
||||||
logger.debug("Found metadata secret in leader DB")
|
logger.debug("Found metadata secret in leader DB")
|
||||||
@ -513,6 +817,8 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
|||||||
if self.unit.is_leader():
|
if self.unit.is_leader():
|
||||||
logger.debug("Creating metadata secret")
|
logger.debug("Creating metadata secret")
|
||||||
self.set_shared_metadatasecret()
|
self.set_shared_metadatasecret()
|
||||||
|
self.handle_traefik_ready(event)
|
||||||
|
self.set_config_on_update()
|
||||||
else:
|
else:
|
||||||
logger.debug("Metadata secret not ready")
|
logger.debug("Metadata secret not ready")
|
||||||
return
|
return
|
||||||
@ -522,6 +828,23 @@ class NovaOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
|
|||||||
NOVA_SCHEDULER_CONTAINER
|
NOVA_SCHEDULER_CONTAINER
|
||||||
)
|
)
|
||||||
scheduler_handler.enable_service_check = False
|
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)
|
super().configure_charm(event)
|
||||||
if scheduler_handler.pebble_ready:
|
if scheduler_handler.pebble_ready:
|
||||||
logging.debug("Starting nova scheduler service, 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"
|
"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__":
|
if __name__ == "__main__":
|
||||||
main(NovaOperatorCharm)
|
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 }} \
|
WSGIDaemonProcess nova-api processes=4 threads=1 user={{ wsgi_config.user }} group={{ wsgi_config.group }} \
|
||||||
display-name=%{GROUP}
|
display-name=%{GROUP}
|
||||||
WSGIProcessGroup nova-api
|
WSGIProcessGroup nova-api
|
||||||
{% if ingress_public and ingress_public.ingress_path -%}
|
{% if traefik_route_public and traefik_route_public.nova_ingress_path -%}
|
||||||
WSGIScriptAlias {{ ingress_public.ingress_path }} {{ wsgi_config.wsgi_public_script }}
|
WSGIScriptAlias {{ traefik_route_public.nova_ingress_path }} {{ wsgi_config.wsgi_public_script }}
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
WSGIScriptAlias / {{ wsgi_config.wsgi_public_script }}
|
WSGIScriptAlias / {{ wsgi_config.wsgi_public_script }}
|
||||||
WSGIApplicationGroup %{GLOBAL}
|
WSGIApplicationGroup %{GLOBAL}
|
||||||
|
@ -18,6 +18,9 @@
|
|||||||
|
|
||||||
import charm
|
import charm
|
||||||
import ops_sunbeam.test_utils as test_utils
|
import ops_sunbeam.test_utils as test_utils
|
||||||
|
from ops.testing import (
|
||||||
|
Harness,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class _NovaTestOperatorCharm(charm.NovaOperatorCharm):
|
class _NovaTestOperatorCharm(charm.NovaOperatorCharm):
|
||||||
@ -77,11 +80,34 @@ class TestNovaOperatorCharm(test_utils.CharmTestCase):
|
|||||||
self.addCleanup(self.harness.cleanup)
|
self.addCleanup(self.harness.cleanup)
|
||||||
self.harness.begin()
|
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):
|
def test_pebble_ready_handler(self):
|
||||||
"""Test pebble ready handler."""
|
"""Test pebble ready handler."""
|
||||||
self.assertEqual(self.harness.charm.seen_events, [])
|
self.assertEqual(self.harness.charm.seen_events, [])
|
||||||
test_utils.set_all_pebbles_ready(self.harness)
|
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):
|
def test_all_relations(self):
|
||||||
"""Test all integrations for operator."""
|
"""Test all integrations for operator."""
|
||||||
@ -89,12 +115,12 @@ class TestNovaOperatorCharm(test_utils.CharmTestCase):
|
|||||||
test_utils.set_all_pebbles_ready(self.harness)
|
test_utils.set_all_pebbles_ready(self.harness)
|
||||||
# this adds all the default/common relations
|
# this adds all the default/common relations
|
||||||
test_utils.add_all_relations(self.harness)
|
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
|
# 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)
|
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)
|
test_utils.add_db_relation_credentials(self.harness, rel_id)
|
||||||
|
|
||||||
setup_cmds = [
|
setup_cmds = [
|
||||||
@ -122,14 +148,3 @@ class TestNovaOperatorCharm(test_utils.CharmTestCase):
|
|||||||
]
|
]
|
||||||
for f in config_files:
|
for f in config_files:
|
||||||
self.check_file("nova-api", f)
|
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:
|
receive-ca-cert:
|
||||||
interface: certificate_transfer
|
interface: certificate_transfer
|
||||||
optional: true
|
optional: true
|
||||||
|
nova-service:
|
||||||
|
interface: nova
|
||||||
|
|
||||||
provides:
|
provides:
|
||||||
cos-agent:
|
cos-agent:
|
||||||
|
@ -46,6 +46,10 @@ from charms.ceilometer_k8s.v0.ceilometer_service import (
|
|||||||
from charms.grafana_agent.v0.cos_agent import (
|
from charms.grafana_agent.v0.cos_agent import (
|
||||||
COSAgentProvider,
|
COSAgentProvider,
|
||||||
)
|
)
|
||||||
|
from charms.nova_k8s.v0.nova_service import (
|
||||||
|
NovaConfigChangedEvent,
|
||||||
|
NovaServiceGoneAwayEvent,
|
||||||
|
)
|
||||||
from cryptography import (
|
from cryptography import (
|
||||||
x509,
|
x509,
|
||||||
)
|
)
|
||||||
@ -183,7 +187,12 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm):
|
|||||||
METADATA_SECRET_KEY = "ovn-metadata-proxy-shared-secret"
|
METADATA_SECRET_KEY = "ovn-metadata-proxy-shared-secret"
|
||||||
DEFAULT_SECRET_LENGTH = 32
|
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:
|
def __init__(self, framework: ops.framework.Framework) -> None:
|
||||||
"""Run constructor."""
|
"""Run constructor."""
|
||||||
@ -256,6 +265,16 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm):
|
|||||||
"ovsdb-cms" in self.mandatory_relations,
|
"ovsdb-cms" in self.mandatory_relations,
|
||||||
)
|
)
|
||||||
handlers.append(self.ovsdb_cms)
|
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):
|
if self.can_add_handler("ceilometer-service", handlers):
|
||||||
self.ceilometer = (
|
self.ceilometer = (
|
||||||
sunbeam_rhandlers.CeilometerServiceRequiresHandler(
|
sunbeam_rhandlers.CeilometerServiceRequiresHandler(
|
||||||
@ -439,39 +458,60 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm):
|
|||||||
"Data missing: {}".format(e.name)
|
"Data missing: {}".format(e.name)
|
||||||
)
|
)
|
||||||
# Handle optional config contexts
|
# Handle optional config contexts
|
||||||
try:
|
snap_data.update(self._handle_ceph_access(contexts))
|
||||||
if contexts.ceph_access.uuid:
|
snap_data.update(self._handle_ceilometer_service(contexts))
|
||||||
snap_data.update(
|
snap_data.update(self._handle_nova_service(contexts))
|
||||||
{
|
|
||||||
"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_receive_ca_cert(contexts))
|
snap_data.update(self._handle_receive_ca_cert(contexts))
|
||||||
|
|
||||||
self.set_snap_data(snap_data)
|
self.set_snap_data(snap_data)
|
||||||
self.ensure_services_running()
|
self.ensure_services_running()
|
||||||
self._state.unit_bootstrapped = True
|
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(
|
def _handle_receive_ca_cert(
|
||||||
self, context: sunbeam_core.OPSCharmContexts
|
self, context: sunbeam_core.OPSCharmContexts
|
||||||
) -> dict:
|
) -> dict:
|
||||||
@ -493,6 +533,15 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm):
|
|||||||
self.enable_telemetry = False
|
self.enable_telemetry = False
|
||||||
self.configure_charm(event)
|
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:
|
def stop_services(self, relation: Optional[Set[str]]) -> None:
|
||||||
"""Stop services based on relation goneaway event."""
|
"""Stop services based on relation goneaway event."""
|
||||||
snap_data = {}
|
snap_data = {}
|
||||||
|
@ -107,8 +107,18 @@ class TestCharm(test_utils.CharmTestCase):
|
|||||||
self.socket.getfqdn.return_value = "test.local"
|
self.socket.getfqdn.return_value = "test.local"
|
||||||
self.initial_setup()
|
self.initial_setup()
|
||||||
self.harness.set_leader()
|
self.harness.set_leader()
|
||||||
|
|
||||||
test_utils.add_complete_amqp_relation(self.harness)
|
test_utils.add_complete_amqp_relation(self.harness)
|
||||||
test_utils.add_complete_identity_credentials_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(
|
hypervisor_snap_mock.ensure.assert_any_call(
|
||||||
"latest", channel="essex/stable"
|
"latest", channel="essex/stable"
|
||||||
)
|
)
|
||||||
@ -137,6 +147,7 @@ class TestCharm(test_utils.CharmTestCase):
|
|||||||
"compute.rbd-user": "nova",
|
"compute.rbd-user": "nova",
|
||||||
"compute.rbd-secret-uuid": "ddd",
|
"compute.rbd-secret-uuid": "ddd",
|
||||||
"compute.rbd-key": "eee",
|
"compute.rbd-key": "eee",
|
||||||
|
"compute.nova-spiceproxy-url": "http://INGRESS_IP/nova-spiceproxy",
|
||||||
"credentials.ovn-metadata-proxy-shared-secret": metadata,
|
"credentials.ovn-metadata-proxy-shared-secret": metadata,
|
||||||
"identity.admin-role": None,
|
"identity.admin-role": None,
|
||||||
"identity.auth-url": "http://10.153.2.45:80/openstack-keystone",
|
"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"},
|
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"
|
self.get_local_ip_by_default_route.return_value = "10.0.0.10"
|
||||||
hypervisor_snap_mock = MagicMock()
|
hypervisor_snap_mock = MagicMock()
|
||||||
hypervisor_snap_mock.present = False
|
hypervisor_snap_mock.present = False
|
||||||
@ -230,6 +250,7 @@ class TestCharm(test_utils.CharmTestCase):
|
|||||||
"compute.rbd-user": "nova",
|
"compute.rbd-user": "nova",
|
||||||
"compute.rbd-secret-uuid": "ddd",
|
"compute.rbd-secret-uuid": "ddd",
|
||||||
"compute.rbd-key": "eee",
|
"compute.rbd-key": "eee",
|
||||||
|
"compute.nova-spiceproxy-url": "http://INGRESS_IP/nova-spiceproxy",
|
||||||
"credentials.ovn-metadata-proxy-shared-secret": metadata,
|
"credentials.ovn-metadata-proxy-shared-secret": metadata,
|
||||||
"identity.admin-role": None,
|
"identity.admin-role": None,
|
||||||
"identity.auth-url": "http://10.153.2.45:80/openstack-keystone",
|
"identity.auth-url": "http://10.153.2.45:80/openstack-keystone",
|
||||||
|
@ -58,6 +58,7 @@ INTERNAL_NEUTRON_LIBS=(
|
|||||||
|
|
||||||
INTERNAL_NOVA_LIBS=(
|
INTERNAL_NOVA_LIBS=(
|
||||||
"keystone_k8s"
|
"keystone_k8s"
|
||||||
|
"nova_k8s"
|
||||||
"sunbeam_nova_compute_operator"
|
"sunbeam_nova_compute_operator"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -66,6 +67,7 @@ INTERNAL_OPENSTACK_HYPERVISOR_LIBS=(
|
|||||||
"ovn_central_k8s"
|
"ovn_central_k8s"
|
||||||
"cinder_ceph_k8s"
|
"cinder_ceph_k8s"
|
||||||
"ceilometer_k8s"
|
"ceilometer_k8s"
|
||||||
|
"nova_k8s"
|
||||||
)
|
)
|
||||||
|
|
||||||
INTERNAL_OVN_CENTRAL_LIBS=(
|
INTERNAL_OVN_CENTRAL_LIBS=(
|
||||||
@ -343,7 +345,7 @@ declare -A EXTERNAL_LIBS=(
|
|||||||
[keystone-ldap-k8s]=${NULL_ARRAY[@]}
|
[keystone-ldap-k8s]=${NULL_ARRAY[@]}
|
||||||
[magnum-k8s]=${EXTERNAL_AODH_LIBS[@]}
|
[magnum-k8s]=${EXTERNAL_AODH_LIBS[@]}
|
||||||
[neutron-k8s]=${EXTERNAL_NEUTRON_LIBS[@]}
|
[neutron-k8s]=${EXTERNAL_NEUTRON_LIBS[@]}
|
||||||
[nova-k8s]=${EXTERNAL_AODH_LIBS[@]}
|
[nova-k8s]=${EXTERNAL_HEAT_LIBS[@]}
|
||||||
[octavia-k8s]=${EXTERNAL_OCTAVIA_LIBS[@]}
|
[octavia-k8s]=${EXTERNAL_OCTAVIA_LIBS[@]}
|
||||||
[openstack-exporter-k8s]=${EXTERNAL_OPENSTACK_EXPORTER_LIBS[@]}
|
[openstack-exporter-k8s]=${EXTERNAL_OPENSTACK_EXPORTER_LIBS[@]}
|
||||||
[openstack-hypervisor]=${EXTERNAL_OPENSTACK_HYPERVISOR_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_)
|
ca_bundle.append(chain_)
|
||||||
|
|
||||||
return {"ca_bundle": "\n".join(ca_bundle)}
|
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-api-image: ghcr.io/canonical/nova-consolidated:2024.1
|
||||||
nova-scheduler-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-conductor-image: ghcr.io/canonical/nova-consolidated:2024.1
|
||||||
|
nova-spiceproxy-image: ghcr.io/canonical/nova-consolidated:2024.1
|
||||||
placement:
|
placement:
|
||||||
{% if placement_k8s is defined and placement_k8s is sameas true -%}
|
{% if placement_k8s is defined and placement_k8s is sameas true -%}
|
||||||
charm: ../../../placement-k8s.charm
|
charm: ../../../placement-k8s.charm
|
||||||
@ -164,8 +165,8 @@ relations:
|
|||||||
- nova:amqp
|
- nova:amqp
|
||||||
- - keystone:identity-service
|
- - keystone:identity-service
|
||||||
- nova:identity-service
|
- nova:identity-service
|
||||||
- - traefik:ingress
|
- - traefik:traefik-route
|
||||||
- nova:ingress-public
|
- nova:traefik-route-public
|
||||||
- - keystone:send-ca-cert
|
- - keystone:send-ca-cert
|
||||||
- nova:receive-ca-cert
|
- nova:receive-ca-cert
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user