diff --git a/README.md b/README.md
index 516b92a1a318ab925e7ba2a5aa8292d2b70f60b3..2f18c63af973559faa98387b73c023b557ade723 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
:zap: **New! Runtime [Sequence](https://interdigitalinc.github.io/AdvantEDGE/docs/usage/gui/exec-view/#sequence-diagram) and [Data Flow](https://interdigitalinc.github.io/AdvantEDGE/docs/usage/gui/exec-view/#data-flow-diagram) diagrams :chart_with_upwards_trend:**
-:zap: **Service API upgrade to version 3.2.1 for [ETSI MEC011](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#edge-platform-application-enablement-service), [ETSI MEC012](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#radio-network-information-service), [ETSI MEC013](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#location-service), [ETSI MEC021](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#application-mobility-service), [ETSI MEC030](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#v2x-information-service), [ETSI MEC033](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#iot-api) and [ETSI MEC040](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#mec-federation-service) :arrow_up:**
+:zap: **Service API upgrade to version 3.2.1 for [ETSI MEC011](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#edge-platform-application-enablement-service), [ETSI MEC012](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#radio-network-information-service), [ETSI MEC013](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#location-service), [ETSI MEC021](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#application-mobility-service), [ETSI MEC030](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#v2x-information-service) :arrow_up:**
:zap: **[Runtime Setup](https://interdigitalinc.github.io/AdvantEDGE/docs/setup/env-runtime/) updates to support k8s versions up to 1.26 :arrow_up:**
@@ -26,7 +26,7 @@
:zap: **New edge native service: [ETSI MEC040 - MEC Federation API](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#mec-federation-service)**
-:zap: **Service API upgrade to MEC Phase 3 for [ETSI MEC011](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#edge-platform-application-enablement-service), [ETSI MEC012](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#radio-network-information-service), [ETSI MEC013](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#location-service), [ETSI MEC021](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#application-mobility-service), [ETSI MEC030](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#v2x-information-service), [ETSI MEC033](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#iot-api) and [ETSI MEC040](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#mec-federation-service) :arrow_up:**
+:zap: **Service API upgrade to MEC Phase 3 for [ETSI MEC011](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#edge-platform-application-enablement-service), [ETSI MEC012](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#radio-network-information-service), [ETSI MEC013](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#location-service), [ETSI MEC021](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#application-mobility-service), [ETSI MEC030](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#v2x-information-service), [ETSI MEC033](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#iot-api), [ETSI MEC040](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#mec-federation-service) and [ETSI MEC046](https://interdigitalinc.github.io/AdvantEDGE/docs/overview/edge-services/#mec-sensors-sharing) :arrow_up:**
:zap: **New command line api to develop MEC application/service without GUI**
diff --git a/examples/demo6/golang/app_instance.yaml b/examples/demo6/golang/app_instance.yaml
index d71965f945ee17e9dfbf4700ce299bb69fd94281..79036f8c125c18c0964422ed7efe7fe524989b41 100644
--- a/examples/demo6/golang/app_instance.yaml
+++ b/examples/demo6/golang/app_instance.yaml
@@ -3,7 +3,7 @@
# Set where mec application is running either on MEC Sandbox or AdvantEDGE. Expected fields: sandbox | advantedge
mode: 'sandbox'
# Set MEC plateform address
-sandbox: 'mec-platform2.etsi.org'
+sandbox: 'try-mec.etsi.org'
# Set if sandbox url uses https. Expected fields: true | false
https: true
# Set the mec platform name demo-6 will run on. Example field: mep1
diff --git a/examples/demo6/golang/main.go b/examples/demo6/golang/main.go
index e7d5c72811fe2b7474b27db50eb9c777f96ff109..5f1af3b19d1a33ed0f25532d63f01f091062a254 100644
--- a/examples/demo6/golang/main.go
+++ b/examples/demo6/golang/main.go
@@ -264,7 +264,7 @@ type UeContext struct {
var (
dir string
fileName string
- provider string = "Jupyter2024" //"github"
+ provider string = "github" //"Jupyter2024"
run bool = true
done chan bool
cfg *client.Configuration = nil
diff --git a/examples/demo7/Dockerfile b/examples/demo7/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..a5a306ab06bc7f6eca06cfef48880664a8742b30
--- /dev/null
+++ b/examples/demo7/Dockerfile
@@ -0,0 +1,22 @@
+# Copyright (c) 2022 The AdvantEDGE Authors
+#
+# 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.
+
+FROM ubuntu:22.04
+
+COPY ./fsmsggen /fsmsggen
+COPY ./entrypoint.sh /entrypoint.sh
+
+RUN apt-get update && apt-get install -y curl libjson-c-dev libgps-dev libpcap-dev libssl-dev && chmod +x /entrypoint.sh && chmod +x /fsmsggen
+
+ENTRYPOINT ["/entrypoint.sh"]
diff --git a/examples/demo7/README.md b/examples/demo7/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..5fdf3ed024568413b5add6c375063abcfa61da27
--- /dev/null
+++ b/examples/demo7/README.md
@@ -0,0 +1,4 @@
+# Demo7
+Demo7 showcases the V2X capabilities of the platform.
+It is a C-V2X OBU simulator taht can be used as MEC application
+
diff --git a/examples/demo7/dockerize.sh b/examples/demo7/dockerize.sh
new file mode 100755
index 0000000000000000000000000000000000000000..23af55866260067711539ca4761c0d84b8abc3b3
--- /dev/null
+++ b/examples/demo7/dockerize.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+
+# Get full path to script directory
+SCRIPT=$(readlink -f "$0")
+BASEDIR=$(dirname "$SCRIPT")
+
+echo ""
+echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"
+echo ">>> Dockerizing fsmsggen"
+echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"
+echo ""
+
+# Dockerize demo
+docker build --no-cache --rm -t meep-docker-registry:30001/fsmsggen .
+docker push meep-docker-registry:30001/fsmsggen
+
+
+echo ""
+echo ">>> fsmsggen dockerize completed"
+
+
+
+
diff --git a/examples/demo7/entrypoint.sh b/examples/demo7/entrypoint.sh
new file mode 100755
index 0000000000000000000000000000000000000000..6e6495e9fdeac1bc446e4e8c8e26aacc4254bc64
--- /dev/null
+++ b/examples/demo7/entrypoint.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+#
+# Here are some examples of env. variable values:
+# COMM: [--iface eth0|--mode 2]
+#
+# GOBAL: --verbose --srcaddr 00:01:02:03:04:05 --no-sec true
+#
+# SEC_MODE: --no-sec true
+#
+# BEACON: --no-sec-beacon true
+#
+# CAM: --cam-station-type passengerCar --cam-station-id 12345 --cam-no-sec
+#
+# DENM
+#
+# VERBOSE: --verbose
+
+# E.g. yann@yann-linux:~/frameworks/fsmsggen$ sudo ./build/x86_64-linux-gnu-d/fsmsggen --iface wlo1 --verbose --srcaddr 00:01:02:03:04:05 --no-sec true --cam-station-type passengerCar --cam-station-id 12345 --cam-no-sec --out ./out.pcap
+
+
+set -e
+set +vx
+
+echo "Starting '/fsmsggen ${COMM} ${GOBAL} ${SEC_MODE} ${COMM_MODE} ${BEACON} ${CAM} ${DENM} ${VERBOSE}'"
+/fsmsggen ${COMM} ${GOBAL} ${SEC_MODE} ${BEACON} ${CAM} ${DENM} ${VERBOSE}
\ No newline at end of file
diff --git a/examples/demo7/fsmsggen b/examples/demo7/fsmsggen
new file mode 100644
index 0000000000000000000000000000000000000000..c93135604ba829e024062ee35114fe0496126e36
Binary files /dev/null and b/examples/demo7/fsmsggen differ
diff --git a/examples/demo8/CAPIF_And_ETSI_MEC_Tutorial.ipynb b/examples/demo8/CAPIF_And_ETSI_MEC_Tutorial.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..b1349992751731795778f45847f664b78b4525bc
--- /dev/null
+++ b/examples/demo8/CAPIF_And_ETSI_MEC_Tutorial.ipynb
@@ -0,0 +1,2158 @@
+{
+ "nbformat": 4,
+ "nbformat_minor": 0,
+ "metadata": {
+ "colab": {
+ "private_outputs": true,
+ "provenance": [],
+ "toc_visible": true
+ },
+ "kernelspec": {
+ "name": "python3",
+ "display_name": "Python 3"
+ },
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "source": [
+ "# Using ETSI MEC profile of CAPIF in CAPIF application\n",
+ "\n",
+ "## Introduction\n",
+ "\n",
+ "3GPP CAPIF (Common API Framework) is a standardized API management framework designed to enable a unified northbound API approach across 3GPP network functions (see 3GPP TS 23.222 version 18.6.0 Release 18/ETSI TS 123 222 V18.6.0 (2024-06) and 3GPP TS 29.222 version 18.6.0 Release 18/ETSI TS 129 222 V18.6.0 (2022-06)).\n",
+ "\n",
+ "This tutorial introduces the step by step procedure to create a basic CAPIF application to exploit the ETSI MEC CAPIF profile as described in ETSI GS MEC 011 (V3.2.1) Clause 9.\n",
+ "It uses the ETSI MEC Sandbox simulator.\n",
+ "\n",
+ "
\n",
+ " Note: These source code examples are simplified and ignore return codes and error checks to a large extent. We do this to highlight how to use the MEC Sandbox API and the different MEC satndards and reduce unrelated code.\n",
+ "A real-world application will of course properly check every return value and exit correctly at the first serious error.\n",
+ "
\n"
+ ],
+ "metadata": {
+ "id": "44TomlvPCGTe"
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## The basics of developing a MEC application\n",
+ "\n",
+ "\n",
+ "\n",
+ " Note: The sub-paragraph 'Putting everything together' is a specific paragraph where all the newly features introduced in the main paragraph are put together to create an executable block of code. It is possible to skip this block of code by removing the comment character (#) on first line of this block of code.\n",
+ "
\n",
+ "\n",
+ "Before going to create our CAPIF application skeleton, the following steps shall be done:\n",
+ "\n",
+ "1) Apply the python imports"
+ ],
+ "metadata": {
+ "id": "4DpxwmiomELg"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "from __future__ import division # Import floating-point division (1/4=0.25) instead of Euclidian division (1/4=0)\n",
+ "\n",
+ "import os\n",
+ "import sys\n",
+ "import re\n",
+ "import logging\n",
+ "import threading\n",
+ "import time\n",
+ "import json\n",
+ "import uuid\n",
+ "import base64\n",
+ "\n",
+ "import pprint\n",
+ "\n",
+ "import requests\n",
+ "\n",
+ "from http import HTTPStatus\n",
+ "from http.server import BaseHTTPRequestHandler, HTTPServer\n",
+ "\n",
+ "try:\n",
+ " import urllib3\n",
+ "except ImportError:\n",
+ " raise ImportError('Swagger python client requires urllib3.')\n"
+ ],
+ "metadata": {
+ "id": "1gjo-NM6hD1k"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "The following imports are required to support the security aspects such as certificates management, signatures..."
+ ],
+ "metadata": {
+ "id": "j9wDIe9IEUQz"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "from OpenSSL.SSL import FILETYPE_PEM\n",
+ "from OpenSSL.crypto import (dump_certificate_request, dump_privatekey, load_publickey, PKey, TYPE_RSA, X509Req, dump_publickey)\n"
+ ],
+ "metadata": {
+ "id": "xb4ReBZZEVLB"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "2) Initialize of the global constants (cell 3)"
+ ],
+ "metadata": {
+ "id": "DrPJzD14nLas"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "REGISTER_HOSTNAME = 'lab-oai.etsi.org' # capif-prev.mobilesandbox.cloud\n",
+ "REGISTER_PORT = 31120 # 36212\n",
+ "REGISTER_USER = 'admin' # Basic AUTH for registration\n",
+ "REGISTER_PASSWORD = 'password123' # Basic AUTH for registration\n",
+ "\n",
+ "CAPIF_HOSTNAME = 'lab-oai.etsi.org'\n",
+ "CAPIF_PORT = 443\n",
+ "\n",
+ "USER_PASSWORD = 'password123'\n",
+ "\n",
+ "TRY_MEC_URL = 'try-mec.etsi.org' # MEC Sandbox URL\n",
+ "TRY_MEC_SESSION_ID = 'sbxgs9x587' # MEC Sandbox identifier\n",
+ "TRY_MEC_PLTF = 'mep1' # MEC Platform identifier (depending of the network scenario loaded)\n",
+ "MEC_APP_INST_ID = 'f1e4d448-e277-496b-bf63-98391cfd20fb' # A MEC application identifier\n"
+ ],
+ "metadata": {
+ "id": "rNibZWiBitPE"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "3) Setup the logger instance and the HTTP REST API (cell 4)"
+ ],
+ "metadata": {
+ "id": "MOa9g-NMnpod"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "# Initialize the logger\n",
+ "logger = logging.getLogger(__name__)\n",
+ "logger.setLevel(logging.DEBUG)\n",
+ "logging.basicConfig(filename='/tmp/' + time.strftime('%Y%m%d-%H%M%S') + '.log')\n",
+ "l = logging.StreamHandler()\n",
+ "l.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))\n",
+ "logger.addHandler(l)\n"
+ ],
+ "metadata": {
+ "id": "-cuxWhfantSw"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "4) Setup the global variables (cell 5)"
+ ],
+ "metadata": {
+ "id": "D67Aq0vujB0q"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "# Initialize the global variables\n",
+ "ca_root = \"\" # The CAPIF root certificate\n",
+ "ccf_api_onboarding_url = \"\" #\n",
+ "ccf_publish_url = \"\" # The CAPIF publish API endpoint\n",
+ "ccf_discover_url = \"\" # The CAPIF discovery endpoint\n",
+ "ccf_security_url = \"\" # The CAPIF security endpoint\n",
+ "ccf_onboarding_url = \"\" # The CAPIF onboarding endpoint\n"
+ ],
+ "metadata": {
+ "id": "7RC7UY-0oACq"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "To enable the Automatic Debugger Calling, uncomment the code bellow."
+ ],
+ "metadata": {
+ "id": "2YvSVMClhPJT"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "#!pip install ipdb\n",
+ "#import ipdb\n",
+ "#%pdb on\n",
+ "# Use the command ipdb.set_trace() to set a breakpoint"
+ ],
+ "metadata": {
+ "id": "OQjYWHgnYM4G"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Create our first CAPIF application\n",
+ "\n",
+ "The first step to develop a MEC application is to create the application skeleton which contains the minimum steps below:\n",
+ "\n",
+ "- Login to instanciate a MEC Sandbox\n",
+ "- Logout to delete a existing MEC Sandbox"
+ ],
+ "metadata": {
+ "id": "1fMmXWk9jLDX"
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "#### Login\n",
+ "\n",
+ "The login operation is required by ETSI TS 123 222 V18.6.0 (2024-06) Clause 4.5 Operations, Administration and Maintenance but is out of the scope of ETSI TS 129 222 V18.6.0 (2022-06)."
+ ],
+ "metadata": {
+ "id": "rtAVXZayoQRx"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "def process_login() -> tuple:\n",
+ " \"\"\"\n",
+ " Logs in to the CAPIF server.\n",
+ " :return A dictionary containing the login response, or None if login fails\n",
+ " \"\"\"\n",
+ " global logger\n",
+ "\n",
+ " logger.debug('>>> process_login')\n",
+ "\n",
+ " try:\n",
+ " url = 'https://' + REGISTER_HOSTNAME + ':' + str(REGISTER_PORT) + '/login'\n",
+ " logger.debug('process_login: url=' + url)\n",
+ " auth_string = f\"{REGISTER_USER}:{REGISTER_PASSWORD}\"\n",
+ " encoded_auth = base64.b64encode(auth_string.encode('utf-8')).decode('utf-8')\n",
+ " headers = {'Content-Type': 'application/json', 'Authorization': f'Basic {encoded_auth}'}\n",
+ " logger.debug('process_login (step1): headers: ' + str(headers))\n",
+ " response = requests.post(url, headers=headers, verify=False)\n",
+ " response.raise_for_status() # Raise an exception for bad status codes\n",
+ " logger.debug('process_login (step2): result: ' + str(response.json()))\n",
+ " if response.status_code != 200:\n",
+ " logger.error(f\"Error creating user: {response.status_code} - {response.text}\")\n",
+ " return None, None\n",
+ " tokens = json.loads(response.text)\n",
+ " return tokens['refresh_token'], tokens['access_token']\n",
+ " except requests.exceptions.RequestException as e:\n",
+ " logger.error(f\"Login failed: {e}\")\n",
+ "\n",
+ " return None, None\n",
+ " # End of function process_login"
+ ],
+ "metadata": {
+ "id": "Ad8g1no-pH7i"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Logout\n",
+ "\n",
+ "The logout operation is required by ETSI TS 123 222 V18.6.0 (2024-06) Clause 4.5 Operations, Administration and Maintenance but is out of the scope of ETSI TS 129 222 V18.6.0 (2022-06)."
+ ],
+ "metadata": {
+ "id": "8Cw5MBc-st1e"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "def process_logout():\n",
+ " \"\"\"\n",
+ " Logs out from the CAPIF server\n",
+ " Nothing to do\n",
+ " \"\"\"\n",
+ " pass\n",
+ " # End of function process_logout"
+ ],
+ "metadata": {
+ "id": "XmyLOuFasuvU"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Putting everything together\n",
+ "Now, it is time now to create the our first iteration of our CAPIF/MEC application. Here the logic is:\n",
+ "* Login\n",
+ "* Print obtained tokens\n",
+ "* Logout\n",
+ "* Check that logout is effective"
+ ],
+ "metadata": {
+ "id": "mCKT-ntspnsM"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "%%script echo skipping\n",
+ "# Uncomment the line above to skip execution of this cell\n",
+ "def process_main():\n",
+ " \"\"\"\n",
+ " This is the first sprint of our CAPIF application:\n",
+ " - Login\n",
+ " - Print obtained tokens\n",
+ " - Logout\n",
+ " - Check that logout is effective\n",
+ " \"\"\"\n",
+ " global logger\n",
+ "\n",
+ " logger.debug('Starting at ' + time.strftime('%Y%m%d-%H%M%S'))\n",
+ " logger.debug('\\t pwd= ' + os.getcwd())\n",
+ "\n",
+ " # Login\n",
+ " refresh_token, admin_token = process_login()\n",
+ " if refresh_token is None:\n",
+ " return\n",
+ "\n",
+ " # Print obtained tokens\n",
+ " logger.debug(\"Login successful: admin_token=\" + admin_token)\n",
+ "\n",
+ " # Logout\n",
+ " process_logout()\n",
+ "\n",
+ " # Check that logout is effective\n",
+ " logger.debug('To check that logout is effective')\n",
+ "\n",
+ " logger.debug('Stopped at ' + time.strftime('%Y%m%d-%H%M%S'))\n",
+ " # End of function process_main\n",
+ "\n",
+ "if __name__ == '__main__':\n",
+ " process_main()\n"
+ ],
+ "metadata": {
+ "id": "XYC8PnDUpvui"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Create the API Provider\n",
+ "\n",
+ "The next step is to create a new user associated to our CAPIF application to obtain a user UUID. It will be used to genereate ceertificates to be used during TLS mutual authentication and API onboarding and offboarding for instance.\n",
+ "\n",
+ "**Note:** It is required by ETSI TS 123 222 V18.6.0 (2024-06) Clause 4.5 Operations, Administration and Maintenance but is out of the scope of ETSI TS 129 222 V18.6.0 (2022-06).\n"
+ ],
+ "metadata": {
+ "id": "rTcvGY5T1pZJ"
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Creating a new user\n",
+ "\n",
+ "The cell below provides an implementation for this user creation.\n",
+ "\n",
+ "**Note:** To improve this code, the user profile shlould be fully parametrized."
+ ],
+ "metadata": {
+ "id": "ysxZ8sIiLLgw"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "def create_user(p_admin_token: str) -> tuple:\n",
+ " \"\"\"\n",
+ " Creates a new user.\n",
+ " :return: The user UUID on success, None otherwise\n",
+ " \"\"\"\n",
+ " global logger\n",
+ "\n",
+ " logger.debug('>>> create_user')\n",
+ "\n",
+ " try:\n",
+ " user_name = str(uuid.uuid1())\n",
+ " url = 'https://' + REGISTER_HOSTNAME + ':' + str(REGISTER_PORT) + '/createUser'\n",
+ " logger.debug('create_user: url=' + url)\n",
+ " headers = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + p_admin_token}\n",
+ " logger.debug('create_user (step1): headers: ' + str(headers))\n",
+ " data = {\n",
+ " 'username': user_name,\n",
+ " 'password': USER_PASSWORD,\n",
+ " 'enterprise': 'ETSI',\n",
+ " 'country': 'France',\n",
+ " 'email': 'ocf@etsi.org',\n",
+ " 'purpose': 'Tutorial on MEC/OpenCAPIF',\n",
+ " 'phone_number': \"+330405060708\",\n",
+ " 'company_web': 'www.etsi.org',\n",
+ " 'description': 'A step by step procedure to create a basic CAPIF application to exploit the ETSI MEC CAPIF profile'\n",
+ " }\n",
+ " response = requests.post(url, headers=headers, data=json.dumps(data), verify=False)\n",
+ " logger.debug('create_user (step2): response=' + str(response))\n",
+ " response.raise_for_status() # Raise an exception for bad status codes\n",
+ " if response.status_code != 201:\n",
+ " logger.error(f\"Error creating user: {response.status_code} - {response.text}\")\n",
+ " return ()\n",
+ " tokens = json.loads(response.text)\n",
+ " return (user_name, tokens['uuid'])\n",
+ " except requests.exceptions.RequestException as e:\n",
+ " logger.error(f\"Error creating user: {e}\")\n",
+ "\n",
+ " return ()\n",
+ " # End of function create_user"
+ ],
+ "metadata": {
+ "id": "Jq-9_sLI8WgW"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Deleting an existing User\n",
+ "\n",
+ "Before to terminate our CAPIF application, we have to clean up the resources. So, a function to delete a created user is required."
+ ],
+ "metadata": {
+ "id": "Ut3CLrRUFT5o"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "def delete_user(p_user_uuid: str, p_admin_token: str) -> int:\n",
+ " \"\"\"\n",
+ " Deletes a user.\n",
+ " :param p_user_uuid: The user UUID\n",
+ " :return: 0 on success, -1 otherwise\n",
+ " \"\"\"\n",
+ " global logger\n",
+ "\n",
+ " logger.debug('>>> delete_user')\n",
+ "\n",
+ " try:\n",
+ " url = 'https://' + REGISTER_HOSTNAME + ':' + str(REGISTER_PORT) + '/deleteUser/' + p_user_uuid\n",
+ " logger.debug('delete_user: url=' + url)\n",
+ " headers = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + p_admin_token}\n",
+ " response = requests.delete(url, headers=headers, verify=False)\n",
+ " logger.debug('delete_user: response=' + str(response))\n",
+ " response.raise_for_status() # Raise an exception for bad status codes\n",
+ " return 0\n",
+ " except requests.exceptions.RequestException as e:\n",
+ " logger.error(f\"Error creating user: {e}\")\n",
+ "\n",
+ " return -1\n",
+ " # End of function delete_user"
+ ],
+ "metadata": {
+ "id": "WRIdwNMNFrdC"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Putting everything together\n",
+ "It is time now to create the our second iteration of our CAPIF/MEC application.\n",
+ "\n",
+ "The sequence is the following:\n",
+ "* Login\n",
+ "* Print obtained tokens\n",
+ "* Create a new user\n",
+ "* Print the user UUID\n",
+ "* Delete the newly created user\n",
+ "* Logout\n"
+ ],
+ "metadata": {
+ "id": "IAh9tN25-82V"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "%%script echo skipping\n",
+ "# Uncomment the line above to skip execution of this cell\n",
+ "def process_main():\n",
+ " \"\"\"\n",
+ " This is the second sprint of our CAPIF/MEC application:\n",
+ " - Login\n",
+ " - Print obtained tokens\n",
+ " - Create a new user\n",
+ " - Print the user UUID\n",
+ " - Delete the newly created user\n",
+ " - Logout\n",
+ " \"\"\"\n",
+ " global logger\n",
+ "\n",
+ " logger.debug('Starting at ' + time.strftime('%Y%m%d-%H%M%S'))\n",
+ " logger.debug('\\t pwd= ' + os.getcwd())\n",
+ "\n",
+ " # Login\n",
+ " refresh_token, admin_token = process_login()\n",
+ " if refresh_token is None:\n",
+ " return\n",
+ "\n",
+ " # Print obtained tokens\n",
+ " logger.debug(\"Login successful: admin_token=\" + admin_token)\n",
+ "\n",
+ " # Create a new user\n",
+ " user_name, user_uuid = create_user(admin_token)\n",
+ " if len(user_uuid) == 0:\n",
+ " return\n",
+ "\n",
+ " # Print User UUID\n",
+ " logger.debug(\"User successfully created: user_uuid=\" + user_uuid)\n",
+ "\n",
+ " time.sleep(5) # Sleep for 5 seconds\n",
+ "\n",
+ " # Delete the newly created user\n",
+ " delete_user(user_uuid, admin_token)\n",
+ "\n",
+ " # Logout\n",
+ " process_logout()\n",
+ "\n",
+ " logger.debug('Stopped at ' + time.strftime('%Y%m%d-%H%M%S'))\n",
+ " # End of function process_main\n",
+ "\n",
+ "if __name__ == '__main__':\n",
+ " process_main()\n"
+ ],
+ "metadata": {
+ "id": "1M_x2I1B_Crp"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Getting security materials\n",
+ "\n",
+ "The purpose is to retrieves peer certificates for the TLS mutual authentication purpose and the JWT token to onboarding and offboarding APIs.\n",
+ "The following information is retrived:\n",
+ "- The root certificate\n",
+ "- An access token which will be used for onboarding and offboarding APIs\n",
+ "- The URLs for the different CAPIF endpoints:\n",
+ " * API onbording endpoint\n",
+ " * API discovery endpoint\n",
+ " * API publish endpoint\n",
+ " * Security endpoint\n",
+ "\n",
+ "This operation needs the user name and the user password used in previous [chapter](#create_the_invoker_/_provider).\n"
+ ],
+ "metadata": {
+ "id": "f896qBJOjMuz"
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Getting certificates"
+ ],
+ "metadata": {
+ "id": "lC2JAah7LWLp"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "def get_auth(p_user_name: str, p_user_password: str) -> dict:\n",
+ " \"\"\"\n",
+ " Gets the authentication information.\n",
+ " :param The user name\n",
+ " :param The user password\n",
+ " :return A dictionary containing the authentication information on success, or an empty dictionary otherwise\n",
+ " \"\"\"\n",
+ " global logger\n",
+ "\n",
+ " logger.debug('>>> get_auth')\n",
+ "\n",
+ " try:\n",
+ " url = 'https://' + REGISTER_HOSTNAME + ':' + str(REGISTER_PORT) + '/getauth'\n",
+ " logger.debug('get_auth: url=' + url)\n",
+ " auth_string = f\"{p_user_name}:{p_user_password}\"\n",
+ " encoded_auth = base64.b64encode(auth_string.encode('utf-8')).decode('utf-8')\n",
+ " headers = {'Content-Type': 'application/json', 'Authorization': f'Basic {encoded_auth}'}\n",
+ " logger.debug('get_auth (step1): headers: ' + str(headers))\n",
+ " response = requests.get(url, headers=headers, verify=False)\n",
+ " response.raise_for_status() # Raise an exception for bad status codes\n",
+ " logger.debug('get_auth (step2): result: ' + str(response.json()))\n",
+ " if response.status_code != 200:\n",
+ " logger.error(f\"Error creating user: {response.status_code} - {response.text}\")\n",
+ " return dict()\n",
+ " auth = json.loads(response.text)\n",
+ " return auth\n",
+ " except requests.exceptions.RequestException as e:\n",
+ " logger.error(f\"get_auth failed: {e}\")\n",
+ "\n",
+ " return dict()\n",
+ " # End of function get_auth"
+ ],
+ "metadata": {
+ "id": "1glmqNSRK1cH"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Putting everything together\n",
+ "\n",
+ "Now, it is time now to create the our third iteration of our MEC application. Here the logic is:\n",
+ "\n",
+ "- Login\n",
+ "- Create the user\n",
+ "- Get the information to use CAPIF (security materials & URLs)\n",
+ "- Print the information to use CAPI\n",
+ "- Delete the user\n",
+ "- Logout\n"
+ ],
+ "metadata": {
+ "id": "BUw-VS1WLb7i"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "%%script echo skipping\n",
+ "# Uncomment the ;line above to skip execution of this cell\n",
+ "def process_main():\n",
+ " \"\"\"\n",
+ " This is the first sprint of our skeleton of our CAPIF application:\n",
+ " - Login\n",
+ " - Print obtained tokens\n",
+ " - Create a new user\n",
+ " - Get the information to use CAPIF (security materials & URLs)\n",
+ " - Print the information to use CAPI\n",
+ " - Delete the newly created user\n",
+ " - Logout\n",
+ " \"\"\"\n",
+ " global logger, ca_root, ccf_api_onboarding_url, ccf_publish_url, ccf_discover_url, ccf_security_url, ccf_onboarding_url\n",
+ "\n",
+ " logger.debug('Starting at ' + time.strftime('%Y%m%d-%H%M%S'))\n",
+ " logger.debug('\\t pwd= ' + os.getcwd())\n",
+ "\n",
+ " # Login\n",
+ " refresh_token, admin_token = process_login()\n",
+ " if refresh_token is None:\n",
+ " return\n",
+ "\n",
+ " # Create a new user\n",
+ " user_name, user_uuid = create_user(admin_token)\n",
+ " if len(user_uuid) == 0:\n",
+ " return\n",
+ "\n",
+ " auth = get_auth(user_name, USER_PASSWORD)\n",
+ " if len(auth) == 0:\n",
+ " return\n",
+ "\n",
+ " # Print the authentication information\n",
+ " logger.debug(\"Authentication information=\" + str(auth))\n",
+ " access_token = auth['access_token']\n",
+ " ca_root = auth['ca_root']\n",
+ " ccf_api_onboarding_url = auth['ccf_api_onboarding_url']\n",
+ " ccf_discover_url = auth['ccf_discover_url']\n",
+ " ccf_onboarding_url = auth['ccf_onboarding_url']\n",
+ " ccf_publish_url = auth['ccf_publish_url']\n",
+ " ccf_security_url = auth['ccf_security_url']\n",
+ "\n",
+ " time.sleep(5) # Sleep for 5 seconds\n",
+ "\n",
+ " # Delete the newly created user\n",
+ " delete_user(user_uuid, admin_token)\n",
+ "\n",
+ " # Logout\n",
+ " process_logout()\n",
+ "\n",
+ " logger.debug('Stopped at ' + time.strftime('%Y%m%d-%H%M%S'))\n",
+ " # End of function process_main\n",
+ "\n",
+ "if __name__ == '__main__':\n",
+ " process_main()\n"
+ ],
+ "metadata": {
+ "id": "J002Vuz2OIKl"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Onboarding and offboarding APIs\n"
+ ],
+ "metadata": {
+ "id": "oNhnnDhjjOd7"
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Generate certificates\n",
+ "\n",
+ "Until now, all HTTPS exchanges were done with the the 'verify' attribute of the HTTP reques set to False. It means that the TLS mutual authentication was disabled.\n",
+ "\n",
+ "Fo the process of onboarding and offboarding APIs, the TLS mutual authentication is required. We already got the peer certificate to verify peer but we need to generate our own certificate to be verified by the CAPIF server. This is the purpose of the following functions to generate the public/private keys and generate a CSR and request certificates for each of the three functions AMF, AEF and APF.\n",
+ "\n",
+ "**Refer to:** ETSI TS 129 222 V18.6.0 (2022-06) Clauses 5.11 CAPIF_API_Provider_Management and 8.9 CAPIF_API_Provider_Management_API\n"
+ ],
+ "metadata": {
+ "id": "K6i4ktfM1xFQ"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "def generate_csr(p_cn: str, p_org: str, p_country: str) -> tuple:\n",
+ " \"\"\"\n",
+ " To generate the CSR and generate the dumps\n",
+ " :param p_cn: The common name\n",
+ " :param p_org: The organization\n",
+ " :param p_country: The country\n",
+ " :return: The CSR and the private keys on success, None otherwise\n",
+ " \"\"\"\n",
+ " global logger\n",
+ "\n",
+ " logger.debug('>>> generate_csr')\n",
+ "\n",
+ " # Generate the public/private key\n",
+ " key = PKey()\n",
+ " key.generate_key(TYPE_RSA, 2048)\n",
+ "\n",
+ " # Generate the CSR\n",
+ " req = X509Req()\n",
+ " req.get_subject().CN = p_cn\n",
+ " req.get_subject().O = p_org\n",
+ " req.get_subject().C = p_country\n",
+ " req.set_pubkey(key)\n",
+ " req.sign(key, 'sha256')\n",
+ "\n",
+ " # Generate the dumps\n",
+ " csr_request = dump_certificate_request(FILETYPE_PEM, req)\n",
+ " private_key = dump_privatekey(FILETYPE_PEM, key)\n",
+ " logger.debug('generate_csr: PrivKey: ' + str(private_key))\n",
+ "\n",
+ " return (csr_request, private_key)\n"
+ ],
+ "metadata": {
+ "id": "gEIS3iAH2D4t"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "**Note:** The function above can be improved using parameter for the SHA, and the signature/encryption algorithm."
+ ],
+ "metadata": {
+ "id": "F2-W0a5S3snI"
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Onboard the API provider\n",
+ "\n",
+ "The purpose here is to get certificates from CAPIF in order to export our APIs for the different functions:\n",
+ "- AMF: API Management Function\n",
+ "- AEF: API Exposing Function\n",
+ "- APF: API Publishing Function"
+ ],
+ "metadata": {
+ "id": "1HyqrdUz-uzn"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "def onboard_provider(p_name: str, p_access_token: str) -> dict:\n",
+ " \"\"\"\n",
+ " To onboard the provider.\n",
+ " :param p_name: The name of the provider\n",
+ " :return: A dictionary containing security material for each CAPIF endpoint on success, or an empty dictionary otherwise\n",
+ " \"\"\"\n",
+ " global logger, ccf_api_onboarding_url, access_token\n",
+ "\n",
+ " logger.debug('>>> onboard_provider')\n",
+ "\n",
+ " try:\n",
+ " url = 'https://' + CAPIF_HOSTNAME + ':' + str(CAPIF_PORT) + '/' + ccf_api_onboarding_url\n",
+ " logger.debug('onboard_provider: url=' + url)\n",
+ " headers = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + p_access_token}\n",
+ " logger.debug('onboard_provider (step1): headers: ' + str(headers))\n",
+ " # Build the list of certificate request for the three endpoints\n",
+ " l = []\n",
+ " amf_csr_request, amf_private_key = generate_csr(\"AMF\", \"ETSI\", \"Fr\")\n",
+ " amf_entry = {\n",
+ " 'regInfo': {\n",
+ " 'apiProvPubKey': amf_csr_request.decode(\"utf-8\")\n",
+ " },\n",
+ " 'apiProvFuncRole': 'AMF'\n",
+ " }\n",
+ " l.append(amf_entry)\n",
+ " aef_csr_request, aef_private_key = generate_csr(\"AEF\", \"ETSI\", \"Fr\")\n",
+ " aef_entry = {\n",
+ " 'regInfo': {\n",
+ " 'apiProvPubKey': aef_csr_request.decode(\"utf-8\")\n",
+ " },\n",
+ " 'apiProvFuncRole': 'AEF'\n",
+ " }\n",
+ " l.append(aef_entry)\n",
+ " apf_csr_request, apf_private_key = generate_csr(\"APF\", \"ETSI\", \"Fr\")\n",
+ " apf_entry = {\n",
+ " 'regInfo': {\n",
+ " 'apiProvPubKey': apf_csr_request.decode(\"utf-8\")\n",
+ " },\n",
+ " 'apiProvFuncRole': 'APF'\n",
+ " }\n",
+ " l.append(apf_entry)\n",
+ " # Build the request body\n",
+ " data = {\n",
+ " 'apiProvFuncs': l,\n",
+ " 'apiProvDomInfo': p_name,\n",
+ " 'suppFeat': 'fff',\n",
+ " 'failReason': 'string',\n",
+ " 'regSec': p_access_token\n",
+ " }\n",
+ " logger.debug('onboard_provider (step2): body: ' + str(data))\n",
+ " response = requests.post(url, headers=headers, data=json.dumps(data), verify=False)\n",
+ " response.raise_for_status() # Raise an exception for bad status codes\n",
+ " logger.debug('onboard_provider (step3): result: ' + str(response.json()))\n",
+ " if response.status_code != 201:\n",
+ " logger.error(f\"Error creating user: {response.status_code} - {response.text}\")\n",
+ " return dict()\n",
+ " res = json.loads(response.text)\n",
+ " # Add an entry for CSRs and private keys for future usage\n",
+ " res['csr'] = {\n",
+ " 'amf': [amf_csr_request, amf_private_key],\n",
+ " 'aef': [aef_csr_request, aef_private_key],\n",
+ " 'apf': [apf_csr_request, apf_private_key]\n",
+ " }\n",
+ " return res\n",
+ " except requests.exceptions.RequestException as e:\n",
+ " logger.error(f\"onboard_provider failed: {e}\")\n",
+ "\n",
+ " return dict()\n",
+ " # End of function onboard_provider"
+ ],
+ "metadata": {
+ "id": "6cCn1vKLGe0k"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Offboard the API provider\n",
+ "\n",
+ "The purpose is to offboard the API provider from the CAPIF server. Here, the certificate and the private key of the AMF endpoint are required (TLS mutual authentication)."
+ ],
+ "metadata": {
+ "id": "yP6ZytijFxKG"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "def offboard_provider(p_api_provider_id: str, p_bundle: tuple) -> list:\n",
+ " \"\"\"\n",
+ " To offboard the API provider.\n",
+ " :param p_api_provider_id: The identifier of the API provider\n",
+ " :param p_bundle: The bundle of certificates and keys for the TLS mutual authentication operations\n",
+ " :return: A list containing the files created for the TLS mutual authentication operations on success, or an empty list otherwise\n",
+ " \"\"\"\n",
+ " global logger, ccf_api_onboarding_url, ca_root\n",
+ "\n",
+ " logger.debug('>>> offboard_provider')\n",
+ "\n",
+ " try:\n",
+ " url = 'https://' + CAPIF_HOSTNAME + ':' + str(CAPIF_PORT) + '/' + ccf_api_onboarding_url + '/' + p_api_provider_id\n",
+ " logger.debug('offboard_provider: url=' + url)\n",
+ " headers = {'Content-Type': 'application/json'}\n",
+ " logger.debug('offboard_provider (step1): headers: ' + str(headers))\n",
+ " bundle = store_certificate_2_files(p_bundle[0], p_bundle[1], ca_root) # Use CA certificate for verif\n",
+ " if len(bundle) != 3:\n",
+ " logger.error(f\"Error converting in-memory bundle into files\")\n",
+ " return []\n",
+ " logger.debug('offboard_provider (step2): bundle: ' + str(bundle))\n",
+ " response = requests.delete(url, headers=headers, cert=(bundle[0], bundle[1]), verify=bundle[2])\n",
+ " logger.debug('offboard_provider (step3): response=' + str(response))\n",
+ " response.raise_for_status() # Raise an exception for bad status codes\n",
+ " if response.status_code != 204:\n",
+ " logger.error(f\"Error creating user: {response.status_code} - {response.text}\")\n",
+ " return []\n",
+ " except requests.exceptions.RequestException as e:\n",
+ " logger.error(f\"offboard_provider failed: {e}\")\n",
+ " return []\n",
+ "\n",
+ " return bundle\n",
+ " # End of function offboard_provider\n",
+ "\n",
+ "def store_certificate_2_files(p_certificate, p_private_key, p_ca_root) -> list:\n",
+ " \"\"\"\n",
+ " Save certificate and key into files\n",
+ " :param p_certificate:\n",
+ " :param p_private_key:\n",
+ " :param p_ca_root:\n",
+ " :return: A list of file paths on success, an empty list otherwise\n",
+ " \"\"\"\n",
+ " global logger\n",
+ "\n",
+ " logger.debug('>>> store_certificate_2_files')\n",
+ " try:\n",
+ " with open(\"p_crt.crt\", \"w\") as f:\n",
+ " f.write(p_certificate)\n",
+ " with open(\"p_key.key\", \"w\") as f:\n",
+ " f.write(p_private_key.decode('utf-8'))\n",
+ " with open(\"ca_root.pem\", \"w\") as f:\n",
+ " f.write(p_ca_root)\n",
+ " return [\"p_crt.crt\", \"p_key.key\", \"ca_root.pem\"]\n",
+ " except Exception as e:\n",
+ " logger.error(f\"An error occurred: {e}\")\n",
+ "\n",
+ " return []\n",
+ " # End of function store_certificate_2_files\n"
+ ],
+ "metadata": {
+ "id": "rbpNr26tF2gr"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Putting everything together\n",
+ "\n",
+ "Now, it is time now to create the our third iteration of our CAPIF/MEC application. Here the logic is:\n",
+ "\n",
+ "- Login\n",
+ "- Create the user\n",
+ "- Get the information to use CAPIF (security materials & URLs)\n",
+ "- Onboard the provider\n",
+ "- Print certificates for each function\n",
+ "- Delete the user\n",
+ "- Logout\n"
+ ],
+ "metadata": {
+ "id": "wmvJSK8I13XD"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "%%script echo skipping\n",
+ "# Uncomment the line above to skip execution of this cell\n",
+ "def process_main():\n",
+ " \"\"\"\n",
+ " This is the third sprint of our CAPIF/MEC application:\n",
+ " - Login\n",
+ " - Print obtained tokens\n",
+ " - Create a new user\n",
+ " - Get the information to use CAPIF (security materials & URLs)\n",
+ " - Onboard the provider\n",
+ " - Print certificates for each function\n",
+ " - Delete the newly created user\n",
+ " - Logout\n",
+ " \"\"\"\n",
+ " global logger, ca_root, ccf_api_onboarding_url, ccf_publish_url, ccf_discover_url, ccf_security_url, ccf_onboarding_url\n",
+ "\n",
+ " logger.debug('Starting at ' + time.strftime('%Y%m%d-%H%M%S'))\n",
+ " logger.debug('\\t pwd= ' + os.getcwd())\n",
+ "\n",
+ " # Login\n",
+ " refresh_token, admin_token = process_login()\n",
+ " if refresh_token is None:\n",
+ " return\n",
+ "\n",
+ " # Create a new user\n",
+ " user_name, user_uuid = create_user(admin_token)\n",
+ " if len(user_uuid) == 0:\n",
+ " return\n",
+ "\n",
+ " auth = get_auth(user_name, USER_PASSWORD)\n",
+ " if len(auth) == 0:\n",
+ " return\n",
+ "\n",
+ " # Set the CAPIF access information\n",
+ " access_token = auth['access_token']\n",
+ " ca_root = auth['ca_root']\n",
+ " ccf_api_onboarding_url = auth['ccf_api_onboarding_url']\n",
+ " ccf_discover_url = auth['ccf_discover_url']\n",
+ " ccf_onboarding_url = auth['ccf_onboarding_url']\n",
+ " ccf_publish_url = auth['ccf_publish_url']\n",
+ " ccf_security_url = auth['ccf_security_url']\n",
+ " logger.debug(\"ccf_api_onboarding_url:\" + ccf_api_onboarding_url)\n",
+ " logger.debug(\"ccf_discover_url:\" + ccf_discover_url)\n",
+ " logger.debug(\"ccf_publish_url:\" + ccf_publish_url)\n",
+ " logger.debug(\"ccf_security_url:\" + ccf_security_url)\n",
+ "\n",
+ " # Onboard the provider\n",
+ " prov = onboard_provider(\"MECSandbox_to_CAPIF_Provider\", access_token)\n",
+ " if len(prov) == 0:\n",
+ " return\n",
+ "\n",
+ " # Print certificates for each function\n",
+ " logger.debug(\"API Provider Id:\" + prov['apiProvDomId'])\n",
+ " logger.debug(\"AMF: \" + prov['apiProvFuncs'][0]['regInfo']['apiProvCert'])\n",
+ " logger.debug(\"AEF: \" + prov['apiProvFuncs'][1]['regInfo']['apiProvCert'])\n",
+ " logger.debug(\"APF: \" + prov['apiProvFuncs'][2]['regInfo']['apiProvCert'])\n",
+ " logger.debug(\"csr: \" + str(prov['csr']))\n",
+ "\n",
+ " time.sleep(5) # Sleep for 5 seconds\n",
+ "\n",
+ " # Offboard the API profider\n",
+ " certs_bundle = (prov['apiProvFuncs'][0]['regInfo']['apiProvCert'], prov['csr']['amf'][1]) # Use AMF certificate and AMF private key\n",
+ " file_bundle = offboard_provider(prov['apiProvDomId'], certs_bundle)\n",
+ " if len(file_bundle) == 0:\n",
+ " for file in file_bundle:\n",
+ " os.remove(file)\n",
+ "\n",
+ " # Delete the newly created user\n",
+ " delete_user(user_uuid, admin_token)\n",
+ "\n",
+ " # Logout\n",
+ " process_logout()\n",
+ "\n",
+ " logger.debug('Stopped at ' + time.strftime('%Y%m%d-%H%M%S'))\n",
+ " # End of function process_main\n",
+ "\n",
+ "if __name__ == '__main__':\n",
+ " process_main()\n"
+ ],
+ "metadata": {
+ "id": "EDcPUuNEM26H"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Using ETSI MEC profile for CAPIF\n",
+ "\n",
+ "The purpose is to export the MEC Profile for CAPIF API into our CAPIF application. To achieve it, we need to fulfill the following requirements:\n",
+ "1. Create an instance of a MEC Sandbox using the '4g-5g-macri-v2x' network scenario\n",
+ "2. Set TRY_MEC_URL, TRY_MEC_SESSION_ID, TRY_MEC_PLTF, MEC_APP_INST_ID constant accordingly\n",
+ "3. Build the ServiceAPIDescription as described in ETSI TS 129 222 V18.6.0 (2022-06) Table 8.2.4.2.2-1: Definition of type ServiceAPIDescription. This is the role of the function below"
+ ],
+ "metadata": {
+ "id": "0wHI1ooMbCy3"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "def build_publish_api_from_mec_services(p_aefId: str) -> dict:\n",
+ " \"\"\"\n",
+ " This function builds the Publish API request body data structure which will be used todo the request for publish API\n",
+ " :param p_aefId: The AEF ID\n",
+ " :return The request body data structure on success, an empty dictionary otherwise\n",
+ " \"\"\"\n",
+ " global logger, TRY_MEC_URL, TRY_MEC_SESSION_ID, TRY_MEC_PLTF\n",
+ "\n",
+ " logger.debug('>>> build_publish_api_from_mec_services: p_aefId=' + p_aefId)\n",
+ "\n",
+ " # Sanity checks\n",
+ " if len(p_aefId) == 0:\n",
+ " logger.error('build_publish_api_from_mec_services: p_aefId is empty')\n",
+ " return dict()\n",
+ "\n",
+ " # Build the service-apis data structure\n",
+ " publish_api_req_body = {\n",
+ " \"apiName\": \"MEC Profile for CAPIF\",\n",
+ " \"aefProfiles\": [\n",
+ " {\n",
+ " \"aefId\": p_aefId,\n",
+ " \"versions\": [\n",
+ " {\n",
+ " \"apiVersion\": \"v1\",\n",
+ " \"expiry\": \"2025-11-30T10:32:02.004Z\",\n",
+ " \"resources\": [\n",
+ " {\n",
+ " \"resourceName\": \"MEC Profile of CAPIF\",\n",
+ " \"commType\": \"REQUEST_RESPONSE\",\n",
+ " \"uri\": f\"/{TRY_MEC_SESSION_ID}/{TRY_MEC_PLTF}/service-apis/v1/allServiceAPIs\",\n",
+ " \"custOpName\": \"string\",\n",
+ " \"operations\": [\n",
+ " \"GET\"\n",
+ " ],\n",
+ " \"description\": \"Endpoint to access MEC services\"\n",
+ " }\n",
+ " ],\n",
+ " \"custOperations\": [\n",
+ " {\n",
+ " \"commType\": \"REQUEST_RESPONSE\",\n",
+ " \"custOpName\": \"string\",\n",
+ " \"operations\": [\n",
+ " \"GET\"\n",
+ " ],\n",
+ " \"description\": \"string\"\n",
+ " }\n",
+ " ]\n",
+ " }\n",
+ " ],\n",
+ " \"protocol\": \"HTTP_1_1\",\n",
+ " \"dataFormat\": \"JSON\",\n",
+ " \"securityMethods\": [\"OAUTH\"],\n",
+ " \"interfaceDescriptions\": [\n",
+ " {\n",
+ " \"ipv4Addr\": TRY_MEC_URL,\n",
+ " \"securityMethods\": [\"OAUTH\"]\n",
+ " }\n",
+ " ]\n",
+ " }\n",
+ " ],\n",
+ " \"description\": \"MEC Profile of CAPIF\",\n",
+ " \"supportedFeatures\": \"fffff\",\n",
+ " \"shareableInfo\": {\n",
+ " \"isShareable\": True,\n",
+ " \"capifProvDoms\": [\n",
+ " \"string\"\n",
+ " ]\n",
+ " },\n",
+ " \"serviceAPICategory\": \"string\",\n",
+ " \"apiSuppFeats\": \"fffff\",\n",
+ " \"pubApiPath\": {\n",
+ " \"ccfIds\": [\n",
+ " \"string\"\n",
+ " ]\n",
+ " },\n",
+ " \"ccfId\": \"string\",\n",
+ " \"apiStatus\":{\n",
+ " \"aefIds\": [\n",
+ " p_aefId\n",
+ " ]\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " logger.debug('<<< build_publish_api_from_mec_services: ' + str(publish_api_req_body))\n",
+ " return publish_api_req_body\n",
+ " # End of build_publish_api_from_mec_services function"
+ ],
+ "metadata": {
+ "id": "S7InJDD1_g-v"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "Having built the ServiceAPIDescription data structure, the next step is to implement the CAPIF publish API.\n",
+ "\n",
+ "To proceed, we need to enable the TLS mutual authentication using the security material obtained during the onboarding APIs operation ([Onboarding APIs](#onboarding_apis)), i.e. the AEF certificate and the AEF private key ([Generate certificates](#Generate_certificates)).\n",
+ "\n",
+ "\n",
+ "**Refer to:** ETSI TS 129 222 V18.6.0 (2022-06) Clauses 5.3 CAPIF_Publish_Service_API and 8.2 CAPIF_Publish_Service_API\n",
+ "\n",
+ "Before to proceed with the steps above, let's create 2 helper functions to simpily the implemantation of the CAPIF publish API. These helper functions cover the following operations:\n",
+ "- Onboarding operations\n",
+ "- Offboarding operations"
+ ],
+ "metadata": {
+ "id": "PRAie110_r8P"
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "#### Onboarding operations\n",
+ "\n",
+ "The Onboarding operations include th following steps:\n",
+ "- login\n",
+ "- create a new user\n",
+ "- Get the information to use CAPIF (security materials & URLs)\n",
+ "- onboard the provider\n"
+ ],
+ "metadata": {
+ "id": "IreHiSXs2U65"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "def onboarding_provider() -> dict:\n",
+ " \"\"\"\n",
+ " To onboard the provider using CAPIF endpoint. It includes:\n",
+ " - login\n",
+ " - create a new user\n",
+ " - Get the information to use CAPIF (security materials & URLs)\n",
+ " - onboard the provider\n",
+ " :return: A dictionary containing security material and additional context information on success, or an empty dictionary otherwise\n",
+ " \"\"\"\n",
+ " global logger, ca_root, ccf_api_onboarding_url, ccf_publish_url, ccf_discover_url, ccf_security_url, ccf_onboarding_url\n",
+ "\n",
+ " # Login\n",
+ " refresh_token, admin_token = process_login()\n",
+ " if refresh_token is None:\n",
+ " return dict()\n",
+ "\n",
+ " # Create a new user\n",
+ " user_name, user_uuid = create_user(admin_token)\n",
+ " if len(user_uuid) == 0:\n",
+ " return dict()\n",
+ "\n",
+ " auth = get_auth(user_name, USER_PASSWORD)\n",
+ " if len(auth) == 0:\n",
+ " return dict()\n",
+ "\n",
+ " # Set the CAPIF access information\n",
+ " access_token = auth['access_token']\n",
+ " ca_root = auth['ca_root']\n",
+ " ccf_api_onboarding_url = auth['ccf_api_onboarding_url']\n",
+ " ccf_discover_url = auth['ccf_discover_url']\n",
+ " ccf_onboarding_url = auth['ccf_onboarding_url']\n",
+ " ccf_publish_url = auth['ccf_publish_url']\n",
+ " ccf_security_url = auth['ccf_security_url']\n",
+ "\n",
+ " # Onboard the provider\n",
+ " prov = onboard_provider(\"MECSandbox_to_CAPIF_Provider\", access_token)\n",
+ " if len(prov) == 0:\n",
+ " return dict()\n",
+ "\n",
+ " # Add context data\n",
+ " prov['refresh_token'] = refresh_token\n",
+ " prov['admin_token'] = admin_token\n",
+ " prov['user_uuid'] = user_uuid\n",
+ " prov['access_token'] = access_token\n",
+ "\n",
+ " return prov\n",
+ " # End of onboarding_provider function"
+ ],
+ "metadata": {
+ "id": "nu-tEA6n2TpI"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "#### Offboarding operations\n",
+ "\n",
+ "The Offboarding operations include th following steps:\n",
+ "- Offboard the API provide\n",
+ "- Delete the user\n",
+ "- Logout\n"
+ ],
+ "metadata": {
+ "id": "e940bUcf2deu"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "def offboarding_provider(p_user_uuid: str, p_api_provider_id: str, p_bundle: tuple, p_admin_token: str) -> int:\n",
+ " \"\"\"\n",
+ " To offboard the provider. It includes:\n",
+ " - Offboard the API provider\n",
+ " - Delete the user\n",
+ " - Logout\n",
+ " :return: 0 on success, or -1 otherwise\n",
+ " \"\"\"\n",
+ " global logger, ccf_api_onboarding_url, access_token\n",
+ "\n",
+ " logger.debug('>>> offboarding_provider: ' + p_user_uuid)\n",
+ "\n",
+ " # Offboard the API profider\n",
+ " file_bundle = offboard_provider(p_api_provider_id, p_bundle)\n",
+ " if len(file_bundle) == 0: # Remove cert files if any\n",
+ " for file in file_bundle:\n",
+ " os.remove(file)\n",
+ "\n",
+ " # Delete the newly created user\n",
+ " delete_user(p_user_uuid, p_admin_token)\n",
+ "\n",
+ " # Logout\n",
+ " process_logout()\n",
+ "\n",
+ " return 0\n",
+ " # End of offboarding_provider function"
+ ],
+ "metadata": {
+ "id": "hEnFLfPI2hms"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "#### Publish CAPIF API function\n",
+ "\n",
+ "As mentionned above , the prupose of this function is to publish an API, using the TLS mutual authentication. To do so, we need the APF certificate (public keys) and its private key for the signature and the encription processes, and the CA certificate for the verification of the peer certifcate.\n",
+ "\n",
+ "**Note**: The http.request function required taht the cerficates and the keys are stored on files, not in memory. So, when our CAPIF applicate terminates, these files shall be removed (freeing resource step). This is the reason the publish_capif_api() function return a tuple containing the files created for the TLS mutual authentication operation.\n"
+ ],
+ "metadata": {
+ "id": "9TSYztWMcaOA"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "def publish_capif_api(p_apfId: str, p_body: dict, p_bundle: tuple) -> list:\n",
+ " \"\"\"\n",
+ " This function is to publish an API on CAPIF server\n",
+ " :param p_apfId: The APF identifier\n",
+ " :param p_body: The request body\n",
+ " :param p_bundle: The bundle of certificates and keys for the TLS mutual authentication operations\n",
+ " :return: A list containing the files created for the TLS mutual authentication operations on success, or an empty list otherwise\n",
+ " \"\"\"\n",
+ " global logger, ccf_publish_url, ca_root\n",
+ "\n",
+ " logger.debug('>>> publish_capif_api')\n",
+ "\n",
+ " # Sanity checks\n",
+ " if len(p_bundle) != 2:\n",
+ " logger.error('publish_capif_api: p_bundle is malformed')\n",
+ " return []\n",
+ "\n",
+ " try:\n",
+ " url = 'https://' + CAPIF_HOSTNAME + ':' + str(CAPIF_PORT) + '/' + ccf_publish_url.replace('', p_apfId)\n",
+ " logger.debug('publish_capif_api: url=' + url)\n",
+ " headers = {'Content-Type': 'application/json'}\n",
+ " logger.debug('publish_capif_api (step1): headers: ' + str(headers))\n",
+ " logger.debug('publish_capif_api (step2): body: ' + str(p_body))\n",
+ " bundle = store_certificate_2_files(p_bundle[0], p_bundle[1], ca_root) # Use CA certificate for verif\n",
+ " if len(bundle) != 3:\n",
+ " logger.error(f\"Error converting in-memory bundle into files\")\n",
+ " return []\n",
+ " logger.debug('publish_capif_api (step3): bundle: ' + str(bundle))\n",
+ " response = requests.post(url, headers=headers, data=json.dumps(p_body), cert=(bundle[0], bundle[1]), verify=bundle[2])\n",
+ " response.raise_for_status() # Raise an exception for bad status codes\n",
+ " logger.debug('publish_capif_api (step4): result: ' + str(response.json()))\n",
+ " if response.status_code != 201:\n",
+ " logger.error(f\"Error creating user: {response.status_code} - {response.text}\")\n",
+ " return []\n",
+ " res = json.loads(response.text)\n",
+ " logger.debug('publish_capif_api (step5): res: ' + str(res))\n",
+ " api_id = res['apiId']\n",
+ " return bundle\n",
+ " except requests.exceptions.RequestException as e:\n",
+ " logger.error(f\"publish_capif_api failed: {e}\")\n",
+ "\n",
+ " return []\n",
+ " # End of function publish_capif_api\n"
+ ],
+ "metadata": {
+ "id": "z_Cwazjl_xGJ"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Putting everything together\n",
+ "\n"
+ ],
+ "metadata": {
+ "id": "-TzvBVLM1fIc"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "%%script echo skipping\n",
+ "# Uncomment the line above to skip execution of this cell\n",
+ "def process_main():\n",
+ " \"\"\"\n",
+ " This is the fourth sprint of our CAPIF/MEC application:\n",
+ " - Onboarding operations\n",
+ " - Offboarding operations\n",
+ " \"\"\"\n",
+ " global logger, ca_root\n",
+ "\n",
+ " logger.debug('Starting at ' + time.strftime('%Y%m%d-%H%M%S'))\n",
+ " logger.debug('\\t pwd= ' + os.getcwd())\n",
+ "\n",
+ " prov = onboarding_provider()\n",
+ " if len(prov) == 0:\n",
+ " return\n",
+ " user_uuid = prov['user_uuid']\n",
+ "\n",
+ " # Build the publish_api body request\n",
+ " aefId = prov['apiProvFuncs'][1]['apiProvFuncId']\n",
+ " apfId = prov['apiProvFuncs'][2]['apiProvFuncId']\n",
+ " publish_api_req_body = build_publish_api_from_mec_services(aefId)\n",
+ " if len(publish_api_req_body) == 0:\n",
+ " offboarding_provider(user_uuid, prov['apiProvDomId'], (prov['apiProvFuncs'][0]['regInfo']['apiProvCert'], prov['csr']['amf'][1]), prov['admin_token']) # Use AMF certificate and AMF private key\n",
+ " return\n",
+ " logger.debug(\"publish_api_req_body: \" + str(publish_api_req_body))\n",
+ " certs_bundle = (prov['apiProvFuncs'][2]['regInfo']['apiProvCert'], prov['csr']['apf'][1]) # Use APF certificate and APF private key\n",
+ "\n",
+ " # Publish the APIs\n",
+ " #ipdb.set_trace()\n",
+ " files_bundle = publish_capif_api(apfId, publish_api_req_body, certs_bundle)\n",
+ " if len(files_bundle) == 0:\n",
+ " for file in files_bundle:\n",
+ " os.remove(file)\n",
+ " offboarding_provider(user_uuid, prov['apiProvDomId'], (prov['apiProvFuncs'][0]['regInfo']['apiProvCert'], prov['csr']['amf'][1]), prov['admin_token']) # Use AMF certificate and AMF private key\n",
+ " return\n",
+ "\n",
+ " # Terminate the application\n",
+ " offboarding_provider(\n",
+ " user_uuid,\n",
+ " prov['apiProvDomId'],\n",
+ " (prov['apiProvFuncs'][0]['regInfo']['apiProvCert'], prov['csr']['amf'][1]), # Use AMF certificate and AMF private key\n",
+ " prov['admin_token']\n",
+ " )\n",
+ "\n",
+ " logger.debug('Stopped at ' + time.strftime('%Y%m%d-%H%M%S'))\n",
+ " # End of function process_main\n",
+ "\n",
+ "if __name__ == '__main__':\n",
+ " process_main()\n"
+ ],
+ "metadata": {
+ "id": "CRtfJ6cm3V6b"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Build an helper function to publish the ETSI MEC profile for CAPIF API\n",
+ "\n",
+ "To simply the API invoker process, let's create two helpers functions:\n",
+ "- One to publish the ETSI MEC profile for CAPIF API\n",
+ "- One to remove the previously published API"
+ ],
+ "metadata": {
+ "id": "aABBc4Hizy88"
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Helper to publish API"
+ ],
+ "metadata": {
+ "id": "r-gZe6mQ4yHH"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "def publish_api() -> tuple:\n",
+ " \"\"\"\n",
+ " \"\"\"\n",
+ " global logger\n",
+ "\n",
+ " logger.debug('>>> publish_api')\n",
+ "\n",
+ " prov = onboarding_provider()\n",
+ " if len(prov) == 0:\n",
+ " return ()\n",
+ " amf_cert_bundle = (prov['apiProvFuncs'][0]['regInfo']['apiProvCert'], prov['csr']['amf'][1]) # Use AMF certificate and AMF private key\n",
+ "\n",
+ " # Build the publish_api body request\n",
+ " aefId = prov['apiProvFuncs'][1]['apiProvFuncId']\n",
+ " apfId = prov['apiProvFuncs'][2]['apiProvFuncId']\n",
+ " apiId = prov['apiProvDomId']\n",
+ " publish_api_req_body = build_publish_api_from_mec_services(aefId)\n",
+ " if len(publish_api_req_body) == 0:\n",
+ " offboarding_provider(prov['user_uuid'], apiId, amf_cert_bundle, prov['admin_token'])\n",
+ " return ()\n",
+ " logger.debug(\"publish_api_req_body: \" + str(publish_api_req_body))\n",
+ " certs_bundle = (prov['apiProvFuncs'][2]['regInfo']['apiProvCert'], prov['csr']['apf'][1]) # Use APF certificate and APF private key\n",
+ "\n",
+ " # Publish the APIs\n",
+ " files_bundle = publish_capif_api(apfId, publish_api_req_body, certs_bundle)\n",
+ " if len(files_bundle) == 0:\n",
+ " for file in files_bundle:\n",
+ " os.remove(file)\n",
+ " offboarding_provider(prov['user_uuid'], apiId, amf_cert_bundle, prov['admin_token']) # Use AMF certificate and AMF private key\n",
+ " return ()\n",
+ "\n",
+ " logger.debug('publish_api: ' + str((apiId, amf_cert_bundle)))\n",
+ " return (apiId, amf_cert_bundle, prov['user_uuid'], prov['admin_token'])\n",
+ " # End of function publish_api\n"
+ ],
+ "metadata": {
+ "id": "ozCMG8jh0UMd"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Helper to remove published API"
+ ],
+ "metadata": {
+ "id": "lrnfAlrZ1-TB"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "def remove_publish_api(p_user_uuid: str, p_api_id: str, p_bundle: tuple, p_admin_token: str):\n",
+ " \"\"\"\n",
+ " To remove published API.\n",
+ " :param p_user_uuid: The user identifier\n",
+ " :param p_api_id: The API identifier\n",
+ " :param p_bundle: The bundle of certificates and keys for the TLS mutual authentication operations\n",
+ " \"\"\"\n",
+ " global logger\n",
+ "\n",
+ " logger.debug('>>> remove_publish_api ')\n",
+ "\n",
+ " # Terminate the application\n",
+ " offboarding_provider(p_user_uuid, p_api_id, p_bundle, p_admin_token)\n",
+ "\n",
+ " return\n",
+ " # End of function reove_publish_api\n"
+ ],
+ "metadata": {
+ "id": "Ql9dC3P41_nO"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Putting everything together\n",
+ "\n",
+ "Let's test these two helpers functions before to go ahead"
+ ],
+ "metadata": {
+ "id": "UNN73-Zg4WZ-"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "%%script echo skipping\n",
+ "# Uncomment the line above to skip execution of this cell\n",
+ "def process_main():\n",
+ " \"\"\"\n",
+ " To test both helpers functions:\n",
+ " - publish_api\n",
+ " - remove_publish_api\n",
+ " \"\"\"\n",
+ " global logger\n",
+ "\n",
+ " logger.debug('Starting at ' + time.strftime('%Y%m%d-%H%M%S'))\n",
+ " logger.debug('\\t pwd= ' + os.getcwd())\n",
+ "\n",
+ " # Publish the MEC profile for CAPIF API\n",
+ " res = publish_api()\n",
+ " if len(res) == 0:\n",
+ " return\n",
+ "\n",
+ " api_id, bundle, user_uuid, admin_token = res\n",
+ "\n",
+ " time.sleep(5) # Sleep for 5 seconds\n",
+ "\n",
+ " # Remove the MEC profile for CAPIF API\n",
+ " remove_publish_api(user_uuid, api_id, bundle, admin_token)\n",
+ "\n",
+ " logger.debug('Stopped at ' + time.strftime('%Y%m%d-%H%M%S'))\n",
+ " # End of function process_main\n",
+ "\n",
+ "if __name__ == '__main__':\n",
+ " process_main()\n"
+ ],
+ "metadata": {
+ "id": "bVYS13iV4-s8"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Using the published API: The CAPIF Invoker side\n",
+ "\n",
+ "Now that we are able to publish an API on the CAPIF server, the next step is to use it in order to invoke some MEC Services API. To achieve this goal, we have to implement the CAPI Invoker, following these steps:\n",
+ "- Onboard an API invoker\n",
+ "- Discover the published APIs\n",
+ "- Get a MEC Service API (this step requires that a MEC Sandox platform is already running)\n",
+ "- Offboard an API invoker"
+ ],
+ "metadata": {
+ "id": "9W2SvVdx6fxk"
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Onboard an API invoker\n"
+ ],
+ "metadata": {
+ "id": "YasCvixW7E4o"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "def onboard_invoker(p_name: str, p_access_token: str) -> dict:\n",
+ " \"\"\"\n",
+ " To onboard the API invoker.\n",
+ " :param p_name: The name of the invoker\n",
+ " :return: A dictionary containing security material for each CAPIF endpoint on success, or an empty dictionary otherwise\n",
+ " \"\"\"\n",
+ " global logger, ccf_api_onboarding_url, ccf_onboarding_url\n",
+ "\n",
+ " logger.debug('>>> onboard_invoker: ' + p_name)\n",
+ " logger.debug('>>> onboard_invoker: ' + p_access_token)\n",
+ "\n",
+ " try:\n",
+ " url = 'https://' + CAPIF_HOSTNAME + ':' + str(CAPIF_PORT) + '/' + ccf_onboarding_url\n",
+ " logger.debug('onboard_invoker: url=' + url)\n",
+ " headers = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + p_access_token}\n",
+ " logger.debug('onboard_invoker (step1): headers: ' + str(headers))\n",
+ " # Request body for onboarding the invoker\n",
+ " invoker_csr_request, invoker_private_key = generate_csr(\"API Invoker\", \"ETSI\", \"Fr\")\n",
+ " data = {\n",
+ " \"notificationDestination\" : \"http://host.docker.internal:8086/netapp_callback\",\n",
+ " \"supportedFeatures\" : \"fffffff\",\n",
+ " \"apiInvokerInformation\" : \"dummy\",\n",
+ " \"websockNotifConfig\" : {\n",
+ " \"requestWebsocketUri\" : True,\n",
+ " \"websocketUri\" : \"websocketUri\"\n",
+ " },\n",
+ " \"onboardingInformation\" : {\n",
+ " \"apiInvokerPublicKey\" : invoker_csr_request.decode(\"utf-8\"),\n",
+ " },\n",
+ " \"requestTestNotification\" : True\n",
+ " }\n",
+ " logger.debug('onboard_invoker (step2): body: ' + str(data))\n",
+ " response = requests.post(url, headers=headers, data=json.dumps(data), verify=\"ca_root.pem\")\n",
+ " response.raise_for_status() # Raise an exception for bad status codes\n",
+ " logger.debug('onboard_invoker (step3): result: ' + str(response.json()))\n",
+ " if response.status_code != 201:\n",
+ " logger.error(f\"Error creating user: {response.status_code} - {response.text}\")\n",
+ " return dict()\n",
+ " res = json.loads(response.text)\n",
+ " # Add an entry for CSRs and private keys for future usage\n",
+ " res['csr'] = [invoker_csr_request, invoker_private_key]\n",
+ " return res\n",
+ " except requests.exceptions.RequestException as e:\n",
+ " logger.error(f\"onboard_invoker failed: {e}\")\n",
+ "\n",
+ " return dict()\n",
+ " # End of function onboard_invoker"
+ ],
+ "metadata": {
+ "id": "f11_uMS67I9J"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Offboard an API invoker"
+ ],
+ "metadata": {
+ "id": "kQmJW-d99cGo"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "def offboard_invoker(p_invoker_id: str, p_bundle: tuple) -> list:\n",
+ " \"\"\"\n",
+ " To offboard the API invoker.\n",
+ " :param p_invoker_id: The API invoker identifier\n",
+ " :param p_bundle: The bundle of certificates and keys for the TLS mutual authentication operations\n",
+ " :return: 0 on success, -1 otherwise\n",
+ " \"\"\"\n",
+ " global logger, ccf_onboarding_url, ca_root\n",
+ "\n",
+ " logger.debug('>>> offboard_invoker: ' + p_invoker_id)\n",
+ "\n",
+ " try:\n",
+ " # Delete the newly created user\n",
+ " url = 'https://' + CAPIF_HOSTNAME + ':' + str(CAPIF_PORT) + '/' + ccf_onboarding_url + '/' + p_invoker_id\n",
+ " logger.debug('offboard_invoker: url=' + url)\n",
+ " headers = {'Content-Type': 'application/json'}\n",
+ " logger.debug('offboard_invoker (step1): headers: ' + str(headers))\n",
+ " bundle = store_certificate_2_files(p_bundle[0], p_bundle[1], ca_root) # Use CA certificate for verif\n",
+ " if len(bundle) != 3:\n",
+ " logger.error(f\"Error converting in-memory bundle into files\")\n",
+ " return []\n",
+ " logger.debug('offboard_invoker (step2): bundle: ' + str(bundle))\n",
+ " response = requests.delete(url, headers=headers, cert=(bundle[0], bundle[1]), verify=bundle[2])\n",
+ " logger.debug('offboard_invoker (step3): response=' + str(response))\n",
+ " response.raise_for_status() # Raise an exception for bad status codes\n",
+ " if response.status_code != 204:\n",
+ " logger.error(f\"Error creating user: {response.status_code} - {response.text}\")\n",
+ " return []\n",
+ " except requests.exceptions.RequestException as e:\n",
+ " logger.error(f\"offboard_invoker failed: {e}\")\n",
+ " return []\n",
+ "\n",
+ " return bundle\n",
+ " # End of function offboard_invoker"
+ ],
+ "metadata": {
+ "id": "KRC_xkGO9hEY"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Discover published APIs"
+ ],
+ "metadata": {
+ "id": "-h0zz7ocxtyv"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "def discover(p_invoker_id: str, p_bundle: tuple, p_access_token: str) -> dict:\n",
+ " \"\"\"\n",
+ " To discover the APIs published by capif core.\n",
+ " :param p_invoker_id: The API invoker identifier\n",
+ " :return: A dictionary containing the APIs published by capif core on success, or an empty dictionary otherwise\n",
+ " \"\"\"\n",
+ " global logger, ca_root, ccf_discover_url\n",
+ "\n",
+ " logger.debug('>>> Discover APIs published by capif core')\n",
+ "\n",
+ " try:\n",
+ " url = 'https://' + CAPIF_HOSTNAME + ':' + str(CAPIF_PORT) + '/' + ccf_discover_url + p_invoker_id\n",
+ " logger.debug('Discover: url=' + url)\n",
+ " headers = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + p_access_token}\n",
+ " logger.debug('Discover (step1): headers: ' + str(headers))\n",
+ " bundle = store_certificate_2_files(p_bundle[0], p_bundle[1], ca_root) # Use CA certificate for verif\n",
+ " if len(bundle) != 3:\n",
+ " logger.error(f\"Error converting in-memory bundle into files\")\n",
+ " return dict()\n",
+ " logger.debug('Discover (step2): bundle: ' + str(bundle))\n",
+ " response = requests.get(url, headers=headers, cert=(bundle[0], bundle[1]), verify=bundle[2])\n",
+ " logger.debug('Discover (step3): response=' + str(response))\n",
+ " response.raise_for_status() # Raise an exception for bad status codes\n",
+ " logger.debug('Discover : result: ' + str(response.json()))\n",
+ " if response.status_code != 200:\n",
+ " logger.error(f\"Discovery failed: {response.status_code} - {response.text}\")\n",
+ " return dict()\n",
+ " res = json.loads(response.text)\n",
+ " return res\n",
+ " except requests.exceptions.RequestException as e:\n",
+ " logger.error(f\"Discovery failed: {e}\")\n",
+ "\n",
+ " return dict()\n",
+ " # End of function discover\n",
+ "\n",
+ "def extract_ipv4_and_uri(p_response_data: json.loads) -> dict:\n",
+ " # Extract ipv4Addr using a list comprehension\n",
+ " ipv4_addrs = [\n",
+ " desc.get(\"ipv4Addr\")\n",
+ " for profile in p_response_data.get(\"serviceAPIDescriptions\", [])\n",
+ " for aef in profile.get(\"aefProfiles\", [])\n",
+ " for desc in aef.get(\"interfaceDescriptions\", [])\n",
+ " ]\n",
+ "\n",
+ " # Extract uri using a list comprehension\n",
+ " uris = [\n",
+ " resource.get(\"uri\")\n",
+ " for profile in p_response_data.get(\"serviceAPIDescriptions\", [])\n",
+ " for aef in profile.get(\"aefProfiles\", [])\n",
+ " for version in aef.get(\"versions\", [])\n",
+ " for resource in version.get(\"resources\", [])\n",
+ " ]\n",
+ "\n",
+ " return {\"ipv4Addr\": ipv4_addrs, \"uri\": uris}\n",
+ " # End of function extract_ipv4_and_uri"
+ ],
+ "metadata": {
+ "id": "ofUuploUxuhn"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Putting everything together"
+ ],
+ "metadata": {
+ "id": "RElS9XFZ9hvQ"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "%%script echo skipping\n",
+ "# Uncomment the line above to skip execution of this cell\n",
+ "def process_main():\n",
+ " \"\"\"\n",
+ " This is the fiveth sprint of our CAPIF/MEC application:\n",
+ " - Publish the MEC profile for CAPIF API\n",
+ " - Create a new user for the invoker\n",
+ " - Get certificates\n",
+ " - Onboad the API invoker\n",
+ " - Do the discovery\n",
+ " - Offboard the API invoker\n",
+ " - Delete the\n",
+ " - Logout the invoker user\n",
+ " - Remove the MEC profile for CAPIF API\n",
+ " \"\"\"\n",
+ " global logger, ca_root, ccf_api_onboarding_url, ccf_discover_url, ccf_onboarding_url, ccf_publish_url, ccf_security_url\n",
+ "\n",
+ " logger.debug('Starting at ' + time.strftime('%Y%m%d-%H%M%S'))\n",
+ " logger.debug('\\t pwd= ' + os.getcwd())\n",
+ "\n",
+ " # Publish the MEC profile for CAPIF API\n",
+ " res = publish_api()\n",
+ " if len(res) == 0:\n",
+ " return\n",
+ " api_id, bundle, prov_user_uuid, prov_admin_token = res\n",
+ "\n",
+ " # Login for the new user for the invoker\n",
+ " refresh_token, admin_token = process_login()\n",
+ " if refresh_token is None:\n",
+ " remove_publish_api(prov_user_uuid, api_id, bundle, prov_admin_token)\n",
+ " return\n",
+ "\n",
+ " # Create a new user for the invoker\n",
+ " res = create_user(admin_token)\n",
+ " if len(res) == 0:\n",
+ " remove_publish_api(prov_user_uuid, api_id, bundle, prov_admin_token)\n",
+ " return\n",
+ " user_name, user_uuid = res\n",
+ "\n",
+ " # Get certificates\n",
+ " auth = get_auth(user_name, USER_PASSWORD)\n",
+ " if len(auth) == 0:\n",
+ " delete_user(user_name, admin_token)\n",
+ " remove_publish_api(prov_user_uuid, api_id, bundle, prov_admin_token)\n",
+ " return\n",
+ "\n",
+ " # Sanity checks\n",
+ " if auth['ca_root'] != ca_root:\n",
+ " raise Exception('CA root mismatch')\n",
+ " if auth['ccf_api_onboarding_url'] != ccf_api_onboarding_url:\n",
+ " raise Exception('CCF API onboarding URL mismatch')\n",
+ " if auth['ccf_discover_url'] != ccf_discover_url:\n",
+ " raise Exception('CCF discover URL mismatch')\n",
+ " if auth['ccf_onboarding_url'] != ccf_onboarding_url:\n",
+ " raise Exception('CCF onboarding URL mismatch')\n",
+ " if auth['ccf_publish_url'] != ccf_publish_url:\n",
+ " raise Exception('CCF publish URL mismatch')\n",
+ " if auth['ccf_security_url'] != ccf_security_url:\n",
+ " raise Exception('CCF security URL mismatch')\n",
+ " access_token = auth['access_token']\n",
+ "\n",
+ " # Onboad the API invoker\n",
+ " res = onboard_invoker('API Invoker', access_token)\n",
+ " if len(res) == 0:\n",
+ " delete_user(user_uuid, admin_token)\n",
+ " remove_publish_api(prov_user_uuid, api_id, bundle, prov_admin_token)\n",
+ " return\n",
+ "\n",
+ " # Do the discovery\n",
+ " invoker_id = res['apiInvokerId']\n",
+ " certs_bundle = (res['onboardingInformation']['apiInvokerCertificate'], res['csr'][1])\n",
+ " mec_api = discover(invoker_id, certs_bundle, access_token)\n",
+ " if len(mec_api) == 0:\n",
+ " files_bundle = offboard_invoker(invoker_id, certs_bundle)\n",
+ " if len(files_bundle) == 0:\n",
+ " for file in files_bundle:\n",
+ " os.remove(file)\n",
+ " delete_user(user_uuid, admin_token)\n",
+ " remove_publish_api(prov_user_uuid, api_id, bundle, prov_admin_token)\n",
+ " return\n",
+ "\n",
+ " # Extract the URL to access to the MEC Sandbox platform\n",
+ " addrs = extract_ipv4_and_uri(mec_api)\n",
+ " logger.debug('addrs: ' + str(addrs))\n",
+ "\n",
+ " time.sleep(5) # Sleep for 5 seconds\n",
+ "\n",
+ " # Offboard the API invoker\n",
+ " files_bundle = offboard_invoker(invoker_id, certs_bundle)\n",
+ " if len(files_bundle) == 0:\n",
+ " for file in files_bundle:\n",
+ " os.remove(file)\n",
+ "\n",
+ " # Delete the invoker user\n",
+ " delete_user(user_uuid, admin_token)\n",
+ "\n",
+ " # Logout the invoker user\n",
+ " process_logout()\n",
+ "\n",
+ " # Remove the MEC profile for CAPIF API\n",
+ " remove_publish_api(prov_user_uuid, api_id, bundle, prov_admin_token)\n",
+ "\n",
+ "if __name__ == '__main__':\n",
+ " process_main()"
+ ],
+ "metadata": {
+ "id": "QPZPYJZM9mNr"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Discoverig MEC services\n"
+ ],
+ "metadata": {
+ "id": "6tJWDz4woyz1"
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Invoking the MEC profile for CAPIF\n",
+ "\n",
+ "AFter discovering the published API, we have the information (see content of addrs data structure in previous execution) to do a request to an existing MEC Sandbox platform to get the list of the MEC services exposed (see TRY_MEC_SESSION_ID)."
+ ],
+ "metadata": {
+ "id": "Uy-XNKA9pN5h"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "def discovering_mec_services(p_url: str) -> dict:\n",
+ " \"\"\"\n",
+ " To discover MEC services API\n",
+ " :param p_url: The URL to access to the MEC Sandbox platform\n",
+ " :return: A dictionary containing the MEC services on success, or an empty dictionary otherwise\n",
+ " \"\"\"\n",
+ " global logger\n",
+ "\n",
+ " logger.debug('>>> discovering_mec_services: ' + p_url)\n",
+ "\n",
+ " try:\n",
+ " headers = {'Content-Type': 'application/json', 'accept': 'application/json',}\n",
+ " logger.debug('discovering_mec_services (step1): headers: ' + str(headers))\n",
+ " response = requests.get(p_url, headers=headers)\n",
+ " logger.debug('discovering_mec_services (step2): result: ' + str(response.json()))\n",
+ " response.raise_for_status() # Raise an exception for bad status codes\n",
+ " if response.status_code != 200:\n",
+ " logger.error(f\"Error creating user: {response.status_code} - {response.text}\")\n",
+ " return dict()\n",
+ " res = json.loads(response.text)\n",
+ " return res\n",
+ " except requests.exceptions.RequestException as e:\n",
+ " logger.error(f\"discovering_mec_services failed: {e}\")\n",
+ "\n",
+ " return dict()\n",
+ " # End of function discovering_mec_services"
+ ],
+ "metadata": {
+ "id": "ZL8Gyo0Ao2_u"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### Putting everything together\n",
+ "\n",
+ "Here is the last and complete version of our CAPIF application achieving the main objective of this tutorial: retrieve the MEC services exposed by an existing MEC Sandbox platform."
+ ],
+ "metadata": {
+ "id": "Wa_8khiGpTAa"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "#%%script echo skipping\n",
+ "# Uncomment the line above to skip execution of this cell\n",
+ "def process_main():\n",
+ " \"\"\"\n",
+ " This is the fiveth sprint of our CAPIF/MEC application:\n",
+ " - Publish the MEC profile for CAPIF API\n",
+ " - Create a new user for the invoker\n",
+ " - Get certificates\n",
+ " - Onboad the API invoker\n",
+ " - Do the discovery\n",
+ " - Offboard the API invoker\n",
+ " - Delete the\n",
+ " - Logout the invoker user\n",
+ " - Remove the MEC profile for CAPIF API\n",
+ " \"\"\"\n",
+ " global logger, ca_root, ccf_api_onboarding_url, ccf_discover_url, ccf_onboarding_url, ccf_publish_url, ccf_security_url\n",
+ "\n",
+ " logger.debug('Starting at ' + time.strftime('%Y%m%d-%H%M%S'))\n",
+ " logger.debug('\\t pwd= ' + os.getcwd())\n",
+ "\n",
+ " # Publish the MEC profile for CAPIF API\n",
+ " res = publish_api()\n",
+ " if len(res) == 0:\n",
+ " return\n",
+ " api_id, bundle, prov_user_uuid, prov_admin_token = res\n",
+ "\n",
+ " # Login for the new user for the invoker\n",
+ " refresh_token, admin_token = process_login()\n",
+ " if refresh_token is None:\n",
+ " remove_publish_api(prov_user_uuid, api_id, bundle, prov_admin_token)\n",
+ " return\n",
+ "\n",
+ " # Create a new user for the invoker\n",
+ " res = create_user(admin_token)\n",
+ " if len(res) == 0:\n",
+ " remove_publish_api(prov_user_uuid, api_id, bundle, prov_admin_token)\n",
+ " return\n",
+ " user_name, user_uuid = res\n",
+ "\n",
+ " # Get certificates\n",
+ " auth = get_auth(user_name, USER_PASSWORD)\n",
+ " if len(auth) == 0:\n",
+ " delete_user(user_name, admin_token)\n",
+ " remove_publish_api(prov_user_uuid, api_id, bundle, prov_admin_token)\n",
+ " return\n",
+ "\n",
+ " # Sanity checks\n",
+ " if auth['ca_root'] != ca_root:\n",
+ " raise Exception('CA root mismatch')\n",
+ " if auth['ccf_api_onboarding_url'] != ccf_api_onboarding_url:\n",
+ " raise Exception('CCF API onboarding URL mismatch')\n",
+ " if auth['ccf_discover_url'] != ccf_discover_url:\n",
+ " raise Exception('CCF discover URL mismatch')\n",
+ " if auth['ccf_onboarding_url'] != ccf_onboarding_url:\n",
+ " raise Exception('CCF onboarding URL mismatch')\n",
+ " if auth['ccf_publish_url'] != ccf_publish_url:\n",
+ " raise Exception('CCF publish URL mismatch')\n",
+ " if auth['ccf_security_url'] != ccf_security_url:\n",
+ " raise Exception('CCF security URL mismatch')\n",
+ " access_token = auth['access_token']\n",
+ "\n",
+ " # Onboad the API invoker\n",
+ " res = onboard_invoker('API Invoker', access_token)\n",
+ " if len(res) == 0:\n",
+ " delete_user(user_uuid, admin_token)\n",
+ " remove_publish_api(prov_user_uuid, api_id, bundle, prov_admin_token)\n",
+ " return\n",
+ "\n",
+ " # Do the discovery\n",
+ " invoker_id = res['apiInvokerId']\n",
+ " certs_bundle = (res['onboardingInformation']['apiInvokerCertificate'], res['csr'][1])\n",
+ " mec_api = discover(invoker_id, certs_bundle, access_token)\n",
+ " if len(mec_api) == 0:\n",
+ " files_bundle = offboard_invoker(invoker_id, certs_bundle)\n",
+ " if len(files_bundle) == 0:\n",
+ " for file in files_bundle:\n",
+ " os.remove(file)\n",
+ " delete_user(user_uuid, admin_token)\n",
+ " remove_publish_api(prov_user_uuid, api_id, bundle, prov_admin_token)\n",
+ " return\n",
+ "\n",
+ " # Extract the URL to access to the MEC Sandbox platform\n",
+ " addrs = extract_ipv4_and_uri(mec_api)\n",
+ " logger.debug('addrs: ' + str(addrs))\n",
+ "\n",
+ " # Discovering MEC services\n",
+ " url = ''\n",
+ " if 'ports' in addrs:\n",
+ " url = 'https://' + addrs['ipv4Addr'][0] + ':' + addrs['ports'][0]\n",
+ " else:\n",
+ " url = 'https://' + addrs['ipv4Addr'][0]\n",
+ " url += addrs['uri'][0]\n",
+ " mec_services = discovering_mec_services(url)\n",
+ " if len(mec_services) != 0:\n",
+ " logger.debug('===> The list of the MEC services exposed by ' + addrs['ipv4Addr'][0] + ' is: ' + str(mec_services))\n",
+ "\n",
+ " # Offboard the API invoker\n",
+ " files_bundle = offboard_invoker(invoker_id, certs_bundle)\n",
+ " if len(files_bundle) == 0:\n",
+ " for file in files_bundle:\n",
+ " os.remove(file)\n",
+ "\n",
+ " # Delete the invoker user\n",
+ " delete_user(user_uuid, admin_token)\n",
+ "\n",
+ " # Logout the invoker user\n",
+ " process_logout()\n",
+ "\n",
+ " # Remove the MEC profile for CAPIF API\n",
+ " remove_publish_api(prov_user_uuid, api_id, bundle, prov_admin_token)\n",
+ "\n",
+ "if __name__ == '__main__':\n",
+ " process_main()"
+ ],
+ "metadata": {
+ "id": "aTllbmoUpXKx"
+ },
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "### What to do next\n",
+ "\n",
+ "here is a list of several additional tasks yo can do to refine our CAPIF application:\n",
+ "1. Simply the code of the process_main() function above\n",
+ "2. Find the endpoint of the MEC Location API service (MEC 013) and send a request to get the list of zones available\n",
+ "3. Create your own CAPIF application"
+ ],
+ "metadata": {
+ "id": "cfy6D8wYt5GA"
+ }
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## Conlusion: what do we learn?\n",
+ "\n",
+ "The main objective of this tutorial is to demonstrate how to use the MEC profile for CAPIF (as described in MEC 011 v3.x.x Clause 9) in a CAPIF application. Along this tutrial, we learned how to develop a basic CAPIF application, including both API provider and API invoker. We learned also how to use a published API to send REQUEST to a MEC Sandbox platform instance.\n",
+ "\n",
+ "\n",
+ "**That's all Folks**\n",
+ "\n"
+ ],
+ "metadata": {
+ "id": "m_BbV7KdpX24"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/demo8/README.md b/examples/demo8/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..988ac0f669ceaf62b00a6abc56145e9e7a93fff3
--- /dev/null
+++ b/examples/demo8/README.md
@@ -0,0 +1,3 @@
+The main objective of this tutorial is to demonstrate how to use the MEC profile for CAPIF (as described in MEC 011 v3.x.x Clause 9) in a CAPIF application.
+Along this tutrial, we learned how to develop a basic CAPIF application, including both API provider and API invoker.
+We learned also how to use a published API to send REQUEST to a MEC Sandbox platform instance.
diff --git a/go-apps/meep-iot/Dockerfile b/go-apps/meep-iot/Dockerfile
index e7af4228809ba57f78dc77792b7c9ebe1c81fc7d..e7c8f06e520a19fa43d83e59ba7d6985b99707d8 100644
--- a/go-apps/meep-iot/Dockerfile
+++ b/go-apps/meep-iot/Dockerfile
@@ -1,4 +1,4 @@
-# Copyright (c) 2024 The AdvantEDGE Authors
+# Copyright (c) 2025 The AdvantEDGE Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
diff --git a/go-apps/meep-iot/main.go b/go-apps/meep-iot/main.go
index ff72f16d39f458bf680ee61df302114d025981bd..5114ba103670fbe80ada7f2ae962c955dfbf4a87 100644
--- a/go-apps/meep-iot/main.go
+++ b/go-apps/meep-iot/main.go
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024 The AdvantEDGE Authors
+ * Copyright (c) 2025 The AdvantEDGE Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/go-apps/meep-iot/main_test.go b/go-apps/meep-iot/main_test.go
index 5e054c3f011cf7b40f026148c216fe1a0f08a525..d22beaff7ae32edd7ed2d8ada7495eb071b16d97 100644
--- a/go-apps/meep-iot/main_test.go
+++ b/go-apps/meep-iot/main_test.go
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024 The AdvantEDGE Authors
+ * Copyright (c) 2025 The AdvantEDGE Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/go-apps/meep-iot/sbi/iot-sbi.go b/go-apps/meep-iot/sbi/iot-sbi.go
index 68f8d2168480ab88aa9a218a8f2db6e6a4e36de9..abdc11e9c6eb463f0ecc511f6a977c8139ca84e5 100644
--- a/go-apps/meep-iot/sbi/iot-sbi.go
+++ b/go-apps/meep-iot/sbi/iot-sbi.go
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024 The AdvantEDGE Authors
+ * Copyright (c) 2025 The AdvantEDGE Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -329,7 +329,7 @@ func RegisterIotPlatformInfo(iotPlatformInfo IotPlatformInfo) (responseData IotP
log.Info(">>> RegisterIotPlatformInfo: ", iotPlatformInfo)
// Populate the list of the devices for this IoT platform
- pltf := convertToIotMgr(iotPlatformInfo)
+ pltf := convertIotPlatformInfoToIotMgr(iotPlatformInfo)
err = sbi.iotMgr.RegisterIotPlatformInfo(pltf)
if err != nil {
return iotPlatformInfo, err
@@ -351,18 +351,25 @@ func DeregisterIotPlatformInfo(iotPlatformId string) (err error) {
}
func GetDevices() (devices []DeviceInfo, err error) {
- log.Info(">>> GetDevices")
+ log.Info(">>> sbi.GetDevices")
- _, err = sbi.iotMgr.GetDevices()
+ dev, err := sbi.iotMgr.GetDevices()
if err != nil {
- return devices, err
+ return nil, err
}
+ //log.Info("sbi.GetDevices: dev: ", dev)
+
+ devices, err = convertDeviceInfosFromIotMgr(dev)
+ if err != nil {
+ return nil, err
+ }
+ //log.Info("sbi.GetDevices: devices: ", devices)
return devices, nil
}
func GetDevice(deviceId string) (device DeviceInfo, err error) {
- log.Info(">>> GetDevice: ", deviceId)
+ log.Info(">>> sbi.GetDevice: ", deviceId)
_, err = sbi.iotMgr.GetDevice(deviceId)
if err != nil {
@@ -372,7 +379,7 @@ func GetDevice(deviceId string) (device DeviceInfo, err error) {
return device, nil
}
-func convertToIotMgr(val IotPlatformInfo) (item tm.IotPlatformInfo) {
+func convertIotPlatformInfoToIotMgr(val IotPlatformInfo) (item tm.IotPlatformInfo) {
item.IotPlatformId = val.IotPlatformId
item.Enabled = val.Enabled
for _, userTransportInfo := range val.UserTransportInfo {
@@ -452,3 +459,31 @@ func convertToIotMgr(val IotPlatformInfo) (item tm.IotPlatformInfo) {
return item
}
+
+func convertDeviceInfosFromIotMgr(devicesList []tm.DeviceInfo) (devices []DeviceInfo, err error) {
+ //log.Debug(">>> convertDeviceInfosFromIotMgr")
+
+ devices = make([]DeviceInfo, len(devicesList))
+ for idx, item := range devicesList { // FIXME FSCOM Add Filter
+ var device = DeviceInfo{
+ DeviceAuthenticationInfo: item.DeviceAuthenticationInfo,
+ Gpsi: item.Gpsi,
+ Pei: item.Pei,
+ Supi: item.Supi,
+ Msisdn: item.Msisdn,
+ Imei: item.Imei,
+ Imsi: item.Imsi,
+ Iccid: item.Iccid,
+ DeviceId: item.DeviceId,
+ RequestedIotPlatformId: item.RequestedIotPlatformId,
+ RequestedUserTransportId: item.RequestedUserTransportId,
+ ClientCertificate: item.ClientCertificate,
+ Enabled: item.Enabled,
+ }
+ // FIXME FSCOM Add missing fileds (pointers & arrays)
+ devices[idx] = device
+ } // End of 'for' statement
+ //log.Debug("convertDeviceInfosFromIotMgr: devices: ", devices)
+
+ return devices, nil
+}
diff --git a/go-apps/meep-iot/server/convert.go b/go-apps/meep-iot/server/convert.go
index 16f72cf6a5c56196760d6efc1fbbe59675f33e8e..b9d95d88d3af8e664bbaef50d03fc9faf2324fca 100644
--- a/go-apps/meep-iot/server/convert.go
+++ b/go-apps/meep-iot/server/convert.go
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024 The AdvantEDGE Authors
+ * Copyright (c) 2025 The AdvantEDGE Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/go-apps/meep-iot/server/logger.go b/go-apps/meep-iot/server/logger.go
index 7d76d08153f8d5e407b1b8a22c7a549dbdb14d87..9fb28ab2b19133a0a1b3122cbce6db90cc0b9d59 100644
--- a/go-apps/meep-iot/server/logger.go
+++ b/go-apps/meep-iot/server/logger.go
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024 The AdvantEDGE Authors
+ * Copyright (c) 2025 The AdvantEDGE Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/go-apps/meep-iot/server/meep-iot.go b/go-apps/meep-iot/server/meep-iot.go
index d8c205c218d1789e8ef4743acbc8ef6ec3e643bd..59bf6531bbdff4a1b262e85b95d31a33d1c0eee3 100644
--- a/go-apps/meep-iot/server/meep-iot.go
+++ b/go-apps/meep-iot/server/meep-iot.go
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024 The AdvantEDGE Authors
+ * Copyright (c) 2025 The AdvantEDGE Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -631,7 +631,7 @@ func registerediotplatformsByIdGET(w http.ResponseWriter, r *http.Request) {
} else {
var iotPlatformInfo IotPlatformInfo
if noFilter {
- iotPlatformInfo = convertFromSbi(val)
+ iotPlatformInfo = convertIotPlatformInfoFromSbi(val)
} else {
// FIXME FSCOM To be done
http.Error(w, "Filtering not implemented yet", http.StatusNotImplemented)
@@ -693,7 +693,7 @@ func registerediotplatformsGET(w http.ResponseWriter, r *http.Request) {
}
var iotPlatformInfos = []IotPlatformInfo{}
for _, val := range l {
- item := convertFromSbi(val)
+ item := convertIotPlatformInfoFromSbi(val)
iotPlatformInfos = append(iotPlatformInfos, item)
}
jsonResponse, err := json.Marshal(iotPlatformInfos)
@@ -790,7 +790,7 @@ func registerediotplatformsPOST(w http.ResponseWriter, r *http.Request) {
}
// Populate registeredIotPlatformsMap
- s := convertToSbi(requestData)
+ s := convertIotPlatformInfoToSbi(requestData)
responseData, err := sbi.RegisterIotPlatformInfo(s)
if err != nil {
log.Error("Failed to register IotPlatformInfo: ", err)
@@ -801,7 +801,7 @@ func registerediotplatformsPOST(w http.ResponseWriter, r *http.Request) {
log.Debug("registerediotplatformsPOST: new registeredIotPlatformsMap: ", registeredIotPlatformsMap)
// Prepare & send response
- c := convertFromSbi(responseData)
+ c := convertIotPlatformInfoFromSbi(responseData)
jsonResponse := convertIoTPlatformInfotoJson(&c)
w.WriteHeader(http.StatusCreated)
fmt.Fprint(w, jsonResponse)
@@ -855,7 +855,7 @@ func registereddevicesGET(w http.ResponseWriter, r *http.Request) {
errHandlerProblemDetails(w, err.Error(), http.StatusBadRequest)
return
}
- err = validateFilter(q["filter"], validParams)
+ filter, err := validateFilter(q["filter"], validParams)
if err != nil {
errHandlerProblemDetails(w, err.Error(), http.StatusBadRequest)
return
@@ -863,14 +863,21 @@ func registereddevicesGET(w http.ResponseWriter, r *http.Request) {
devicesList, err := sbi.GetDevices()
if err != nil {
+ log.Error("registereddevicesGET: ", err)
errHandlerProblemDetails(w, err.Error(), http.StatusInternalServerError)
return
}
+ log.Debug("registereddevicesGET: devicesList=", devicesList)
if len(devicesList) == 0 {
w.WriteHeader(http.StatusNotFound)
return
}
- devices, err := applyFiltering(devicesList)
+ var devices []DeviceInfo
+ if len(filter) == 0 {
+ devices, err = convertDeviceInfosFromSbi(devicesList)
+ } else {
+ devices, err = applyFiltering(devicesList, filter)
+ }
if err != nil {
errHandlerProblemDetails(w, err.Error(), http.StatusInternalServerError)
return
@@ -880,12 +887,22 @@ func registereddevicesGET(w http.ResponseWriter, r *http.Request) {
return
}
+ // Prepare & send the response
+ jsonResponse, err := json.Marshal(devices)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ log.Debug("registereddevicesGET: jsonResponse=", string(jsonResponse))
+ fmt.Fprint(w, string(jsonResponse))
w.WriteHeader(http.StatusOK)
}
-func applyFiltering(devicesList []sbi.DeviceInfo) (devices []DeviceInfo, err error) {
-
- return devices, nil
+func applyFiltering(devicesList []sbi.DeviceInfo, filter []string) (devices []DeviceInfo, err error) {
+ log.Debug(">>> applyFiltering")
+ devices, err = convertDeviceInfosFromSbi_with_filter(devicesList, filter)
+ log.Debug("After applyFiltering: devices: ", devices)
+ return devices, err
}
func registereddevicesPOST(w http.ResponseWriter, r *http.Request) {
@@ -912,28 +929,28 @@ func validateQueryParams(params url.Values, validParams []string) error {
return nil
}
-func validateFilter(filter []string, validParams []string) error {
+func validateFilter(filter []string, validParams []string) (f []string, err error) {
log.Debug(">>> validateFilter: ", filter)
for _, val := range filter {
log.Debug("validateFilter: Processing ", val)
val := strings.Trim(val, "()")
- f := strings.Split(val, ",")
+ f = strings.Split(val, ",")
if len(f) != 3 {
- return errors.New("validateFilter: Invalid filter structure: " + val)
+ return nil, errors.New("validateFilter: Invalid filter structure: " + val)
}
if _, ok := visitedOp[f[0]]; !ok {
- return errors.New("validateFilter: Invalid filter operator value: " + f[0])
+ return nil, errors.New("validateFilter: Invalid filter operator value: " + f[0])
}
if _, ok := visitedFilter[f[1]]; !ok {
- return errors.New("validateFilter: Invalid filter field value: " + f[1])
+ return nil, errors.New("validateFilter: Invalid filter field value: " + f[1])
}
} // End of 'for' statement
- return nil
+ return f, nil
}
-func convertFromSbi(val sbi.IotPlatformInfo) (item IotPlatformInfo) {
+func convertIotPlatformInfoFromSbi(val sbi.IotPlatformInfo) (item IotPlatformInfo) {
item.IotPlatformId = val.IotPlatformId
item.Enabled = val.Enabled
@@ -1015,7 +1032,7 @@ func convertFromSbi(val sbi.IotPlatformInfo) (item IotPlatformInfo) {
return item
}
-func convertToSbi(val IotPlatformInfo) (item sbi.IotPlatformInfo) {
+func convertIotPlatformInfoToSbi(val IotPlatformInfo) (item sbi.IotPlatformInfo) {
item.IotPlatformId = val.IotPlatformId
item.Enabled = val.Enabled
for _, userTransportInfo := range val.UserTransportInfo {
@@ -1095,3 +1112,75 @@ func convertToSbi(val IotPlatformInfo) (item sbi.IotPlatformInfo) {
return item
}
+
+func convertDeviceInfosFromSbi(devicesList []sbi.DeviceInfo) (devices []DeviceInfo, err error) {
+ devices = make([]DeviceInfo, len(devicesList))
+ for idx, item := range devicesList { // FIXME FSCOM Add Filter
+ var device = DeviceInfo{
+ DeviceAuthenticationInfo: item.DeviceAuthenticationInfo,
+ Gpsi: item.Gpsi,
+ Pei: item.Pei,
+ Supi: item.Supi,
+ Msisdn: item.Msisdn,
+ Imei: item.Imei,
+ Imsi: item.Imsi,
+ Iccid: item.Iccid,
+ DeviceId: item.DeviceId,
+ RequestedIotPlatformId: item.RequestedIotPlatformId,
+ RequestedUserTransportId: item.RequestedUserTransportId,
+ ClientCertificate: item.ClientCertificate,
+ Enabled: item.Enabled,
+ }
+ // FIXME FSCOM Add missing fileds (pointers & arrays)
+ devices[idx] = device
+ } // End of 'for' statement
+ //log.Debug("convertDeviceInfosFromSbi: devices: ", devices)
+
+ return devices, nil
+}
+
+func convertDeviceInfosFromSbi_with_filter(devicesList []sbi.DeviceInfo, filter []string) (devices []DeviceInfo, err error) {
+ log.Debug(">>> convertDeviceInfosFromSbi_with_filter: ", filter)
+
+ devices = make([]DeviceInfo, 0)
+ for _, item := range devicesList { // FIXME FSCOM Add Filter
+ process_it := false
+ if filter[0] == "eq" { // E.g. [eq requestedUserTransportId 4ba6bf28-a748-4b35-aba6-d882a70c4337]
+ if filter[1] == "gpsi" {
+ process_it = item.Gpsi == filter[2]
+ } else if filter[1] == "Msisdn" {
+ process_it = item.Gpsi == filter[2]
+ } else if filter[1] == "deviceId" {
+ process_it = item.DeviceId == filter[2]
+ } else if filter[1] == "requestedIotPlatformId" {
+ process_it = item.RequestedIotPlatformId == filter[2]
+ } else if filter[1] == "requestedUserTransportId" {
+ process_it = item.RequestedUserTransportId == filter[2]
+ } // FIXME FSCOM Add support of deviceMetadata & requestedMecTrafficRule
+ }
+ if process_it {
+ var device = DeviceInfo{
+ DeviceAuthenticationInfo: item.DeviceAuthenticationInfo,
+ Gpsi: item.Gpsi,
+ Pei: item.Pei,
+ Supi: item.Supi,
+ Msisdn: item.Msisdn,
+ Imei: item.Imei,
+ Imsi: item.Imsi,
+ Iccid: item.Iccid,
+ DeviceId: item.DeviceId,
+ RequestedIotPlatformId: item.RequestedIotPlatformId,
+ RequestedUserTransportId: item.RequestedUserTransportId,
+ ClientCertificate: item.ClientCertificate,
+ Enabled: item.Enabled,
+ }
+ // FIXME FSCOM Add missing fileds (pointers & arrays)
+ devices = append(devices, device)
+ } else {
+ log.Debug("convertDeviceInfosFromSbi_with_filter: skip ", item)
+ }
+ } // End of 'for' statement
+ //log.Debug("convertDeviceInfosFromSbi_with_filter: devices: ", devices)
+
+ return devices, nil
+}
diff --git a/go-apps/meep-iot/server/routers.go b/go-apps/meep-iot/server/routers.go
index ffbd1480cabb910bf7a99c201a77a292be05006c..d08ea39e9f13ace61b8b88580c3c605d818981f6 100644
--- a/go-apps/meep-iot/server/routers.go
+++ b/go-apps/meep-iot/server/routers.go
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024 The AdvantEDGE Authors
+ * Copyright (c) 2025 The AdvantEDGE Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/go-apps/meep-sss/Dockerfile b/go-apps/meep-sss/Dockerfile
index 7a36e2fe12360c0376916db7540359fd35be7eb2..b238bbe6ce2837d139a7e287ad1c7c0fa37bd8b0 100644
--- a/go-apps/meep-sss/Dockerfile
+++ b/go-apps/meep-sss/Dockerfile
@@ -1,4 +1,4 @@
-# Copyright (c) 2024 The AdvantEDGE Authors
+# Copyright (c) 2025 The AdvantEDGE Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
diff --git a/go-apps/meep-sss/main.go b/go-apps/meep-sss/main.go
index 029675a8a9773ca17872ab42c3802028fffeca99..6a4c028f695a0d2a12ac2eb17a3d7a8852d3dd13 100644
--- a/go-apps/meep-sss/main.go
+++ b/go-apps/meep-sss/main.go
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024 The AdvantEDGE Authors
+ * Copyright (c) 2025 The AdvantEDGE Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/go-apps/meep-sss/main_test.go b/go-apps/meep-sss/main_test.go
index 5e054c3f011cf7b40f026148c216fe1a0f08a525..d22beaff7ae32edd7ed2d8ada7495eb071b16d97 100644
--- a/go-apps/meep-sss/main_test.go
+++ b/go-apps/meep-sss/main_test.go
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024 The AdvantEDGE Authors
+ * Copyright (c) 2025 The AdvantEDGE Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/go-apps/meep-sss/sbi/sss-sbi.go b/go-apps/meep-sss/sbi/sss-sbi.go
index 7612d96481a942ac32c6fd162a008835aaf164db..7022b91c790c5909843da23d53761c3d660f7579 100644
--- a/go-apps/meep-sss/sbi/sss-sbi.go
+++ b/go-apps/meep-sss/sbi/sss-sbi.go
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024 The AdvantEDGE Authors
+ * Copyright (c) 2025 The AdvantEDGE Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/go-apps/meep-sss/server/convert.go b/go-apps/meep-sss/server/convert.go
index 9afb971f2a91b8cd38e636442662fd5e22ec2efb..611aaef890805964d915ef5ec0a803c358fcf921 100644
--- a/go-apps/meep-sss/server/convert.go
+++ b/go-apps/meep-sss/server/convert.go
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024 The AdvantEDGE Authors
+ * Copyright (c) 2025 The AdvantEDGE Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/go-apps/meep-sss/server/meep-sss.go b/go-apps/meep-sss/server/meep-sss.go
index dd03e04775309d5da3f2045fb9c79198a8d8e7e9..e7baecb400916ce8c32d9389f459cf3a966f3a0d 100644
--- a/go-apps/meep-sss/server/meep-sss.go
+++ b/go-apps/meep-sss/server/meep-sss.go
@@ -24,6 +24,7 @@ import (
"net/http"
"net/url"
"os"
+ "reflect"
"strconv"
"strings"
"time"
@@ -555,6 +556,15 @@ func sensorDiscoveryLookupGET(w http.ResponseWriter, r *http.Request) {
iotPlatformIdParamStr := vars["registeredIotPlatformId"]
log.Debug("sensorDiscoveryLookupGET: iotPlatformIdParamStr: ", iotPlatformIdParamStr)
+ // Validate query parameters
+ u, _ := url.Parse(r.URL.String())
+ q := u.Query()
+ log.Debug("sensorDiscoveryLookupGET: q: ", q)
+ log.Debug("sensorDiscoveryLookupGET: q[type][0]: ", q["type"][0])
+ log.Debug("sensorDiscoveryLookupGET: q[sensorCharacteristicList][0]: ", q["sensorCharacteristicList"][0])
+ log.Debug("sensorDiscoveryLookupGET: type(q[sensorCharacteristicList][0]): ", reflect.TypeOf(q["sensorCharacteristicList"][0]))
+ //q: map[geographicalArea:[[object Object]] sensorCharacteristicList:[[object Object]] sensorPropertyList:[string1,string2] type:[string]]","time":"2025-02-04T08:35:35Z"}
+
w.WriteHeader(http.StatusOK)
}
diff --git a/go-apps/meep-sss/server/model_app_termination_notification.go b/go-apps/meep-sss/server/model_app_termination_notification.go
index 50ded34cc8b9d7b998e0cac7823cc733575294cf..2593cd5c2f7b6161318509cbd5a519c384c7adea 100644
--- a/go-apps/meep-sss/server/model_app_termination_notification.go
+++ b/go-apps/meep-sss/server/model_app_termination_notification.go
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024 The AdvantEDGE Authors
+ * Copyright (c) 2025 The AdvantEDGE Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/go-apps/meep-sss/server/model_app_termination_notification__links.go b/go-apps/meep-sss/server/model_app_termination_notification__links.go
index 8cefa51894114feb0184996c12c7f95d94b8cb66..3035fbaeb9196721b6f537e9845b9ff18fc978ca 100644
--- a/go-apps/meep-sss/server/model_app_termination_notification__links.go
+++ b/go-apps/meep-sss/server/model_app_termination_notification__links.go
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024 The AdvantEDGE Authors
+ * Copyright (c) 2025 The AdvantEDGE Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/go-apps/meep-sss/server/model_operation_action_type.go b/go-apps/meep-sss/server/model_operation_action_type.go
index 17f93c981b27a80b22cdc85b5eeb6d2b77ffd9af..8122ab94e70dbf831bd2de08249de7c8ae4fdd68 100644
--- a/go-apps/meep-sss/server/model_operation_action_type.go
+++ b/go-apps/meep-sss/server/model_operation_action_type.go
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024 The AdvantEDGE Authors
+ * Copyright (c) 2025 The AdvantEDGE Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/go-apps/meep-sss/server/routers.go b/go-apps/meep-sss/server/routers.go
index b45f68e95ef7d10b948b9cd4cbe6ecf6ecf3daf1..aa27a304f5ea8ce5844772895185d4e8cba2ff61 100644
--- a/go-apps/meep-sss/server/routers.go
+++ b/go-apps/meep-sss/server/routers.go
@@ -69,147 +69,147 @@ var routes = Routes{
Route{
"Index",
"GET",
- "/sandboxname/sens/v1/",
+ "/sens/v1/",
Index,
},
Route{
"SensorMgmtPUT",
strings.ToUpper("Put"),
- "/sandboxname/sens/v1/sensor_management",
+ "/sens/v1/sensor_management",
SensorMgmtPUT,
},
Route{
"SensorDataLookupGET",
strings.ToUpper("Get"),
- "/sandboxname/sens/v1/queries/status_data",
+ "/sens/v1/queries/status_data",
SensorDataLookupGET,
},
Route{
"SensorDataIndividualSubscriptionGET",
strings.ToUpper("Get"),
- "/sandboxname/sens/v1/queries/status_data/{subscriptionId}",
+ "/sens/v1/queries/status_data/{subscriptionId}",
SensorDataIndividualSubscriptionGET,
},
Route{
"SensorDataSubscriptionDELETE",
strings.ToUpper("Delete"),
- "/sandboxname/sens/v1/queries/status_data/{subscriptionId}",
+ "/sens/v1/queries/status_data/{subscriptionId}",
SensorDataSubscriptionDELETE,
},
Route{
"SensorDataSubscriptionGET",
strings.ToUpper("Get"),
- "/sandboxname/sens/v1/subscriptions/sensor_data",
+ "/sens/v1/subscriptions/sensor_data",
SensorDataSubscriptionGET,
},
Route{
"SensorDataSubscriptionPOST",
strings.ToUpper("Post"),
- "/sandboxname/sens/v1/subscriptions/sensor_data",
+ "/sens/v1/subscriptions/sensor_data",
SensorDataSubscriptionPOST,
},
Route{
"SensorDataSubscriptionPUT",
strings.ToUpper("Put"),
- "/sandboxname/sens/v1/queries/status_data/{subscriptionId}",
+ "/sens/v1/queries/status_data/{subscriptionId}",
SensorDataSubscriptionPUT,
},
Route{
"SensorDiscoveryLookupGET",
strings.ToUpper("Get"),
- "/sandboxname/sens/v1/queries/sensor_discovery",
+ "/sens/v1/queries/sensor_discovery",
SensorDiscoveryLookupGET,
},
Route{
"SensorDiscoveryIndividualSubscriptionGET",
strings.ToUpper("Get"),
- "/sandboxname/sens/v1/subscriptions/sensor_discovery/{subscriptionId}",
+ "/sens/v1/subscriptions/sensor_discovery/{subscriptionId}",
SensorDiscoveryIndividualSubscriptionGET,
},
Route{
"SensorDiscoverySubscriptionDELETE",
strings.ToUpper("Delete"),
- "/sandboxname/sens/v1/subscriptions/sensor_discovery/{subscriptionId}",
+ "/sens/v1/subscriptions/sensor_discovery/{subscriptionId}",
SensorDiscoverySubscriptionDELETE,
},
Route{
"SensorDiscoverySubscriptionGET",
strings.ToUpper("Get"),
- "/sandboxname/sens/v1/subscriptions/sensor_discovery",
+ "/sens/v1/subscriptions/sensor_discovery",
SensorDiscoverySubscriptionGET,
},
Route{
"SensorDiscoverySubscriptionPOST",
strings.ToUpper("Post"),
- "/sandboxname/sens/v1/subscriptions/sensor_discovery",
+ "/sens/v1/subscriptions/sensor_discovery",
SensorDiscoverySubscriptionPOST,
},
Route{
"SensorDiscoverySubscriptionPUT",
strings.ToUpper("Put"),
- "/sandboxname/sens/v1/subscriptions/sensor_discovery/{subscriptionId}",
+ "/sens/v1/subscriptions/sensor_discovery/{subscriptionId}",
SensorDiscoverySubscriptionPUT,
},
Route{
"SensorMgmtGET",
strings.ToUpper("Get"),
- "/sandboxname/sens/v1/sensor_management",
+ "/sens/v1/sensor_management",
SensorMgmtGET,
},
Route{
"SensorStatusLookupGET",
strings.ToUpper("Get"),
- "/sandboxname/sens/v1/queries/sensor_status",
+ "/sens/v1/queries/sensor_status",
SensorStatusLookupGET,
},
Route{
"SensorSatusIndividualSubscriptionGET",
strings.ToUpper("Get"),
- "/sandboxname/sens/v1/subscriptions/sensor_status/{subscriptionId}",
+ "/sens/v1/subscriptions/sensor_status/{subscriptionId}",
SensorSatusIndividualSubscriptionGET,
},
Route{
"SensorStatusIndividualSubscriptionGET",
strings.ToUpper("Get"),
- "/sandboxname/sens/v1/subscriptions/sensor_status",
+ "/sens/v1/subscriptions/sensor_status",
SensorStatusIndividualSubscriptionGET,
},
Route{
"SensorStatusSubscriptionDELETE",
strings.ToUpper("Delete"),
- "/sandboxname/sens/v1/subscriptions/sensor_status/{subscriptionId}",
+ "/sens/v1/subscriptions/sensor_status/{subscriptionId}",
SensorStatusSubscriptionDELETE,
},
Route{
"SensorStatusSubscriptionPUT",
strings.ToUpper("Put"),
- "/sandboxname/sens/v1/subscriptions/sensor_status/{subscriptionId}",
+ "/sens/v1/subscriptions/sensor_status/{subscriptionId}",
SensorStatusSubscriptionPUT,
},
Route{
"SensorStatusSubscriptionPOST",
strings.ToUpper("Post"),
- "/sandboxname/sens/v1/subscriptions/sensor_status",
+ "/sens/v1/subscriptions/sensor_status",
SensorStatusSubscriptionPOST,
},
diff --git a/go-packages/meep-iot-mgr/iot-mgr.go b/go-packages/meep-iot-mgr/iot-mgr.go
index 61d1bd78fbd9853d269b2599d29894cbab7cfdde..6c9313d54963609e08193939baebbf2d3d20c154 100644
--- a/go-packages/meep-iot-mgr/iot-mgr.go
+++ b/go-packages/meep-iot-mgr/iot-mgr.go
@@ -23,7 +23,9 @@ import (
"io"
"io/ioutil"
"net/http"
+ "reflect"
"strconv"
+ "strings"
"sync"
"time"
@@ -99,6 +101,13 @@ type ImplSpecificInfo struct {
DownlinkTopics []string
}
+// ETSI GS MEC 046 V3.1.1 (2024-04) Clause 6.2.1 Type: SensorDiscoveryInfo
+type SensorCharacteristic struct {
+ CharacteristicName string
+ CharacteristicValue string
+ CharacteristicUnitOfMeasure *string
+}
+
type DeviceInfo struct {
DeviceAuthenticationInfo string
//DeviceMetadata []KeyValuePair
@@ -115,8 +124,12 @@ type DeviceInfo struct {
RequestedUserTransportId string
//DeviceSpecificMessageFormats *DeviceSpecificMessageFormats
//DownlinkInfo *DownlinkInfo
- ClientCertificate string
- Enabled bool
+ ClientCertificate string
+ Enabled bool
+ SensorIdentifier string
+ SensorStatusType string
+ SensorPropertyList []string
+ SensorCharacteristicList []SensorCharacteristic
}
var registeredIotPlatformsMap = map[string]IotPlatformInfo{} // List of discovered IOT Plateform
@@ -212,19 +225,26 @@ func (tm *IotMgr) GetDevices() (devices []DeviceInfo, err error) {
profilingTimers["GetDevices"] = time.Now()
}
- log.Info(">>> GetDevices: iotPlatformId")
+ log.Info(">>> GetDevices")
+ devices = make([]DeviceInfo, 0)
if len(registeredIotPlatformsMap) == 0 {
return devices, nil
}
// Refresh the list of devices
- for _, val := range registeredIotPlatformsMap {
- err := populateDevices(val.IotPlatformId)
+ for _, iotPlatform := range registeredIotPlatformsMap {
+ log.Info("GetDevices: processing: ", iotPlatform.IotPlatformId)
+ err := populateDevices(iotPlatform)
if err != nil {
+ log.Error("GetDevices: ", err)
continue
}
- }
+ for _, v := range devicesPerPlatformMap[iotPlatform.IotPlatformId] {
+ log.Info("GetDevices: adding device: ", v)
+ devices = append(devices, devicesMap[v])
+ }
+ } // End of 'for' statement
log.Info("GetDevices: devices: ", devices)
if profiling {
@@ -257,76 +277,155 @@ func (tm *IotMgr) GetDevice(deviceId string) (device DeviceInfo, err error) {
}
/*
- * func (tm *IotMgr) PopulateDevices IoT devices for the specified Iot platform
+ * func PopulateDevices IoT devices for the specified Iot platform
* @param {string} iotPlatformId contains the IoT platform identifier
* @return {struct} nil on success, error otherwise
*/
-func populateDevices(iotPlatformId string) error {
+func populateDevices(iotPlatformInfo IotPlatformInfo) error {
if profiling {
profilingTimers["populateDevices"] = time.Now()
}
- log.Info(">>> populateDevices: iotPlatformId=", iotPlatformId)
+ log.Info(">>> populateDevices: iotPlatformId=", iotPlatformInfo.IotPlatformId)
+ // 1. Get the list of the AE
// Build the URL
- t := registeredIotPlatformsMap[iotPlatformId].CustomServicesTransportInfo[0]
+ t := registeredIotPlatformsMap[iotPlatformInfo.IotPlatformId].CustomServicesTransportInfo[0]
log.Info("populateDevices: t.Endpoint.Addresses[0]=", t.Endpoint.Addresses[0])
url := "http://" + t.Endpoint.Addresses[0].Host + ":" + strconv.Itoa(int(t.Endpoint.Addresses[0].Port)) + "/" + t.Name
- // Builkd the headers
+ log.Info("populateDevices: url=", url)
+ // Build the headers
var headers = http.Header{}
headers["Accept"] = []string{headerAccept}
headers["ContentType"] = []string{headerContentType}
headers["X-M2M-Origin"] = []string{"CAdmin"}
headers["X-M2M-RI"] = []string{uuid.New().String()}
headers["X-M2M-RVI"] = []string{t.Version}
+ // Build the queries
+ queries := map[string]string{}
+ queries["fu"] = "1" // Filter usage
+ queries["ty"] = "4" // Filter on oneM2M CIN for device
// Send the request
- response, err := sendRequest("GET", url, headers, nil, nil, nil, 200)
+ response, err := sendRequest("GET", url, headers, nil, nil, queries, 200)
if err != nil {
log.Error("populateDevices: ", err.Error())
return err
}
log.Debug("populateDevices: response: ", string(response))
- var oneM2M_data map[string]interface{}
- err = json.Unmarshal(response, &oneM2M_data)
+ var oneM2M_uril map[string][]string
+ err = json.Unmarshal(response, &oneM2M_uril)
if err != nil {
log.Error("populateDevices: ", err.Error())
return err
}
- log.Debug("populateDevices: oneM2M_data: ", len(oneM2M_data))
- log.Debug(oneM2M_data)
- for k := range oneM2M_data {
- log.Info("populateDevices: Processing key: ", k)
- var device = DeviceInfo{
- DeviceId: "",
- RequestedIotPlatformId: "",
+ log.Debug("populateDevices: oneM2M_uril: ", len(oneM2M_uril))
+ log.Debug(oneM2M_uril)
+ if _, ok := oneM2M_uril["m2m:uril"]; !ok {
+ err := errors.New("populateDevices: Key not found: m2m:uril")
+ log.Error(err.Error())
+ return err
+ }
+ // Loop for each CIN and build the device list
+ for _, v := range oneM2M_uril["m2m:uril"] {
+ log.Info("populateDevices: Processing key: ", v)
+
+ url := "http://" + t.Endpoint.Addresses[0].Host + ":" + strconv.Itoa(int(t.Endpoint.Addresses[0].Port)) + "/" + v
+ log.Info("populateDevices: url=", url)
+ // Build the headers
+ var headers = http.Header{}
+ headers["Accept"] = []string{headerAccept}
+ headers["ContentType"] = []string{headerContentType}
+ headers["X-M2M-Origin"] = []string{"CAdmin"}
+ headers["X-M2M-RI"] = []string{uuid.New().String()}
+ headers["X-M2M-RVI"] = []string{t.Version}
+ // Build the queries
+ queries := map[string]string{}
+ queries["fu"] = "2" // Filter usage
+ // Send the request
+ response, err := sendRequest("GET", url, headers, nil, nil, queries, 200)
+ if err != nil {
+ log.Error("populateDevices: ", err.Error())
+ return err
+ }
+ log.Debug("populateDevices: response: ", string(response))
+ var oneM2M_cin map[string]map[string]interface{}
+ err = json.Unmarshal(response, &oneM2M_cin)
+ if err != nil {
+ log.Error("populateDevices: ", err.Error())
+ continue
}
+ //log.Debug("populateDevices: type(oneM2M_cin): ", reflect.TypeOf(oneM2M_cin))
+ //log.Debug("populateDevices: len(oneM2M_cin): ", len(oneM2M_cin))
+ //log.Debug("populateDevices: oneM2M_cin: ", oneM2M_cin)
+ for _, m := range oneM2M_cin {
+ //log.Debug("==> ", i, " value is ", m)
+ var device = DeviceInfo{}
+ device.RequestedIotPlatformId = iotPlatformInfo.IotPlatformId
+ device.RequestedUserTransportId = registeredIotPlatformsMap[iotPlatformInfo.IotPlatformId].UserTransportInfo[0].Id
+ device.Enabled = true
+
+ // m is a map[string]interface.
+ // loop over keys and values in the map.
+ for k, v := range m {
+ log.Debug(k, " value is ", v)
+ log.Debug("populateDevices: type(v): ", reflect.TypeOf(v))
+ if k == "rn" {
+ if item, ok := v.(string); ok {
+ device.DeviceId = item
+ } else {
+ log.Error("populateDevices: Failed to process ", k)
+ }
+ } else if k == "ri" {
+ if item, ok := v.(string); ok {
+ device.SensorIdentifier = item
+ } else {
+ log.Error("populateDevices: Failed to process ", k)
+ }
+ } else if k == "ty" {
+ if item, ok := v.(float64); ok {
+ device.SensorStatusType = strconv.FormatFloat(item, 'f', -1, 64)
+ } else {
+ log.Error("populateDevices: Failed to process ", k)
+ }
+ } else { // default: if k == "lt" || k == "et" || k == "ct" || k == "st" || k == "pi" || k == "lbl" {
+ if item, ok := v.(string); ok {
+ device.SensorCharacteristicList = append(
+ device.SensorCharacteristicList,
+ SensorCharacteristic{
+ CharacteristicName: k,
+ CharacteristicValue: string(item),
+ CharacteristicUnitOfMeasure: nil,
+ })
+ } else if item, ok := v.(float64); ok {
+ device.SensorCharacteristicList = append(
+ device.SensorCharacteristicList,
+ SensorCharacteristic{
+ CharacteristicName: k,
+ CharacteristicValue: strconv.FormatFloat(item, 'f', -1, 64),
+ CharacteristicUnitOfMeasure: nil,
+ })
+ } else if item, ok := v.([]string); ok {
+ device.SensorCharacteristicList = append(
+ device.SensorCharacteristicList,
+ SensorCharacteristic{
+ CharacteristicName: k,
+ CharacteristicValue: strings.Join(item, ","),
+ CharacteristicUnitOfMeasure: nil,
+ })
+ } else {
+ log.Error("populateDevices: Failed to process ", k)
+ }
+ }
+ } // End of 'for' loop
+ log.Info("populateDevices: device: ", device)
+ devicesMap[device.DeviceId] = device
+ devicesPerPlatformMap[device.RequestedIotPlatformId] = append(devicesPerPlatformMap[device.RequestedIotPlatformId], device.DeviceId)
+ } // End of 'for' loop
- // type DeviceInfo struct {
- // DeviceAuthenticationInfo string
- // //DeviceMetadata []KeyValuePair
- // Gpsi string
- // Pei string
- // Supi string
- // Msisdn string
- // Imei string
- // Imsi string
- // Iccid string
- // DeviceId string
- // //RequestedMecTrafficRule []TrafficRuleDescriptor
- // RequestedIotPlatformId string
- // RequestedUserTransportId string
- // //DeviceSpecificMessageFormats *DeviceSpecificMessageFormats
- // //DownlinkInfo *DownlinkInfo
- // ClientCertificate string
- // Enabled bool
- // }
- devicesMap[device.DeviceId] = device
- devicesPerPlatformMap[device.RequestedIotPlatformId] = append(devicesPerPlatformMap[device.RequestedIotPlatformId], device.DeviceId)
} // End of 'for' statement
- // for _, val := range devices {
- // }
- // log.Info("populateDevices: devices: ", devices)
+ log.Info("populateDevices: devicesMap: ", devicesMap)
+ log.Info("populateDevices: devicesPerPlatformMap: ", devicesPerPlatformMap)
if profiling {
now := time.Now()