Skip to content
This repository was archived by the owner on Aug 11, 2025. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ requires:
interface: fiveg_n4
logging:
interface: loki_push_api
certificates:
ui-certificates:
interface: tls-certificates
cfg-certificates:
interface: tls-certificates

provides:
Expand Down Expand Up @@ -85,5 +87,5 @@ config:
options:
log-level:
type: string
default: info
default: debug
description: Log level for the NMS. One of `debug`, `info`, `warn`, `error`, `fatal`, `panic`.
98 changes: 72 additions & 26 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@
BASE_CONFIG_PATH = "/nms/config"
CONFIG_FILE_NAME = "nmscfg.conf"
NMS_CONFIG_PATH = f"{BASE_CONFIG_PATH}/{CONFIG_FILE_NAME}"
CERTS_MOUNT_PATH = "/support/TLS"
BASE_CERT_PATH = "/support/TLS"
UI_CERTS_MOUNT_PATH = f"{BASE_CERT_PATH}/ui"
CFG_CERTS_MOUNT_PATH = f"{BASE_CERT_PATH}/cfg"
WORKLOAD_VERSION_FILE_NAME = "/etc/workload-version"
AUTH_DATABASE_RELATION_NAME = "auth_database"
COMMON_DATABASE_RELATION_NAME = "common_database"
Expand All @@ -54,7 +56,7 @@
AUTH_DATABASE_NAME = "authentication"
COMMON_DATABASE_NAME = "free5gc"
WEBUI_DATABASE_NAME = "webui"
GRPC_PORT = 9876
NMS_CONFIG_PORT = 5001
NMS_URL_PORT = 5000
NMS_LOGIN_SECRET_LABEL = "NMS_LOGIN"

Expand All @@ -76,14 +78,16 @@ def to_dict(self) -> dict[str, str]:
}


TLS_RELATION_NAME = "certificates"
TLS_UI_RELATION_NAME = "ui-certificates"
TLS_CFG_RELATION_NAME = "cfg-certificates"
MANDATORY_RELATIONS = [
COMMON_DATABASE_RELATION_NAME,
AUTH_DATABASE_RELATION_NAME,
WEBUI_DATABASE_RELATION_NAME,
TLS_RELATION_NAME,
TLS_UI_RELATION_NAME,
TLS_CFG_RELATION_NAME,
]
CA_CERTIFICATE_CHARM_PATH = f"/var/lib/juju/storage/certs/0/{CA_CERTIFICATE_NAME}"
CA_CERTIFICATE_CHARM_PATH = f"/var/lib/juju/storage/certs/0/ui/{CA_CERTIFICATE_NAME}"


