Skip to content

Commit

Permalink
Merge pull request #9 from charmed-osm/implements-lte-core-interface
Browse files Browse the repository at this point in the history
sanchezfdezjavier authored Oct 20, 2022
2 parents edd3fea + d1a5880 commit ebae9df
Showing 6 changed files with 286 additions and 77 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -56,4 +56,5 @@ juju run-action <unit> remove-default-gw --wait

## Relations

- **lte-vepc**: LTE VEPC Interface. Shares enodeB's MME address.
- **lte-core**: The LTE core interface is used to connect to a 4G/LTE core network via its MME IPv4 address.

242 changes: 242 additions & 0 deletions lib/charms/lte_core_interface/v0/lte_core_interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
# Copyright 2022 Canonical Ltd.
# See LICENSE file for licensing details.

"""Library for the `lte-core` relation.
This library contains the Requires and Provides classes for handling the `lte-core`
interface.
The purpose of the library is to relate a charmed EPC (Provider) with charmed simulated enodeB and
user equipment (UEs) (Requirer). The interface will share the IP address of the MME
(Mobility Management Entity) from EPC to the charm which contains the corresponding
simulated enodeBs and UEs.
## Getting Started
From a charm directory, fetch the library using `charmcraft`:
```shell
charmcraft fetch-lib charms.lte_core_interface.v0.lte_core_interface
```
Add the following libraries to the charm's `requirements.txt` file:
- jsonschema
### Requirer charm
The requirer charm is the one requiring to connect to the LTE core
from another charm that provides this interface.
Example:
```python
from ops.charm import CharmBase
from ops.main import main
from charms.lte_core_interface.v0.lte_core_interface import (
LTECoreAvailableEvent,
LTECoreRequires,
)
class DummyLTECoreRequirerCharm(CharmBase):
def __init__(self, *args):
super().__init__(*args)
self.lte_core_requirer = LTECoreRequires(self, "lte-core")
self.framework.observe(
self.lte_core_requirer.on.lte_core_available,
self._on_lte_core_available,
)
def _on_lte_core_available(self, event: LTECoreAvailableEvent):
mme_ipv4_address = event.mme_ipv4_address
<Do something with the mme_ipv4_address>
if __name__ == "__main__":
main(DummyLTECoreRequirerCharm)
```
### Provider charm
The provider charm is the one providing information about the LTE core network
for another charm that requires this interface.
Example:
```python
from ops.charm import CharmBase, RelationJoinedEvent
from ops.main import main
from charms.lte_core_interface.v0.lte_core_interface import (
LTECoreProvides,
)
class DummyLTECoreProviderCharm(CharmBase):
def __init__(self, *args):
super().__init__(*args)
self.lte_core_provider = LTECoreProvides(self, "lte-core")
self.framework.observe(
self.on.lte_core_relation_joined, self._on_lte_core_relation_joined
)
def _on_lte_core_relation_joined(self, event: RelationJoinedEvent) -> None:
if not self.unit.is_leader():
return
mme_ipv4_address = "some code for fetching the mme ipv4 address"
self.lte_core_provider.set_lte_core_information(mme_ipv4_address=mme_ipv4_address)
if __name__ == "__main__":
main(DummyLTECoreProviderCharm)
```
"""

import logging
from ipaddress import AddressValueError, IPv4Address

from jsonschema import exceptions, validate # type: ignore[import]
from ops.charm import CharmBase, CharmEvents, RelationChangedEvent
from ops.framework import EventBase, EventSource, Handle, Object

# The unique Charmhub library identifier, never change it
LIBID = "3fbbdca922ec4ddd9598c3382034ad61"

# 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 = 3


logger = logging.getLogger(__name__)

REQUIRER_JSON_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "`lte-core` requirer root schema",
"type": "object",
"description": "The `lte-core` root schema comprises the entire requirer databag for this interface.", # noqa: E501
"examples": [
{
"mme_ipv4_address": "127.0.0.1", # noqa: E501
}
],
"properties": {
"mme_ipv4_address": {
"description": "The IP address of the MME (Mobility Management Entity) from the LTE core.", # noqa: E501
"type": "string",
"format": "ipv4",
},
},
"required": [
"mme_ipv4_address",
],
"additionalProperties": True,
}


class LTECoreAvailableEvent(EventBase):
"""Charm event emitted when a LTE core is available. It carries the mme ipv4 address."""

def __init__(self, handle: Handle, mme_ipv4_address: str):
"""Init."""
super().__init__(handle)
self.mme_ipv4_address = mme_ipv4_address

def snapshot(self) -> dict:
"""Returns snapshot."""
return {"mme_ipv4_address": self.mme_ipv4_address}

def restore(self, snapshot: dict) -> None:
"""Restores snapshot."""
self.mme_ipv4_address = snapshot["mme_ipv4_address"]


class LTECoreRequirerCharmEvents(CharmEvents):
"""List of events that the LTE core requirer charm can leverage."""

lte_core_available = EventSource(LTECoreAvailableEvent)


