Files
sunbeam-charms/charms/barbican-k8s/src/charm.py
T
Hemanth Nakkina e07819a9d9 Derive mandatory relations from charmcraft.yaml
Currently each charm defines in the code all the
mandatory relations. Instead populate the mandatory
relations from chamrcraft.yaml requires relations
with optional as False.
Charm can define mandatory relations that are not
derived from requires relations and they will be
appended to the list of mandatory_relations.
Barbican and ovn-central charms follows this pattern.

Change-Id: Iff45fca33dc954593ded52b97e905431b6a7bb53
2025-02-03 15:23:59 +05:30

503 lines
16 KiB
Python
Executable File

#!/usr/bin/env python3
# Copyright 2023 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Barbican Operator Charm.
This charm provide Barbican services as part of an OpenStack deployment
"""
import hashlib
import json
import logging
import secrets
from typing import (
List,
Optional,
)
import charms.keystone_k8s.v0.identity_resource as identity_resource
import ops
import ops.framework
import ops_sunbeam.charm as sunbeam_charm
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
import ops_sunbeam.tracing as sunbeam_tracing
from charms.vault_k8s.v0 import (
vault_kv,
)
from ops import (
framework,
model,
pebble,
)
logger = logging.getLogger(__name__)
BARBICAN_API_CONTAINER = "barbican-api"
BARBICAN_WORKER_CONTAINER = "barbican-worker"
VAULT_KV_RELATION = "vault-kv"
NONCE_SECRET_LABEL = "nonce"
class NoRelationError(Exception):
"""No relation found."""
pass
@sunbeam_tracing.trace_type
class WSGIBarbicanAdminConfigContext(sunbeam_ctxts.ConfigContext):
"""Configuration context for WSGI configuration."""
def context(self) -> dict:
"""WSGI configuration options."""
return {
"name": self.charm.service_name,
"public_port": 9312,
"user": self.charm.service_user,
"group": self.charm.service_group,
"wsgi_admin_script": "/usr/bin/barbican-wsgi-api",
"wsgi_public_script": "/usr/bin/barbican-wsgi-api",
"error_log": "/dev/stdout",
"custom_log": "/dev/stdout",
}
@sunbeam_tracing.trace_type
class VaultKvRequiresHandler(sunbeam_rhandlers.RelationHandler):
"""Handler for vault-kv relation."""
charm: "BarbicanVaultOperatorCharm"
interface: vault_kv.VaultKvRequires
def __init__(
self,
charm: ops.CharmBase,
relation_name: str,
callback_f,
mount_suffix: str,
mandatory: bool = False,
):
self.mount_suffix = mount_suffix
super().__init__(charm, relation_name, callback_f, mandatory)
def setup_event_handler(self) -> ops.Object:
"""Configure event handlers for a vault-kv relation."""
logger.debug("Setting up vault-kv event handler")
interface = sunbeam_tracing.trace_type(vault_kv.VaultKvRequires)(
self.charm,
self.relation_name,
self.mount_suffix,
)
self.framework.observe(interface.on.connected, self._on_connected)
self.framework.observe(interface.on.ready, self._on_ready)
self.framework.observe(interface.on.gone_away, self._on_gone_away)
try:
self.request_credentials(interface, self._relation)
except NoRelationError:
pass
return interface
@property
def _relation(self) -> ops.Relation:
relation = self.model.get_relation(VAULT_KV_RELATION)
if relation is None:
raise NoRelationError("Vault-kv relation not found")
return relation
def _on_connected(self, event: vault_kv.VaultKvConnectedEvent):
"""Handle on connected event."""
relation = self.model.get_relation(
event.relation_name, event.relation_id
)
if relation is None:
raise RuntimeError(
"Vault-kv relation not found during a connected event"
)
self.request_credentials(self.interface, relation)
def _on_ready(self, event: vault_kv.VaultKvReadyEvent):
"""Handle client ready relation."""
self.callback_f(event)
def _on_gone_away(self, event: vault_kv.VaultKvGoneAwayEvent):
"""Handle client gone away relation."""
self.callback_f(event)
def request_credentials(
self, interface: vault_kv.VaultKvRequires, relation: ops.Relation
):
"""Request credentials from vault-kv relation."""
nonce = self.charm.get_nonce()
if nonce is None:
return
binding = self.model.get_binding(relation)
if binding is None:
logger.debug("No binding found for vault-kv relation")
return
egress_subnet = str(binding.network.interfaces[0].subnet)
interface.request_credentials(relation, egress_subnet, nonce)
@property
def ready(self) -> bool:
"""Whether the handler is ready for use."""
relation = self.model.get_relation(VAULT_KV_RELATION)
if relation is None:
return False
return all(
(
self.interface.get_unit_credentials(relation),
self.interface.get_vault_url(relation),
self.interface.get_mount(relation),
)
)
def context(self) -> dict:
"""Context containing relation data."""
vault_kv_relation = self._relation
unit_credentials = self.interface.get_unit_credentials(
vault_kv_relation
)
if not unit_credentials:
return {}
secret = self.model.get_secret(id=unit_credentials)
secret_content = secret.get_content(refresh=True)
return {
"kv_mountpoint": self.interface.get_mount(vault_kv_relation),
"vault_url": self.interface.get_vault_url(vault_kv_relation),
"approle_role_id": secret_content["role-id"],
"approle_secret_id": secret_content["role-secret-id"],
"ca_crt_file": self.charm.ca_crt_file,
"ca_certificate": self.interface.get_ca_certificate(
vault_kv_relation
),
}
@sunbeam_tracing.trace_type
class BarbicanWorkerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
"""Pebble handler for Barbican worker."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.enable_service_check = True
def get_layer(self) -> dict:
"""Barbican worker service layer.
:returns: pebble layer configuration for worker service
:rtype: dict
"""
return {
"summary": "barbican worker layer",
"description": "pebble configuration for barbican worker",
"services": {
"barbican-worker": {
"override": "replace",
"summary": "Barbican Worker",
"command": "barbican-worker",
"user": "barbican",
"group": "barbican",
}
},
}
def default_container_configs(
self,
) -> List[sunbeam_core.ContainerConfigFile]:
"""Container configurations for handler."""
return [
sunbeam_core.ContainerConfigFile(
"/etc/barbican/barbican.conf", "barbican", "barbican"
),
sunbeam_core.ContainerConfigFile(
"/usr/local/share/ca-certificates/ca-bundle.pem",
"root",
"barbican",
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 barbican worker")
return super().service_ready
else:
logging.debug("Service checks disabled for barbican worker")
return self.pebble_ready
class BarbicanOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
"""Charm the service."""
_state = framework.StoredState()
service_name = "barbican-api"
wsgi_admin_script = "/usr/bin/barbican-wsgi-api"
wsgi_public_script = "/usr/bin/barbican-wsgi-api"
db_sync_cmds = [
["sudo", "-u", "barbican", "barbican-manage", "db", "upgrade"]
]
def configure_unit(self, event: framework.EventBase) -> None:
"""Run configuration on this unit."""
self.disable_barbican_config()
super().configure_unit(event)
def get_relation_handlers(
self,
handlers: Optional[List[sunbeam_rhandlers.RelationHandler]] = None,
) -> List[sunbeam_rhandlers.RelationHandler]:
"""Relation handlers for the service."""
handlers = super().get_relation_handlers(handlers)
self.id_ops = sunbeam_rhandlers.IdentityResourceRequiresHandler(
self,
"identity-ops",
self.handle_keystone_ops,
mandatory="identity-ops" in self.mandatory_relations,
)
handlers.append(self.id_ops)
return handlers
def handle_keystone_ops(self, event: ops.EventBase) -> None:
"""Event handler for identity ops."""
if isinstance(event, identity_resource.IdentityOpsProviderReadyEvent):
self._state.identity_ops_ready = True
if not self.unit.is_leader():
return
# Send op request only by leader unit
ops = self._get_barbican_role_ops()
id_ = self.hash_ops(ops)
request = {
"id": id_,
"tag": "barbican_roles_setup",
"ops": ops,
}
logger.debug("Sending ops request: %r", request)
self.id_ops.interface.request_ops(request)
elif isinstance(
event,
identity_resource.IdentityOpsProviderGoneAwayEvent,
):
self._state.identity_ops_ready = False
elif isinstance(event, identity_resource.IdentityOpsResponseEvent):
if not self.unit.is_leader():
return
response = self.id_ops.interface.response
logger.debug("Got response from keystone: %r", response)
request_tag = response.get("tag")
if request_tag == "barbican_roles_setup":
self._handle_barbican_roles_setup(event)
def _handle_barbican_roles_setup(
self,
event: ops.EventBase,
) -> None:
"""Handle roles setup response from identity-ops."""
if {
op.get("return-code")
for op in self.id_ops.interface.response.get(
"ops",
[],
)
} != {0}:
logger.error("Failed to setup barbican roles")
def hash_ops(self, ops: list) -> str:
"""Return the sha1 of the requested ops."""
return hashlib.sha1(json.dumps(ops).encode()).hexdigest()
def _get_barbican_role_ops(self) -> list:
"""Generate ops request for domain setup."""
roles = ["key-manager:service-admin", "creator", "observer", "audit"]
ops = [
{"name": "create_role", "params": {"name": name}} for name in roles
]
return ops
@property
def config_contexts(self) -> List[sunbeam_ctxts.ConfigContext]:
"""Generate list of configuration adapters for the charm."""
_cadapters = super().config_contexts
_cadapters.extend(
[
WSGIBarbicanAdminConfigContext(
self,
"wsgi_barbican_admin",
)
]
)
return _cadapters
def disable_barbican_config(self):
"""Disable default barbican config."""
container = self.unit.get_container(BARBICAN_API_CONTAINER)
try:
process = container.exec(
["a2disconf", "barbican-api"], timeout=5 * 60
)
out, warnings = process.wait_output()
if warnings:
for line in warnings.splitlines():
logger.warning("a2disconf warn: %s", line.strip())
logging.debug(f"Output from a2disconf: \n{out}")
except pebble.ExecError:
logger.exception("Failed to disable barbican-api conf in apache")
self.status = model.ErrorStatus(
"Failed to disable barbican-api conf in apache"
)
def get_pebble_handlers(
self,
) -> List[sunbeam_chandlers.ServicePebbleHandler]:
"""Pebble handlers for operator."""
pebble_handlers = super().get_pebble_handlers()
pebble_handlers.extend(
[
BarbicanWorkerPebbleHandler(
self,
BARBICAN_WORKER_CONTAINER,
"barbican-worker",
[],
self.template_dir,
self.configure_charm,
),
]
)
return pebble_handlers
@property
def service_conf(self) -> str:
"""Service default configuration file."""
return "/etc/barbican/barbican.conf"
@property
def service_user(self) -> str:
"""Service user file and directory ownership."""
return "barbican"
@property
def service_group(self) -> str:
"""Service group file and directory ownership."""
return "barbican"
@property
def service_endpoints(self):
"""Service endpoints configuration."""
return [
{
"service_name": "barbican",
"type": "key-manager",
"description": "OpenStack Barbican API",
"internal_url": f"{self.internal_url}",
"public_url": f"{self.public_url}",
"admin_url": f"{self.admin_url}",
}
]
@property
def default_public_ingress_port(self):
"""Default port."""
return 9311
@property
def healthcheck_http_url(self) -> str:
"""Healthcheck HTTP URL for the service."""
# / returns a 300 return code, which is not understood by Pebble as OK
return super().healthcheck_http_url + "?build"
@sunbeam_tracing.trace_sunbeam_charm
class BarbicanVaultOperatorCharm(BarbicanOperatorCharm):
"""Vault specialized Barbican Operator Charm."""
mandatory_relations = {VAULT_KV_RELATION}
def __init__(self, *args):
super().__init__(*args)
self.framework.observe(self.on.install, self._on_install)
def _on_install(self, event: ops.framework.EventBase) -> None:
"""Handle install event."""
self.unit.add_secret(
{"nonce": secrets.token_hex(16)},
label=NONCE_SECRET_LABEL,
description="nonce for vault-kv relation",
)
def get_relation_handlers(
self,
handlers: Optional[List[sunbeam_rhandlers.RelationHandler]] = None,
) -> List[sunbeam_rhandlers.RelationHandler]:
"""Relation handlers for the service."""
handlers = super().get_relation_handlers(handlers)
if self.can_add_handler(VAULT_KV_RELATION, handlers):
self.vault_kv = VaultKvRequiresHandler(
self,
VAULT_KV_RELATION,
self.configure_charm,
self.mount_suffix,
VAULT_KV_RELATION in self.mandatory_relations,
)
handlers.append(self.vault_kv)
return handlers
@property
def container_configs(self) -> List[sunbeam_core.ContainerConfigFile]:
"""Container configuration files for the service."""
_cconfigs = super().container_configs
_cconfigs.extend(
[
sunbeam_core.ContainerConfigFile(
self.ca_crt_file, "barbican", "barbican"
),
sunbeam_core.ContainerConfigFile(
"/usr/local/share/ca-certificates/ca-bundle.pem",
"root",
"barbican",
0o640,
),
]
)
return _cconfigs
@property
def mount_suffix(self):
"""Secret backend for vault."""
return "secrets"
@property
def ca_crt_file(self):
"""Vault CA certificate file location."""
return "/etc/barbican/vault_ca.crt"
def get_nonce(self) -> Optional[str]:
"""Return nonce stored in secret."""
try:
secret = self.model.get_secret(label=NONCE_SECRET_LABEL)
return secret.get_content(refresh=True)["nonce"]
except ops.SecretNotFoundError:
return None
if __name__ == "__main__": # pragma: nocover
ops.main(BarbicanVaultOperatorCharm)