def _get_pod_ip() -> Optional[str]:
Expand All @@ -110,12 +114,19 @@ def __init__(self, *args):
return
self._container_name = self._service_name = "nms"
self._container = self.unit.get_container(self._container_name)
self._tls = Tls(
self._ui_tls = Tls(
charm=self,
relation_name=TLS_RELATION_NAME,
relation_name=TLS_UI_RELATION_NAME,
container=self._container,
domain_name=socket.getfqdn(),
workload_storage_path=CERTS_MOUNT_PATH,
workload_storage_path=UI_CERTS_MOUNT_PATH,
)
self._cfg_tls = Tls(
charm=self,
relation_name=TLS_CFG_RELATION_NAME,
container=self._container,
domain_name=socket.getfqdn(),
workload_storage_path=CFG_CERTS_MOUNT_PATH,
)
self._common_database = DatabaseRequires(
self,
Expand All @@ -135,7 +146,7 @@ def __init__(self, *args):
database_name=WEBUI_DATABASE_NAME,
extra_user_roles="admin",
)
self.unit.set_ports(GRPC_PORT, NMS_URL_PORT)
self.unit.set_ports(NMS_CONFIG_PORT, NMS_URL_PORT)
self.ingress = IngressPerAppRequirer(
charm=self,
port=NMS_URL_PORT,
Expand All @@ -149,6 +160,7 @@ def __init__(self, *args):
self._fiveg_core_gnb_provider = FivegCoreGnbProvides(self, FIVEG_CORE_GNB_RELATION_NAME)
self._sdcore_config = SdcoreConfigProvides(self, SDCORE_CONFIG_RELATION_NAME)
self.framework.observe(self.on.update_status, self._configure_sdcore_nms)
self.framework.observe(self.on.certs_storage_attached, self._configure_sdcore_nms)
self.framework.observe(self.on.nms_pebble_ready, self._configure_sdcore_nms)
self.framework.observe(self.on.common_database_relation_joined, self._configure_sdcore_nms)
self.framework.observe(self.on.auth_database_relation_joined, self._configure_sdcore_nms)
Expand All @@ -170,12 +182,19 @@ def __init__(self, *args):
self.on[FIVEG_N4_RELATION_NAME].relation_broken,
self._configure_sdcore_nms,
)
self.framework.observe(self.on.certificates_relation_joined, self._configure_sdcore_nms)
self.framework.observe(self.on.ui_certificates_relation_joined, self._configure_sdcore_nms)
self.framework.observe(self.on.cfg_certificates_relation_joined, self._configure_sdcore_nms)
self.framework.observe(
self.on.certificates_relation_broken, self._on_certificates_relation_broken
self.on.ui_certificates_relation_broken, self._on_ui_certificates_relation_broken
)
self.framework.observe(
self._tls._certificates.on.certificate_available, self._configure_sdcore_nms
self.on.cfg_certificates_relation_broken, self._on_cfg_certificates_relation_broken
)
self.framework.observe(
self._ui_tls._certificates.on.certificate_available, self._configure_sdcore_nms
)
self.framework.observe(
self._cfg_tls._certificates.on.certificate_available, self._configure_sdcore_nms
)
# Handling config changed event to publish the new url if the unit reboots and gets new IP
self.framework.observe(self.on.config_changed, self._configure_sdcore_nms)
Expand All @@ -201,7 +220,12 @@ def _configure_sdcore_nms(self, event: EventBase) -> None: # noqa: C901
return
if not self._container.exists(path=BASE_CONFIG_PATH):
return
if not self._container.exists(path=CERTS_MOUNT_PATH):
if not self._container.exists(path=BASE_CERT_PATH):
return
self._setup_cert_directories()
if not self._container.exists(path=UI_CERTS_MOUNT_PATH):
return
if not self._container.exists(path=CFG_CERTS_MOUNT_PATH):
return
for relation in MANDATORY_RELATIONS:
if not self._relation_created(relation):
Expand All @@ -212,8 +236,11 @@ def _configure_sdcore_nms(self, event: EventBase) -> None: # noqa: C901
return
if not self._webui_database_resource_is_available():
return
if not self._tls.certificate_is_available():
logger.info("The TLS certificate is not available yet.")
if not self._ui_tls.certificate_is_available():
logger.info("The UI TLS certificate is not available yet.")
return
if not self._cfg_tls.certificate_is_available():
logger.info("The CFG TLS certificate is not available yet.")
return
self._configure_workload()
self._configure_charm_authorization()
Expand All @@ -222,6 +249,12 @@ def _configure_sdcore_nms(self, event: EventBase) -> None: # noqa: C901
self._sync_upfs()
self._sync_network_config(event)

def _setup_cert_directories(self):
"""Create UI and config TLS certificate directories in the NMS container."""
logger.info("Creating TLS certificate directories")
self._container.make_dir(path=UI_CERTS_MOUNT_PATH, make_parents=True)
self._container.make_dir(path=CFG_CERTS_MOUNT_PATH, make_parents=True)

def _sync_network_config(self, event: EventBase):
"""Synchronize network configuration between the Core and the RAN.

Expand Down Expand Up @@ -294,7 +327,7 @@ def _on_collect_unit_status(self, event: CollectStatusEvent): # noqa: C901
self.unit.set_workload_version(self._get_workload_version())

if not self._container.exists(path=BASE_CONFIG_PATH) or not self._container.exists(
path=CERTS_MOUNT_PATH
path=BASE_CERT_PATH) or not self._container.exists(path=CFG_CERTS_MOUNT_PATH) or not self._container.exists(path=UI_CERTS_MOUNT_PATH
):
event.add_status(WaitingStatus("Waiting for storage to be attached"))
logger.info("Waiting for storage to be attached")
Expand All @@ -303,9 +336,12 @@ def _on_collect_unit_status(self, event: CollectStatusEvent): # noqa: C901
event.add_status(WaitingStatus("Waiting for NMS config file to be stored"))
logger.info("Waiting for NMS config file to be stored")
return
if not self._tls.certificate_is_available():
event.add_status(WaitingStatus("Waiting for certificates to be available"))
logger.info("Waiting for certificates to be available")
if not self._ui_tls.certificate_is_available():
event.add_status(WaitingStatus("Waiting for UI certificates to be available"))
logger.info("Waiting for UI certificates to be available")
if not self._cfg_tls.certificate_is_available():
event.add_status(WaitingStatus("Waiting for CFG certificates to be available"))
logger.info("Waiting for CFG certificates to be available")
if not self._is_nms_service_running():
event.add_status(WaitingStatus("Waiting for NMS service to start"))
logger.info("Waiting for NMS service to start")
Expand Down Expand Up @@ -352,12 +388,19 @@ def _get_admin_account(self) -> LoginSecret | None:
logger.info("NMS_LOGIN secret not found.")
return None

def _on_certificates_relation_broken(self, event: EventBase) -> None:
def _on_ui_certificates_relation_broken(self, event: EventBase) -> None:
"""Delete TLS related artifacts."""
if not self._container.can_connect():
event.defer()
return
self._ui_tls.clean_up_certificates()

def _on_cfg_certificates_relation_broken(self, event: EventBase) -> None:
"""Delete TLS related artifacts."""
if not self._container.can_connect():
event.defer()
return
self._tls.clean_up_certificates()
self._cfg_tls.clean_up_certificates()

def _publish_sdcore_config_url(self) -> None:
if not self._relation_created(SDCORE_CONFIG_RELATION_NAME):
Expand All @@ -367,11 +410,12 @@ def _publish_sdcore_config_url(self) -> None:
self._sdcore_config.set_webui_url_in_all_relations(webui_url=self._nms_config_url)

def _configure_workload(self):
certificate_update_required = self._tls.check_and_update_certificate()
ui_certificate_update_required = self._ui_tls.check_and_update_certificate()
cfg_certificate_update_required = self._cfg_tls.check_and_update_certificate()
desired_config_file = self._generate_nms_config_file()
if (
not self._is_config_file_update_required(desired_config_file)
and not certificate_update_required
and not ui_certificate_update_required and not cfg_certificate_update_required
):
self._configure_pebble()
return
Expand Down Expand Up @@ -427,8 +471,10 @@ def _generate_nms_config_file(self) -> str:
auth_database_url=self._get_auth_database_url(),
webui_database_name=WEBUI_DATABASE_NAME,
webui_database_url=self._get_webui_database_url(),
tls_key_path=self._tls.private_key_workload_path,
tls_certificate_path=self._tls.certificate_workload_path,
webui_tls_key_path=self._ui_tls.private_key_workload_path,
webui_tls_certificate_path=self._ui_tls.certificate_workload_path,
nfconfig_tls_key_path=self._cfg_tls.private_key_workload_path,
nfconfig_tls_certificate_path=self._cfg_tls.certificate_workload_path,
log_level=self._get_log_level_config(),
)

Expand Down Expand Up @@ -633,7 +679,7 @@ def _relation_created(self, relation_name: str) -> bool:

@property
def _nms_config_url(self) -> str:
return f"{self.app.name}:{GRPC_PORT}"
return f"https://{self.app.name}:{NMS_CONFIG_PORT}"

@property
def _nms_endpoint(self) -> str:
Expand Down
9 changes: 6 additions & 3 deletions src/templates/nmscfg.conf.j2
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ configuration:
webuiDbUrl: {{ webui_database_url }}
spec-compliant-sdf: false
enableAuthentication: true
tls:
key: {{ tls_key_path }}
pem: {{ tls_certificate_path }}
webui-tls:
key: {{ webui_tls_key_path }}
pem: {{ webui_tls_certificate_path }}
nfconfig-tls:
key: {{ nfconfig_tls_key_path }}
pem: {{ nfconfig_tls_certificate_path }}
send_pebble_notifications: true
info:
description: WebUI initial local configuration
Expand Down
13 changes: 11 additions & 2 deletions src/tls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"""Module use to handle TLS certificates for the NMS."""

import logging
import os
from typing import Optional

from charms.tls_certificates_interface.v4.tls_certificates import (
Expand Down Expand Up @@ -150,8 +151,16 @@ def _ca_certificate_is_stored(self) -> bool:
return self._container.exists(path=self.ca_certificate_workload_path)

def _store_certificate(self, certificate: Certificate) -> None:
self._container.push(path=self.certificate_workload_path, source=str(certificate))
logger.info("Pushed certificate to workload")
dir_path = os.path.dirname(self.certificate_workload_path)
self._container.make_dir(
path=dir_path,
make_parents=True
)

self._container.push(
path=self.certificate_workload_path,
source=str(certificate)
)

def _store_private_key(self, private_key: PrivateKey) -> None:
self._container.push(path=self.private_key_workload_path, source=str(private_key))
Expand Down
Loading