class LTECoreRequires(Object):
"""Class to be instantiated by the charm requiring the LTE core."""

on = LTECoreRequirerCharmEvents()

def __init__(self, charm: CharmBase, relationship_name: str):
"""Init."""
super().__init__(charm, relationship_name)
self.charm = charm
self.relationship_name = relationship_name
self.framework.observe(
charm.on[relationship_name].relation_changed, self._on_relation_changed
)

@staticmethod
def _relation_data_is_valid(remote_app_relation_data: dict) -> bool:
try:
validate(instance=remote_app_relation_data, schema=REQUIRER_JSON_SCHEMA)
return True
except exceptions.ValidationError as e:
logger.error("Invalid relation data: %s", e)
return False

def _on_relation_changed(self, event: RelationChangedEvent) -> None:
"""Handler triggered on relation changed event.
Args:
event: Juju event (RelationChangedEvent)
Returns:
None
"""
relation = event.relation
if not relation.app:
logger.warning("No remote application in relation: %s", self.relationship_name)
return
remote_app_relation_data = relation.data[relation.app]
if not self._relation_data_is_valid(dict(remote_app_relation_data)):
logger.warning(
"Provider relation data did no pass JSON Schema validation: \n%s",
event.relation.app[event.app],
)
return
self.on.lte_core_available.emit(
mme_ipv4_address=remote_app_relation_data["mme_ipv4_address"],
)


class LTECoreProvides(Object):
"""Class to be instantiated by the charm providing the LTE core."""

def __init__(self, charm: CharmBase, relationship_name: str):
"""Init."""
super().__init__(charm, relationship_name)
self.relationship_name = relationship_name
self.charm = charm

@staticmethod
def _mme_ipv4_address_is_valid(mme_ipv4_address: str) -> bool:
"""Returns whether mme ipv4 address is valid."""
try:
IPv4Address(mme_ipv4_address)
return True
except (AddressValueError): # noqa: E722
return False

