diff --git a/.github/workflows/matrix-tests.yaml b/.github/workflows/matrix-tests.yaml index 82128a27..94fc06c5 100644 --- a/.github/workflows/matrix-tests.yaml +++ b/.github/workflows/matrix-tests.yaml @@ -26,11 +26,16 @@ jobs: needs: set-matrix runs-on: ubuntu-latest strategy: + fail-fast: false matrix: interface: ${{ fromJSON(needs.set-matrix.outputs.matrix_values) }} steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 - name: Set up python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 - name: Install dependencies diff --git a/docs/json_schemas/fiveg_core_gnb/v0/provider.json b/docs/json_schemas/fiveg_core_gnb/v0/provider.json new file mode 100644 index 00000000..8269d932 --- /dev/null +++ b/docs/json_schemas/fiveg_core_gnb/v0/provider.json @@ -0,0 +1,122 @@ +{ + "$defs": { + "BaseModel": { + "properties": {}, + "title": "BaseModel", + "type": "object" + }, + "FivegCoreGnbProviderAppData": { + "properties": { + "tac": { + "description": "Tracking Area Code", + "examples": [ + 1 + ], + "maximum": 16777215, + "minimum": 1, + "title": "Tac", + "type": "integer" + }, + "plmns": { + "items": { + "$ref": "#/$defs/PLMNConfig" + }, + "title": "Plmns", + "type": "array" + } + }, + "required": [ + "tac", + "plmns" + ], + "title": "FivegCoreGnbProviderAppData", + "type": "object" + }, + "PLMNConfig": { + "properties": { + "mcc": { + "description": "Mobile Country Code", + "examples": [ + "001", + "208", + "302" + ], + "pattern": "[0-9][0-9][0-9]", + "title": "Mcc", + "type": "string" + }, + "mnc": { + "description": "Mobile Network Code", + "examples": [ + "01", + "001", + "999" + ], + "pattern": "[0-9][0-9][0-9]?", + "title": "Mnc", + "type": "string" + }, + "sst": { + "description": "Slice/Service Type", + "examples": [ + 1, + 2, + 3, + 4 + ], + "maximum": 255, + "minimum": 0, + "title": "Sst", + "type": "integer" + }, + "sd": { + "anyOf": [ + { + "maximum": 16777215, + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Slice Differentiator", + "examples": [ + 1 + ], + "title": "Sd" + } + }, + "required": [ + "mcc", + "mnc", + "sst" + ], + "title": "PLMNConfig", + "type": "object" + } + }, + "description": "The schema for the provider side of the fiveg_core_gnb interface.", + "properties": { + "unit": { + "anyOf": [ + { + "$ref": "#/$defs/BaseModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "app": { + "$ref": "#/$defs/FivegCoreGnbProviderAppData" + } + }, + "required": [ + "app" + ], + "title": "ProviderSchema", + "type": "object" +} \ No newline at end of file diff --git a/docs/json_schemas/fiveg_core_gnb/v0/requirer.json b/docs/json_schemas/fiveg_core_gnb/v0/requirer.json new file mode 100644 index 00000000..73557dbc --- /dev/null +++ b/docs/json_schemas/fiveg_core_gnb/v0/requirer.json @@ -0,0 +1,48 @@ +{ + "$defs": { + "BaseModel": { + "properties": {}, + "title": "BaseModel", + "type": "object" + }, + "FivegCoreGnbRequirerAppData": { + "properties": { + "cu_name": { + "description": "Unique identifier of the CU/gnB.", + "examples": [ + "gnb001" + ], + "title": "Cu Name", + "type": "string" + } + }, + "required": [ + "cu_name" + ], + "title": "FivegCoreGnbRequirerAppData", + "type": "object" + } + }, + "description": "The schema for the requirer side of the fiveg_core_gnb interface.", + "properties": { + "unit": { + "anyOf": [ + { + "$ref": "#/$defs/BaseModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "app": { + "$ref": "#/$defs/FivegCoreGnbRequirerAppData" + } + }, + "required": [ + "app" + ], + "title": "RequirerSchema", + "type": "object" +} \ No newline at end of file diff --git a/docs/json_schemas/fiveg_f1/v0/provider.json b/docs/json_schemas/fiveg_f1/v0/provider.json index 39f6fa31..c1a4ec3d 100644 --- a/docs/json_schemas/fiveg_f1/v0/provider.json +++ b/docs/json_schemas/fiveg_f1/v0/provider.json @@ -23,14 +23,97 @@ ], "title": "F1 Port", "type": "integer" + }, + "tac": { + "description": "Tracking Area Code", + "examples": [ + 1 + ], + "maximum": 16777215, + "minimum": 1, + "title": "Tac", + "type": "integer" + }, + "plmns": { + "items": { + "$ref": "#/$defs/PLMNConfig" + }, + "title": "Plmns", + "type": "array" } }, "required": [ "f1_ip_address", - "f1_port" + "f1_port", + "tac", + "plmns" ], "title": "FivegF1ProviderAppData", "type": "object" + }, + "PLMNConfig": { + "properties": { + "mcc": { + "description": "Mobile Country Code", + "examples": [ + "001", + "208", + "302" + ], + "pattern": "^[0-9][0-9][0-9]$", + "title": "Mcc", + "type": "string" + }, + "mnc": { + "description": "Mobile Network Code", + "examples": [ + "01", + "001", + "999" + ], + "pattern": "^[0-9][0-9][0-9]?$", + "title": "Mnc", + "type": "string" + }, + "sst": { + "description": "Slice/Service Type", + "examples": [ + 1, + 2, + 3, + 4 + ], + "maximum": 255, + "minimum": 0, + "title": "Sst", + "type": "integer" + }, + "sd": { + "anyOf": [ + { + "maximum": 16777215, + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Slice Differentiator", + "examples": [ + 1 + ], + "title": "Sd" + } + }, + "required": [ + "mcc", + "mnc", + "sst" + ], + "title": "PLMNConfig", + "type": "object" } }, "description": "Provider schema for fiveg_f1.", diff --git a/docs/json_schemas/fiveg_rfsim/v0/provider.json b/docs/json_schemas/fiveg_rfsim/v0/provider.json index 80a64032..ed0d712d 100644 --- a/docs/json_schemas/fiveg_rfsim/v0/provider.json +++ b/docs/json_schemas/fiveg_rfsim/v0/provider.json @@ -14,10 +14,42 @@ ], "title": "Rfsim Address", "type": "string" + }, + "sst": { + "description": "Slice/Service Type", + "examples": [ + 1, + 2, + 3, + 4 + ], + "maximum": 255, + "minimum": 0, + "title": "Sst", + "type": "integer" + }, + "sd": { + "anyOf": [ + { + "maximum": 16777215, + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Slice Differentiator", + "examples": [ + 1 + ], + "title": "Sd" } }, "required": [ - "rfsim_address" + "rfsim_address", + "sst" ], "title": "FivegRFSIMProviderAppData", "type": "object" diff --git a/docs/json_schemas/tempo_cluster/v1/provider.json b/docs/json_schemas/tempo_cluster/v1/provider.json new file mode 100644 index 00000000..212ae4d7 --- /dev/null +++ b/docs/json_schemas/tempo_cluster/v1/provider.json @@ -0,0 +1,184 @@ +{ + "$defs": { + "BaseModel": { + "properties": {}, + "title": "BaseModel", + "type": "object" + }, + "TempoClusterProviderAppData": { + "description": "TempoClusterProviderAppData.", + "properties": { + "worker_config": { + "contentMediaType": "application/json", + "contentSchema": { + "type": "string" + }, + "description": "The tempo configuration that the requirer should run with.Yaml-encoded. Must conform to the schema that the presently deployed workload version supports; for example see: https://grafana.com/docs/tempo/latest/configuration/#configure-tempo.", + "title": "Worker Config", + "type": "string" + }, + "loki_endpoints": { + "anyOf": [ + { + "contentMediaType": "application/json", + "contentSchema": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "List of loki-push-api endpoints to which the worker node can push any logs it generates.", + "title": "Loki Endpoints" + }, + "ca_cert": { + "anyOf": [ + { + "contentMediaType": "application/json", + "contentSchema": { + "type": "string" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "CA certificate for tls encryption.", + "title": "Ca Cert" + }, + "server_cert": { + "anyOf": [ + { + "contentMediaType": "application/json", + "contentSchema": { + "type": "string" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Server certificate for tls encryption.", + "title": "Server Cert" + }, + "privkey_secret_id": { + "anyOf": [ + { + "contentMediaType": "application/json", + "contentSchema": { + "type": "string" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "ID of a Juju secret that holds the private key used by the coordinator for TLS encryption.", + "title": "Privkey Secret Id" + }, + "remote_write_endpoints": { + "anyOf": [ + { + "contentMediaType": "application/json", + "contentSchema": { + "items": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "type": "array" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Endpoints to which the workload (and the worker charm) can push metrics to.", + "title": "Remote Write Endpoints" + }, + "charm_tracing_receivers": { + "anyOf": [ + { + "contentMediaType": "application/json", + "contentSchema": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Endpoints to which the worker node can push its charm traces to.It is a mapping from protocol names such as `zipkin`, `otlp_grpc`, `otlp_http`.", + "title": "Charm Tracing Receivers" + }, + "workload_tracing_receivers": { + "anyOf": [ + { + "contentMediaType": "application/json", + "contentSchema": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Endpoints to which the worker node can push its workload traces to.It is a mapping from protocol names such as `zipkin`, `otlp_grpc`, `otlp_http`.", + "title": "Workload Tracing Receivers" + } + }, + "required": [ + "worker_config" + ], + "title": "TempoClusterProviderAppData", + "type": "object" + } + }, + "description": "The schema for the provider side of this interface.", + "properties": { + "unit": { + "anyOf": [ + { + "$ref": "#/$defs/BaseModel" + }, + { + "type": "null" + } + ], + "default": null + }, + "app": { + "$ref": "#/$defs/TempoClusterProviderAppData" + } + }, + "required": [ + "app" + ], + "title": "ProviderSchema", + "type": "object" +} \ No newline at end of file diff --git a/docs/json_schemas/tempo_cluster/v1/requirer.json b/docs/json_schemas/tempo_cluster/v1/requirer.json new file mode 100644 index 00000000..857fed5d --- /dev/null +++ b/docs/json_schemas/tempo_cluster/v1/requirer.json @@ -0,0 +1,116 @@ +{ + "$defs": { + "TempoClusterRequirerAppData": { + "description": "TempoClusterRequirerAppData.", + "properties": { + "role": { + "contentMediaType": "application/json", + "contentSchema": { + "$ref": "#/$defs/TempoRole" + }, + "title": "Role", + "type": "string" + } + }, + "required": [ + "role" + ], + "title": "TempoClusterRequirerAppData", + "type": "object" + }, + "TempoClusterRequirerUnitData": { + "description": "TempoClusterRequirerUnitData.", + "properties": { + "juju_topology": { + "contentMediaType": "application/json", + "contentSchema": { + "$ref": "#/$defs/_Topology" + }, + "title": "Juju Topology", + "type": "string" + }, + "address": { + "contentMediaType": "application/json", + "contentSchema": { + "type": "string" + }, + "title": "Address", + "type": "string" + } + }, + "required": [ + "juju_topology", + "address" + ], + "title": "TempoClusterRequirerUnitData", + "type": "object" + }, + "TempoRole": { + "description": "Tempo component role names.\n\nReferences:\n arch:\n -> https://grafana.com/docs/tempo/latest/operations/architecture/\n config:\n -> https://grafana.com/docs/tempo/latest/configuration/#server", + "enum": [ + "all", + "querier", + "query-frontend", + "ingester", + "distributor", + "compactor", + "metrics-generator" + ], + "title": "TempoRole", + "type": "string" + }, + "_Topology": { + "description": "JujuTopology as defined by cos-lib.", + "properties": { + "application": { + "title": "Application", + "type": "string" + }, + "charm_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Charm Name" + }, + "unit": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Unit" + } + }, + "required": [ + "application", + "charm_name", + "unit" + ], + "title": "_Topology", + "type": "object" + } + }, + "description": "The schema for the requirer side of this interface.", + "properties": { + "unit": { + "$ref": "#/$defs/TempoClusterRequirerUnitData" + }, + "app": { + "$ref": "#/$defs/TempoClusterRequirerAppData" + } + }, + "required": [ + "unit", + "app" + ], + "title": "RequirerSchema", + "type": "object" +} \ No newline at end of file diff --git a/interfaces/fiveg_core_gnb/v0/README.md b/interfaces/fiveg_core_gnb/v0/README.md new file mode 100644 index 00000000..bd50381c --- /dev/null +++ b/interfaces/fiveg_core_gnb/v0/README.md @@ -0,0 +1,67 @@ +# `fiveg_core_gnb` + +## Usage + +Within 5G, the CU is the Central Unit of a RAN (Radio Access Network) and needs to be configured according to the 5G network parameters. + +The `fiveg_core_gnb` relation interface describes the expected behavior of any charm claiming to be able to provide or consume the CU (or gNodeB) configuration information. + +In a typical 5G network, the provider of this interface would be a CU or a gNodeB. The requirer of this interface would be the NMS (Network Management System). + +## Direction + +```mermaid +flowchart TD + Provider -- MCC, MNC, TAC, SST, SD --> Requirer + Requirer -- CU/gNodeB Identifier --> Provider +``` + +As with all Juju relations, the `fiveg_core_gnb` interface consists of two parties: a Provider and a Requirer. + +## Behavior + +Both the Requirer and the Provider need to adhere to criteria to be considered compatible with the interface. + +### Provider + +- Is expected to provide the following data: + - TAC (Tracking Area Code) + - List of PLMNs + +The list of PLMNs should include the following data: + - MCC (Mobile Country Code) + - MNC (Mobile Network Code) + - SST (Slice Service Type) + - SD (Slice Differentiator) + + +### Requirer + +- Is expected to provide a unique identifier of the CU (or gNodeB). + +## Relation Data + +[\[Pydantic Schema\]](./schema.py) + +#### Example + +```yaml +provider: + app: { + "tac": 1, + "plmns": [ + { + "mcc": "001", + "mnc": "01", + "sst": 1, + "sd": 1, + } + ], + } + unit: {} +requirer: + app: { + "cu_name": "gnb001" + } + unit: {} +``` diff --git a/interfaces/fiveg_core_gnb/v0/interface.yaml b/interfaces/fiveg_core_gnb/v0/interface.yaml new file mode 100644 index 00000000..6edceeb2 --- /dev/null +++ b/interfaces/fiveg_core_gnb/v0/interface.yaml @@ -0,0 +1,14 @@ +name: fiveg_core_gnb +internal: true + +version: 0 +status: draft + +providers: + - name: sdcore-nms-k8s-operator + url: https://github.com/canonical/sdcore-nms-k8s-operator +requirers: + - name: sdcore-gnbsim-k8s-operator + url: https://github.com/canonical/sdcore-gnbsim-k8s-operator + +maintainer: telco diff --git a/interfaces/fiveg_core_gnb/v0/schema.py b/interfaces/fiveg_core_gnb/v0/schema.py new file mode 100644 index 00000000..9da5846c --- /dev/null +++ b/interfaces/fiveg_core_gnb/v0/schema.py @@ -0,0 +1,87 @@ +"""This file defines the schemas for the provider and requirer sides of the `fiveg_core_gnb` relation interface. + +It must expose two interfaces.schema_base.DataBagSchema subclasses called: +- ProviderSchema +- RequirerSchema + +Examples: + ProviderSchema: + unit: + app: { + "tac": 1, + "plmns": [ + { + "mcc": "001", + "mnc": "01", + "sst": 1, + "sd": 1, + } + ], + } + RequirerSchema: + unit: + app: { + "cu_name": "gnb001", + } +""" + +from dataclasses import dataclass +from interface_tester.schema_base import DataBagSchema +from pydantic import BaseModel, Field +from typing import List, Optional + + +@dataclass +class PLMNConfig: + """Dataclass representing the configuration for a PLMN.""" + + 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", + examples=[1, 2, 3, 4], + ge=0, + le=255, + ) + sd: Optional[int] = Field( + description="Slice Differentiator", + default=None, + examples=[1], + ge=0, + le=16777215, + ) + + +class FivegCoreGnbProviderAppData(BaseModel): + tac: int = Field( + description="Tracking Area Code", + examples=[1], + ge=1, + le=16777215, + ) + plmns: List[PLMNConfig] + + +class FivegCoreGnbRequirerAppData(BaseModel): + cu_name: str = Field( + description="Unique identifier of the CU/gnB.", + examples=["gnb001"] + ) + + +class ProviderSchema(DataBagSchema): + """The schema for the provider side of the fiveg_core_gnb interface.""" + app: FivegCoreGnbProviderAppData + + +class RequirerSchema(DataBagSchema): + """The schema for the requirer side of the fiveg_core_gnb interface.""" + app: FivegCoreGnbRequirerAppData diff --git a/interfaces/fiveg_f1/v0/README.md b/interfaces/fiveg_f1/v0/README.md index 5c4e5a93..2ace380a 100644 --- a/interfaces/fiveg_f1/v0/README.md +++ b/interfaces/fiveg_f1/v0/README.md @@ -10,7 +10,7 @@ This relation interface describes the expected behavior of any charm claiming to ```mermaid flowchart TD - Provider -- ip_address, port --> Requirer + Provider -- ip_address, port, tac, plmns --> Requirer Requirer -- port --> Provider ``` @@ -22,12 +22,22 @@ Both the Requirer and the Provider need to adhere to criteria to be considered c ### Provider -- Is expected to provide the IP address and port of the CU's F1 interface. +- Is expected to provide the following data: + - IP address of the network interface used for F1 traffic + - Number of the port used for F1 traffic + - TAC (Tracking Area Code) + - List of PLMNs + +The list of PLMNs should include the following data: + - MCC (Mobile Country Code) + - MNC (Mobile Network Code) + - SST (Slice Service Type) + - SD (Slice Differentiator) ### Requirer - Is expected to use the IP address and the port passed by the provider to establish communication over the F1 interface. -- Is expected to provider the number of the port which will handle communication over the F1 interface. +- Is expected to provide the number of the port which will handle communication over the F1 interface. ## Relation Data @@ -39,7 +49,16 @@ Both the Requirer and the Provider need to adhere to criteria to be considered c provider: app: { "f1_ip_address": "192.168.70.132", - "f1_port": 2153 + "f1_port": 2153, + "tac": 1, + "plmns": [ + { + "mcc": "001", + "mnc": "01", + "sst": 1, + "sd": 1, + } + ], } unit: {} requirer: diff --git a/interfaces/fiveg_f1/v0/schema.py b/interfaces/fiveg_f1/v0/schema.py index 257afb35..d783c2e5 100644 --- a/interfaces/fiveg_f1/v0/schema.py +++ b/interfaces/fiveg_f1/v0/schema.py @@ -7,7 +7,16 @@ unit: app: { "f1_ip_address": "192.168.70.132", - "f1_port": 2153 + "f1_port": 2153, + "tac": 1, + "plmns": [ + { + "mcc": "001", + "mnc": "01", + "sst": 1, + "sd": 1, + } + ], } RequirerSchema: unit: @@ -17,9 +26,37 @@ """ from pydantic import BaseModel, IPvAnyAddress, Field - +from dataclasses import dataclass from interface_tester.schema_base import DataBagSchema +from typing import List, Optional, conlist +@dataclass +class PLMNConfig: + """Dataclass representing the configuration for a PLMN.""" + + 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", + examples=[1, 2, 3, 4], + ge=0, + le=255, + ) + sd: Optional[int] = Field( + description="Slice Differentiator", + default=None, + examples=[1], + ge=0, + le=16777215, + ) class FivegF1ProviderAppData(BaseModel): f1_ip_address: IPvAnyAddress = Field( @@ -30,6 +67,13 @@ class FivegF1ProviderAppData(BaseModel): description="Number of the port used for F1 traffic", examples=[2153] ) + tac: int = Field( + description="Tracking Area Code", + examples=[1], + ge=1, + le=16777215, + ) + plmns: conlist(PLMNConfig, min_length=1) class FivegF1RequirerAppData(BaseModel): diff --git a/interfaces/fiveg_rfsim/v0/README.md b/interfaces/fiveg_rfsim/v0/README.md index 926c053e..640b95c5 100644 --- a/interfaces/fiveg_rfsim/v0/README.md +++ b/interfaces/fiveg_rfsim/v0/README.md @@ -4,7 +4,7 @@ Within 5G RAN (Radio Access Network) architecture, the OAI DU charm can be started to act as both the DU and the RU through its RF simulator functionality. -The OAI UE charm requires RF simulator address in order to connect. Hence, the provider of this interface would be a OAI DU charm and the requirer of this interface would be the OAI UE charm. +The OAI UE charm requires RF simulator address and the network information(SST, SD) in order to connect. Hence, the provider of this interface would be a OAI DU charm and the requirer of this interface would be the OAI UE charm. This relation interface describes the expected behavior of charms claiming to be able to provide or consume information on connectivity over the fiveg_rfsim interface. @@ -12,7 +12,7 @@ This relation interface describes the expected behavior of charms claiming to be ```mermaid flowchart TD - Provider -- rfsim_address --> Requirer + Provider -- rfsim_address, sst, sd --> Requirer ``` As with all Juju relations, the `fiveg_rfsim` interface consists of two parties: a Provider and a Requirer. @@ -23,11 +23,15 @@ Both the Requirer and the Provider need to adhere to criteria to be considered c ### Provider -- Is expected to provide the DU's `rfsim` service ip. +Is expected to provide following information: + +- The DU's `rfsim` service ip +- Network Slice/Service Type (SST) +- Slice Differentiator (SD) ### Requirer -- Is expected to use the `rfsim` service address passed by the provider. +- Is expected to use the `rfsim` service address and the network information(SST, SD) passed by the provider. ## Relation Data @@ -39,6 +43,8 @@ Both the Requirer and the Provider need to adhere to criteria to be considered c provider: app: { "rfsim_address": "192.168.70.130", + "sst": 1, + "sd": 1, } unit: {} requirer: diff --git a/interfaces/fiveg_rfsim/v0/schema.py b/interfaces/fiveg_rfsim/v0/schema.py index c53c2700..0363cf96 100644 --- a/interfaces/fiveg_rfsim/v0/schema.py +++ b/interfaces/fiveg_rfsim/v0/schema.py @@ -7,11 +7,14 @@ unit: app: { "rfsim_address": "192.168.70.130", + "sst": 1, + "sd": 1, } RequirerSchema: unit: app: """ +from typing import Optional from pydantic import BaseModel, Field @@ -23,6 +26,20 @@ class FivegRFSIMProviderAppData(BaseModel): description="RF simulator service ip", examples=["192.168.70.130"] ) + sst: int = Field( + description="Slice/Service Type", + examples=[1, 2, 3, 4], + ge=0, + le=255, + ) + sd: Optional[int] = Field( + description="Slice Differentiator", + default=None, + examples=[1], + ge=0, + le=16777215, + ) + class ProviderSchema(DataBagSchema): """Provider schema for the fiveg_rfsim interface.""" @@ -30,4 +47,4 @@ class ProviderSchema(DataBagSchema): class RequirerSchema(DataBagSchema): - """Requirer schema for the fiveg_rfsim interface.""" \ No newline at end of file + """Requirer schema for the fiveg_rfsim interface.""" diff --git a/interfaces/tempo_cluster/v1/README.md b/interfaces/tempo_cluster/v1/README.md new file mode 100644 index 00000000..a3c52953 --- /dev/null +++ b/interfaces/tempo_cluster/v1/README.md @@ -0,0 +1,54 @@ +# `tempo_cluster` + +## Usage + +`tempo_cluster` is an interface meant to exchange cluster configuration in distributed Tempo deployments. +Multiple [Tempo worker](https://github.com/canonical/tempo-worker-k8s-operator) applications can relate to a [Tempo coordinator](https://github.com/canonical/tempo-coordinator-k8s-operator) application over the `tempo_cluster` interface, and send their role and topology, in order to join the cluster. +The coordinator will use the same relation to convey to the workers back the configuration that they must run with. + +## Direction + +This interface implements a provider/requirer pattern. The coordinator charm is the provider of the relation, the worker charm is the requirer. Information flows back and forth: first the requirer shares some data necessary for the coordinator to know the role of the worker, then the provider replies back with the configuration it should run with. + +```mermaid +flowchart TD + Requirer -- Role, JujuTopology --> Provider + Provider -- Config --> Requirer +``` + +## Behavior + +### Provider +The provider is expected to... +- update the gossip rings in all configurations with the addresses of all worker units that are joining the cluster (regardless of their role). +- share the exact same configuration to all nodes, regardless of the role they declare, via application databag. + +### Requirer +The requirer application is expected to... +- publish its role as soon as possible via application databag. +Each requirer unit is expected to... +- publish its address and JujuTopology as soon as possible, via unit databag. + +## Relation Data + +[\[Pydantic Schema\]](./schema.py) + +#### Example +```yaml +provider: + app: + worker_config: + # + unit: {} + +requirer: + app: + role: receiver + unit: + juju_topology: + model: "mymodel", + model_uuid: "1231234120941234", + application: "tempo-receiver", + charm_name: "tempo-worker-k8s", + unit: "tempo-receiver/2", +``` diff --git a/interfaces/tempo_cluster/v1/interface.yaml b/interfaces/tempo_cluster/v1/interface.yaml new file mode 100644 index 00000000..71670dbd --- /dev/null +++ b/interfaces/tempo_cluster/v1/interface.yaml @@ -0,0 +1,18 @@ +name: tempo_cluster +internal: true +version: 1 +status: published + +providers: + - name: tempo-coordinator-k8s + url: https://github.com/canonical/tempo-coordinator-k8s-operator + test_setup: + location: tests/interface/conftest.py + identifier: cluster_tester + +requirers: + - name: tempo-worker-k8s + url: https://github.com/canonical/tempo-worker-k8s-operator + +maintainer: observability + diff --git a/interfaces/tempo_cluster/v1/interface_tests/test_provider.py b/interfaces/tempo_cluster/v1/interface_tests/test_provider.py new file mode 100644 index 00000000..752e68bf --- /dev/null +++ b/interfaces/tempo_cluster/v1/interface_tests/test_provider.py @@ -0,0 +1,95 @@ +# Copyright 2024 Canonical +# See LICENSE file for licensing details. +import json + +from interface_tester.interface_test import Tester +from scenario import Relation, State + + +def test_validation_fails_with_missing_role(): + tester = Tester( + state_in=State( + relations=[ + Relation( + endpoint="tempo_cluster", + interface="tempo_cluster", + remote_app_name="worker", + remote_app_data={}, + remote_units_data={ + 0: { + "juju_topology": json.dumps( + { + "application": "worker", + "unit": "worker/0", + "charm_name": "worker", + } + ), + "address": json.dumps("192.0.2.1"), + } + }, + ) + ] + ) + ) + tester.run("tempo-cluster-relation-created") + tester.assert_relation_data_empty() + + +def test_validation_succeeds_on_joining_with_role(): + tester = Tester( + state_in=State( + relations=[ + Relation( + endpoint="tempo_cluster", + interface="tempo_cluster", + remote_app_name="worker", + remote_app_data={ + "role": json.dumps("all"), + }, + remote_units_data={ + 0: { + "juju_topology": json.dumps( + { + "application": "worker", + "unit": "worker/0", + "charm_name": "worker", + } + ), + "address": json.dumps("192.0.2.1"), + } + }, + ), + ] + ) + ) + tester.run("tempo-cluster-relation-joined") + tester.assert_schema_valid() + + +def test_validation_fails_on_joining_with_invalid_role(): + tester = Tester( + state_in=State( + relations=[ + Relation( + endpoint="tempo_cluster", + interface="tempo_cluster", + remote_app_name="worker", + remote_app_data={"role": json.dumps("imposter")}, + remote_units_data={ + 0: { + "juju_topology": json.dumps( + { + "application": "worker", + "unit": "worker/0", + "charm_name": "worker", + } + ), + "address": json.dumps("192.0.2.1"), + } + }, + ) + ] + ) + ) + tester.run("tempo-cluster-relation-joined") + tester.assert_relation_data_empty() diff --git a/interfaces/tempo_cluster/v1/interface_tests/test_requirer.py b/interfaces/tempo_cluster/v1/interface_tests/test_requirer.py new file mode 100644 index 00000000..dbceb9c5 --- /dev/null +++ b/interfaces/tempo_cluster/v1/interface_tests/test_requirer.py @@ -0,0 +1,31 @@ +# Copyright 2024 Canonical +# See LICENSE file for licensing details. + +import json + +from interface_tester.interface_test import Tester +from scenario import Relation, State + + +def test_data_on_created(): + tester = Tester( + state_in=State( + relations=[ + Relation( + endpoint="tempo_cluster", + interface="tempo_cluster", + remote_app_name="coordinator", + remote_app_data={"worker_config": json.dumps("foo: bar")}, + charm_tracing_receivers={ + "otlp_http": "http://192.0.2.1:4318", + }, + workload_tracing_receivers={ + "otlp_http": "http://192.0.2.2:4318", + "otlp_grpc": "192.0.2.2:4317", + }, + ) + ] + ) + ) + tester.run("tempo-cluster-relation-created") + tester.assert_schema_valid() diff --git a/interfaces/tempo_cluster/v1/schema.py b/interfaces/tempo_cluster/v1/schema.py new file mode 100644 index 00000000..c823f4d3 --- /dev/null +++ b/interfaces/tempo_cluster/v1/schema.py @@ -0,0 +1,93 @@ +"""This file defines the schemas for the provider and requirer sides of this relation interface. + +It must expose two interfaces.schema_base.DataBagSchema subclasses called: +- ProviderSchema +- RequirerSchema +""" +from enum import Enum +from typing import Optional, Dict, List + +from interface_tester.schema_base import DataBagSchema +from pydantic import BaseModel, Field, Json + + +class TempoClusterProviderAppData(BaseModel): + """TempoClusterProviderAppData.""" + worker_config: Json[str] = Field( + description="The tempo configuration that the requirer should run with." + "Yaml-encoded. Must conform to the schema that the presently deployed " + "workload version supports; for example see: " + "https://grafana.com/docs/tempo/latest/configuration/#configure-tempo." + ) + loki_endpoints: Optional[Json[Dict[str, str]]] = Field( + default=None, + description="List of loki-push-api endpoints to which the worker node can push any logs it generates.") + ca_cert: Optional[Json[str]] = Field(default=None, description="CA certificate for tls encryption.") + server_cert: Optional[Json[str]] = Field(default=None, description="Server certificate for tls encryption.") + privkey_secret_id: Optional[Json[str]] = Field( + default=None, + description="ID of a Juju secret that holds the private key used by the coordinator for TLS encryption." + ) + remote_write_endpoints: Optional[Json[List[Dict[str, str]]]] = Field( + default=None, + description="Endpoints to which the workload (and the worker charm) can push metrics to." + ) + charm_tracing_receivers: Optional[Json[Dict[str, str]]] = Field( + default=None, + description="Endpoints to which the worker node can push its charm traces to." + "It is a mapping from protocol names such as `zipkin`, `otlp_grpc`, `otlp_http`." + ) + workload_tracing_receivers: Optional[Json[Dict[str, str]]] = Field( + default=None, + description="Endpoints to which the worker node can push its workload traces to." + "It is a mapping from protocol names such as `zipkin`, `otlp_grpc`, `otlp_http`." + ) + + +class _Topology(BaseModel): + """JujuTopology as defined by cos-lib.""" + application: str + charm_name: Optional[str] + unit: Optional[str] + + +class TempoClusterRequirerUnitData(BaseModel): + """TempoClusterRequirerUnitData.""" + + juju_topology: Json[_Topology] + address: Json[str] + + +class TempoRole(str, Enum): + """Tempo component role names. + + References: + arch: + -> https://grafana.com/docs/tempo/latest/operations/architecture/ + config: + -> https://grafana.com/docs/tempo/latest/configuration/#server + """ + ALL = "all" # default, meta-role. gets remapped to scalable-single-binary by the worker. + QUERIER = "querier" + QUERY_FRONTEND = "query-frontend" + INGESTER = "ingester" + DISTRIBUTOR = "distributor" + COMPACTOR = "compactor" + METRICS_GENERATOR = "metrics-generator" + + +class TempoClusterRequirerAppData(BaseModel): + """TempoClusterRequirerAppData.""" + + role: Json[TempoRole] + + +class ProviderSchema(DataBagSchema): + """The schema for the provider side of this interface.""" + app: TempoClusterProviderAppData + + +class RequirerSchema(DataBagSchema): + """The schema for the requirer side of this interface.""" + app: TempoClusterRequirerAppData + unit: TempoClusterRequirerUnitData diff --git a/interfaces/tracing/v2/interface_tests/test_provider.py b/interfaces/tracing/v2/interface_tests/test_provider.py index 8a065c00..8e69ce36 100644 --- a/interfaces/tracing/v2/interface_tests/test_provider.py +++ b/interfaces/tracing/v2/interface_tests/test_provider.py @@ -5,6 +5,19 @@ from interface_tester.interface_test import Tester from scenario import State, Relation +_VALID_REQUIRER_APP_DATA = {"receivers": json.dumps( + [ + { + "protocol": { + "name": "otlp_grpc", + "type": "grpc" + }, + "url": "http://192.0.2.0/24" + } + ] +) +} + def test_data_on_created(): tester = Tester( @@ -14,9 +27,7 @@ def test_data_on_created(): endpoint='tracing', interface='tracing', remote_app_name='remote', - remote_app_data={ - "receivers": json.dumps(["otlp_grpc"]) - } + remote_app_data=_VALID_REQUIRER_APP_DATA ) ] ) @@ -33,10 +44,7 @@ def test_data_on_joined(): endpoint='tracing', interface='tracing', remote_app_name='remote', - remote_app_data={ - "receivers": json.dumps(["otlp_grpc"]) - } - ) + remote_app_data=_VALID_REQUIRER_APP_DATA ) ] ) ) @@ -52,10 +60,7 @@ def test_data_on_changed(): endpoint='tracing', interface='tracing', remote_app_name='remote', - remote_app_data={ - "receivers": json.dumps(["otlp_grpc"]) - } - ) + remote_app_data=_VALID_REQUIRER_APP_DATA ) ] ) ) diff --git a/utils/interface-validator.py b/utils/interface-validator.py index b37e4138..f32ad590 100644 --- a/utils/interface-validator.py +++ b/utils/interface-validator.py @@ -68,7 +68,7 @@ def _validate_against_path(self, file, model): if model.name != result.group(1): raise MatchError(f"name '{model.name}' does not match folder structure '{result.group(1)}'") if model.version != int(result.group(2)): - raise MatchError("version ({result.group(2)}) does not match folder structure") + raise MatchError(f"version ({result.group(2)}) does not match folder structure") """Runs the validation against all interface definitions.""" def run(self):