SD-Core NMS K8s
- Canonical Telco
Channel | Revision | Published | Runs on |
---|---|---|---|
latest/beta | 9 | 03 Jan 2024 | |
latest/edge | 14 | 21 Jan 2024 | |
1.5/stable | 811 | 20 Dec 2024 | |
1.5/candidate | 811 | 20 Dec 2024 | |
1.5/beta | 811 | 20 Dec 2024 | |
1.5/edge | 811 | 18 Dec 2024 | |
1.5/edge | 721 | 21 Nov 2024 | |
1.6/edge | 814 | Today | |
1.4/beta | 163 | 01 May 2024 | |
1.4/edge | 684 | 29 Oct 2024 | |
1.3/beta | 9 | 22 Jan 2024 | |
1.3/edge | 75 | 12 Apr 2024 | |
0.2/edge | 108 | 23 Apr 2024 |
juju deploy sdcore-nms-k8s --channel 1.5/stable
Deploy Kubernetes operators easily with Juju, the Universal Operator Lifecycle Manager. Need a Kubernetes cluster? Install MicroK8s to create a full CNCF-certified Kubernetes system in under 60 seconds.
Platform:
charms.sdcore_nms_k8s.v0.fiveg_core_gnb
-
- Last updated 09 Dec 2024
- Revision Library version 0.1
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.
"""Library for the `fiveg_core_gnb` relation.
This library contains the Requires and Provides classes for handling the `fiveg_core_gnb`
interface.
The purpose of this library is to provide a way for a 5G Core to provide network information and
configuration to CUs/gNodeBs.
To get started using the library, you need to fetch the library using `charmcraft`.
```shell
cd some-charm
charmcraft fetch-lib charms.sdcore_nms_k8s.v0.fiveg_core_gnb
```
Add the following libraries to the charm's `requirements.txt` file:
- pydantic
- pytest-interface-tester
Charms providing the `fiveg_core_gnb` relation should use `FivegCoreGnbProvides`.
The class `PLMNConfig` represents the configuration of a PLMN for the CU/gNodeB. It is composed by
the Mobile Country Code (MCC), the Mobile Network Code (MNC), the Slice Service Type (SST) and the
Slice Differentiator (SD). Each CU can be configured with a single Tracking Area Code (TAC) and
multiple PLMNs.
Typical usage of this class would look something like:
```python
...
from charms.sdcore_gnbsim_k8s.v0.fiveg_core_gnb import FivegCoreGnbProvides, PLMNConfig
...
class SomeProviderCharm(CharmBase):
def __init__(self, *args):
...
self.fiveg_core_gnb_provider = FivegCoreGnbProvides(
charm=self,
relation_name="fiveg_core_gnb"
)
...
self.framework.observe(
self.on.config_changed_event,
self._on_config_changed_event
)
def _on_config_changed_event(self, event):
...
# implement the logic to populate the list of PLMNs.
plmns = [PLMNConfig(mcc=..., mnc=..., sst=..., sd=...)
self.fiveg_core_gnb_provider.publish_gnb_config_information(
relation_id=event.relation_id,
tac=tac,
plmns=plmns,
)
```
And a corresponding section in charm's `charmcraft.yaml`:
```
provides:
fiveg_core_gnb: # Relation name
interface: fiveg_core_gnb # Relation interface
```
Charms that require the `fiveg_core_gnb` relation should use `FivegCoreGnbRequires`.
Typical usage of this class would look something like:
```python
...
from charms.sdcore_nms_k8s.v0.fiveg_core_gnb import FivegCoreGnbRequires
...
class SomeRequirerCharm(CharmBase):
GNB_NAME = "gnb001"
def __init__(self, *args):
...
self.fiveg_core_gnb = FivegCoreGnbRequires(
charm=self,
relation_name="fiveg_core_gnb"
)
...
# on relation-joined the charm shall publish the CU/gNodeB name to the databag
self.framework.observe(
self.on.fiveg_core_gnb_relation_joined, self._on_fiveg_core_gnb_relation_joined
)
self.framework.observe(self.on.fiveg_core_gnb_relation_changed,
self._on_fiveg_core_gnb_relation_changed)
def _on_fiveg_core_gnb_relation_joined(self, event: RelationJoinedEvent):
self.fiveg_core_gnb.publish_gnb_information(
gnb_name=self.GNB_NAME,
)
def _on_fiveg_core_gnb_relation_changed(self, event: RelationChangedEvent):
tac = self.fiveg_core_gnb.tac,
plmns = self.fiveg_core_gnb.plmns,
# Do something with the TAC and PLMNs.
```
And a corresponding section in charm's `charmcraft.yaml`:
```
requires:
fiveg_core_gnb: # Relation name
interface: fiveg_core_gnb # Relation interface
```
"""
import json
import logging
from dataclasses import dataclass
from json.decoder import JSONDecodeError
from typing import Any, Dict, Optional
from interface_tester.schema_base import DataBagSchema
from ops.charm import CharmBase
from ops.framework import Object
from pydantic import BaseModel, Field, ValidationError, conlist
# The unique Charmhub library identifier, never change it
LIBID = "196ff8f539ba4f2998209fbb50e2dbbf"
# 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
logger = logging.getLogger(__name__)
"""Schemas definition for the provider and requirer sides of the `fiveg_core_gnb` interface.
It exposes two interfaces.schema_base.DataBagSchema subclasses called:
- ProviderSchema
- RequirerSchema
Examples:
ProviderSchema:
unit: <empty>
app: {
"tac": 1,
"plmns": [
{
"mcc": "001",
"mnc": "01",
"sst": 1,
"sd": 1,
}
],
}
RequirerSchema:
unit: <empty>
app: {
"gnb-name": "gnb001",
}
"""
@dataclass
class PLMNConfig(BaseModel):
"""Dataclass representing the configuration for a PLMN."""
def __init__(self, mcc: str, mnc: str, sst: int, sd: Optional[int] = None) -> None:
super().__init__(mcc=mcc, mnc=mnc, sst=sst, sd=sd)
mcc: str = Field(
description="Mobile Country Code",
examples=["001", "208", "302"],
pattern=r"^[0-9][0-9][0-9]$",
)
mnc: str = Field(
description="Mobile Network Code",
examples=["01", "001", "999"],
pattern=r"^[0-9][0-9][0-9]?$",
)
sst: int = Field(
description="Slice/Service Type",
strict=True,
examples=[1, 2, 3, 4],
ge=0,
le=255,
)
sd: Optional[int] = Field(
description="Slice Differentiator",
strict=True,
default=None,
examples=[1],
ge=0,
le=16777215,
)
def asdict(self):
"""Convert the dataclass into a dictionary."""
return {"mcc": self.mcc, "mnc": self.mnc, "sst": self.sst, "sd": self.sd}
class FivegCoreGnbProviderAppData(BaseModel):
"""Provider application data for fiveg_core_gnb."""
tac: int = Field(
description="Tracking Area Code",
strict=True,
examples=[1],
ge=1,
le=16777215,
)
plmns: conlist(PLMNConfig, min_length=1) # type: ignore[reportInvalidTypeForm]
class ProviderSchema(DataBagSchema):
"""Provider schema for fiveg_core_gnb."""
app_data: FivegCoreGnbProviderAppData
def data_matches_provider_schema(data: dict) -> bool:
"""Return whether data matches provider schema.
Args:
data (dict): Data to be validated.
Returns:
bool: True if data matches provider schema, False otherwise.
"""
try:
ProviderSchema(app_data=FivegCoreGnbProviderAppData(**data))
return True
except ValidationError as e:
logger.error("Invalid data: %s", e)
return False
class FivegCoreGnbProvides(Object):
"""Class to be instantiated by provider of the `fiveg_core_gnb`."""
def __init__(self, charm: CharmBase, relation_name: str):
"""Create a new instance of the FivegCoreGnbProvides class.
Args:
charm: Juju charm
relation_name (str): Relation name
"""
self.relation_name = relation_name
self.charm = charm
super().__init__(charm, relation_name)
def publish_gnb_config_information(
self, relation_id: int, tac: int, plmns: list[PLMNConfig]
) -> None:
"""Set TAC and PLMNs in the relation data.
Args:
relation_id (int): Relation ID.
tac (int): Tracking Area Code.
plmns (list[PLMNConfig]): Configured PLMNs.
"""
if not self.charm.unit.is_leader():
raise RuntimeError("Unit must be leader to set application relation data.")
if not data_matches_provider_schema(
data={"tac": tac, "plmns": plmns}
):
raise ValueError(f"Invalid gNB config: {tac}, {plmns}")
relation = self.model.get_relation(
relation_name=self.relation_name, relation_id=relation_id
)
if not relation:
raise RuntimeError(f"Relation {self.relation_name} not created yet.")
relation.data[self.charm.app].update(
{
"tac": str(tac),
"plmns": json.dumps([plmn.asdict() for plmn in plmns])
}
)
def _get_remote_app_relation_data(self, relation_id: int) -> Optional[dict]:
"""Get relation data for the remote application.
Args:
relation_id (int): Relation ID.
Returns:
str: Relation data for the remote application
or None if the relation data is invalid.
"""
relation = self.model.get_relation(
relation_name=self.relation_name,
relation_id=relation_id
)
if not relation:
logger.error("No relation: %s", self.relation_name)
return None
if not relation.app:
logger.warning("No remote application in relation: %s", self.relation_name)
return None
remote_app_relation_data = dict(relation.data[relation.app])
if not data_matches_requirer_schema(remote_app_relation_data):
logger.error("Invalid relation data: %s", remote_app_relation_data)
return None
return remote_app_relation_data
def get_gnb_name(self, relation_id: int) -> Optional[str]:
"""Return the name of the CU/gNodeB for the given relation.
Args:
relation_id (int): Relation ID.
Returns:
str: gNodeB name.
"""
if remote_relation_data := self._get_remote_app_relation_data(relation_id):
return remote_relation_data["gnb-name"]
return None
class FivegCoreGnbRequirerAppData(BaseModel):
"""Requirer application data for fiveg_core_gnb."""
gnb_name: str = Field(
alias="gnb-name",
description="CU/gNB unique identifier",
examples=["gnb001"],
)
class RequirerSchema(DataBagSchema):
"""Requirer schema for fiveg_core_gnb."""
app_data: FivegCoreGnbRequirerAppData
def data_matches_requirer_schema(data: dict) -> bool:
"""Return whether data matches requirer schema.
Args:
data (dict): Data to be validated.
Returns:
bool: True if data matches requirer schema, False otherwise.
"""
try:
RequirerSchema(app_data=FivegCoreGnbRequirerAppData(**data))
return True
except ValidationError as e:
logger.error("Invalid data: %s", e)
return False
class FivegCoreGnbRequires(Object):
"""Class to be instantiated by requirer of the `fiveg_core_gnb`."""
def __init__(self, charm: CharmBase, relation_name: str):
"""Create a new instance of the FivegCoreGnbRequires class.
Args:
charm: Juju charm
relation_name (str): Relation name
"""
self.relation_name = relation_name
self.charm = charm
super().__init__(charm, relation_name)
def publish_gnb_information(self, gnb_name: str) -> None:
"""Set CU/gNB identifier in the relation data.
Args:
gnb_name (str): CU/gNB unique identifier.
"""
if not self.charm.unit.is_leader():
raise RuntimeError("Unit must be leader to set application relation data.")
if not data_matches_requirer_schema(
data={"gnb-name": gnb_name}
):
raise ValueError(f"Invalid gNB name: {gnb_name}")
relation = self.model.get_relation(relation_name=self.relation_name)
if not relation:
raise RuntimeError(f"Relation {self.relation_name} not created yet.")
relation.data[self.charm.app].update({"gnb-name": gnb_name})
def _get_remote_app_relation_data(self) -> Optional[dict]:
"""Get relation data for the remote application.
Returns:
str: Relation data for the remote application
or None if the relation data is invalid.
"""
relation = self.model.get_relation(self.relation_name)
if not relation:
logger.error("No relation: %s", self.relation_name)
return None
if not relation.app:
logger.warning("No remote application in relation: %s", self.relation_name)
return None
remote_app_relation_data: Dict[str, Any] = dict(relation.data[relation.app])
plmns = remote_app_relation_data.get("plmns", "")
try:
remote_app_relation_data["tac"] = int(remote_app_relation_data.get("tac", ""))
remote_app_relation_data["plmns"] = [
PLMNConfig(**data) for data in json.loads(plmns)
]
except (JSONDecodeError, ValidationError, ValueError):
logger.error("Invalid relation data: %s", remote_app_relation_data)
return None
if not data_matches_provider_schema(remote_app_relation_data):
logger.error("Invalid relation data: %s", remote_app_relation_data)
return None
return remote_app_relation_data
@property
def tac(self) -> Optional[int]:
"""Return the configured TAC for the CU/gNodeB.
Returns:
int: TAC.
"""
if remote_relation_data := self._get_remote_app_relation_data():
return remote_relation_data["tac"]
return None
@property
def plmns(self) -> Optional[list[PLMNConfig]]:
"""Return the configured PLMNs for the CU/gNodeB.
Returns:
list: PLMNs.
"""
if remote_relation_data := self._get_remote_app_relation_data():
return remote_relation_data["plmns"]
return None