def set_lte_core_information(self, mme_ipv4_address: str) -> None:
"""Sets mme_ipv4_address in the application relation data.
Args:
mme_ipv4_address: MME ipv4 address
Returns:
None
Raises:
AddressValueError: If mme_ipv4_address is not a valid IPv4 address
"""
if not self.model.get_relation(self.relationship_name):
raise RuntimeError(f"Relation {self.relationship_name} not created yet.")
if not self._mme_ipv4_address_is_valid(mme_ipv4_address):
raise AddressValueError("Invalid MME IPv4 address.")
relation = self.model.get_relation(self.relationship_name)
relation.data[self.charm.app].update({"mme_ipv4_address": mme_ipv4_address}) # type: ignore[union-attr] # noqa: E501
5 changes: 3 additions & 2 deletions metadata.yaml
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ summary: |
Open-source 4G EnodeB and User emulators developed by [Software Radio Systems (SRS)](https://www.srslte.com/).
series:
- focal

requires:
mme:
interface: lte-vepc # s1c
lte-core:
interface: lte-core
5 changes: 3 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
ops==1.5.2
ops == 1.5.2
psutil
netifaces
netaddr
jinja2
jinja2
jsonschema
44 changes: 24 additions & 20 deletions src/charm.py
Original file line number Diff line number Diff line change
@@ -15,7 +15,6 @@
CharmBase,
ConfigChangedEvent,
InstallEvent,
RelationChangedEvent,
StartEvent,
StopEvent,
UpdateStatusEvent,
@@ -24,13 +23,16 @@
from ops.main import main
from ops.model import ActiveStatus, MaintenanceStatus

from lib.charms.lte_core_interface.v0.lte_core_interface import (
LTECoreAvailableEvent,
LTECoreRequires,
)
from utils import (
copy_files,
git_clone,
install_apt_packages,
ip_from_default_iface,
ip_from_iface,
is_ipv4,
service_active,
service_enable,
service_restart,
@@ -126,8 +128,26 @@ def __init__(self, *args):
self.framework.observe(self.on.detach_ue_action, self._on_detach_ue_action)
self.framework.observe(self.on.remove_default_gw_action, self._on_remove_default_gw_action)

# Relations hooks
self.framework.observe(self.on.mme_relation_changed, self._mme_relation_changed)
self.lte_core_requirer = LTECoreRequires(self, "lte-core")
self.framework.observe(
self.lte_core_requirer.on.lte_core_available,
self._on_lte_core_available,
)

def _on_lte_core_available(self, event: LTECoreAvailableEvent) -> None:
"""Triggered on lte_core_available.
Retrieves MME address from relation, configures the srs enb service and restarts it.
"""
mme_addr = event.mme_ipv4_address
logging.info(f"MME IPv4 address from LTE core: {mme_addr}")
self._stored.mme_addr = mme_addr
self._configure_srsenb_service()
if self._stored.started:
self.unit.status = MaintenanceStatus("Reloading srsenb.")
service_restart(SRS_ENB_SERVICE)
logging.info("Restarting EnodeB after MME IP address change.")
self.unit.status = self._get_current_status()

def _on_install(self, _: InstallEvent) -> None:
"""Triggered on install event."""
@@ -207,22 +227,6 @@ def _on_remove_default_gw_action(self, event: ActionEvent) -> None:
shell("route del default")
event.set_results({"status": "ok", "message": "Default route removed!"})

def _mme_relation_changed(self, event: RelationChangedEvent) -> None:
"""Triggered on MME relation changed event.
Retrieves MME address from relation, configures the srs enb service and restarts it.
"""
if event.unit in event.relation.data:
mme_addr = event.relation.data[event.unit].get("mme-addr")
if not is_ipv4(mme_addr):
return
self._stored.mme_addr = mme_addr
self._configure_srsenb_service()
if self._stored.started:
self.unit.status = MaintenanceStatus("Reloading srsenb")
service_restart(SRS_ENB_SERVICE)
self.unit.status = self._get_current_status()

def _configure_srsenb_service(self) -> None:
"""Configures srs enb service."""
self._configure_service(
64 changes: 12 additions & 52 deletions tests/unit/test_charm.py
Original file line number Diff line number Diff line change
@@ -46,6 +46,8 @@ class TestCharm(unittest.TestCase):
DETACH_ACTION_PARAMS = {"usim-imsi": None, "usim-opc": None, "usim-k": None}

def setUp(self) -> None:
self.remote_app_name = "magma-access-gateway-operator"
self.relation_name = "lte-core"
self.harness = testing.Harness(SrsLteCharm)
self.addCleanup(self.harness.cleanup)
self.harness.begin()
@@ -495,65 +497,23 @@ def test_given_on_remove_default_gw_action_when_remove_default_gw_action_then_st

self.assertEqual(self.harness.charm.unit.status, old_status)

@patch("subprocess.run")
@patch("builtins.open", new_callable=mock_open)
def test_given_unit_in_relation_data_and_mme_is_not_ipv4_when_mme_relation_changed_then_status_does_not_change( # noqa: E501
self, _, __
):
old_status = self.harness.charm.unit.status
mock_event = Mock()
mock_event.unit = "lte-vepc/0"
mme_relation_id = self.harness.add_relation(relation_name="mme", remote_app="lte-vepc")
self.harness.add_relation_unit(mme_relation_id, "lte-vepc/0")
self.harness.update_relation_data(
relation_id=mme_relation_id,
app_or_unit="lte-vepc/0",
key_values={"mme-addr": "not an ip"},
)

self.assertEqual(self.harness.charm.unit.status, old_status)

# lte-core-interface
@patch("subprocess.run", new=Mock())
@patch("builtins.open", new_callable=mock_open)
@patch("charm.service_active")
def test_given_unit_in_relation_data_and_mme_is_ipv4_when_mme_relation_changed_then_status_is_active( # noqa: E501
def test_given_lte_core_provider_charm_when_relation_is_created_then_mme_addr_is_updated_in_stored( # noqa: E501
self, patch_service_active, _
):
valid_mme = "0.0.0.0"
mock_event = Mock()
mock_event.unit = "lte-vepc/0"
self.harness.charm._stored.installed = True
self.harness.charm._stored.started = True
patch_service_active.return_value = True
self.harness.charm._stored.mme_addr = valid_mme

mme_relation_id = self.harness.add_relation(relation_name="mme", remote_app="lte-vepc")
self.harness.add_relation_unit(mme_relation_id, "lte-vepc/0")
self.harness.update_relation_data(
relation_id=mme_relation_id,
app_or_unit="lte-vepc/0",
key_values={"mme-addr": valid_mme},
mme_ipv4_address = "0.0.0.0"
relation_data = {"mme_ipv4_address": mme_ipv4_address}
relation_id = self.harness.add_relation(
relation_name=self.relation_name, remote_app=self.remote_app_name
)

self.assertEqual(
self.harness.charm.unit.status, ActiveStatus("srsenb started. mme: 0.0.0.0. ")
)

@patch("utils.service_restart")
@patch("subprocess.run")
@patch("builtins.open", new_callable=mock_open)
def test_given_unit_in_relation_data_mme_is_ipv4_and_service_is_not_started_when_mme_relation_changed_then_service_is_not_restarted( # noqa: E501
self, __, patch_subprocess_run, patch_service_restart
):
mock_event = Mock()
mock_event.unit = "lte-vepc/0"
mme_relation_id = self.harness.add_relation(relation_name="mme", remote_app="lte-vepc")

self.harness.add_relation_unit(mme_relation_id, "lte-vepc/0")
self.harness.update_relation_data(
relation_id=mme_relation_id,
app_or_unit="lte-vepc/0",
key_values={"mme-addr": "0.0.0.0"},
relation_id=relation_id,
app_or_unit=self.remote_app_name,
key_values=relation_data,
)

patch_service_restart.assert_not_called()
self.assertEqual(self.harness.charm._stored.mme_addr, mme_ipv4_address)

0 comments on commit ebae9df

Please sign in to comment.