diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000000000000000000000000000000000..b68fdd05a15475089a5d041dbd8c40ccdfe4937f --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 200 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..0781cbd1f4bee41f28d792a0dd31275b4a41f420 --- /dev/null +++ b/.gitignore @@ -0,0 +1,181 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +*/logs + + +config + + +*.vscode + +.flake8 + +*.DS_store + +*/__pycache__ + + +*.capif_sdk_config_sample_test.json +/test/capif_sdk_config_sample_test.json + diff --git a/LICENSE b/LICENSE index 2e3619cd6aa56222d71eccd7c901b77ab1737836..8d4113aed5e0485d758cad211a82ad1ab7a0aa26 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,10 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2024 OCF + Copyright (c) 2021, Stavros Kolometsos + Copyright (c) 2022, Telefónica Innovación Digital + Copyright (c) 2022, Fogus Innovations & Services P.C. + Copyright (c) 2024, European Telecommunications Standards Institute (ETSI) 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/README.md b/README.md index 9ba26a03fa7b1b51a7b3eb220d77a7dbbc6cdc5c..84068878ead2b95461fbe7cfabe614b813f8b527 100644 --- a/README.md +++ b/README.md @@ -1,92 +1,323 @@ -# sdk +# OpenCAPIF SDK +[![PyPI version](https://img.shields.io/pypi/v/opencapif-sdk.svg)](https://pypi.org/project/opencapif-sdk/) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) ![Python](https://img.shields.io/badge/python-v3.12+-blue.svg) [![PyPI - Downloads](https://img.shields.io/pypi/dm/opencapif-sdk)](https://pypi.org/project/opencapif-sdk/) + +![OpenCAPIF icon](./doc/images/opencapif_icon.jpg) + +This repository develops a Python Software Development Kit(SDK) which focuses on connecting to OpenCAPIF (Common API Framework for 3GPP Northbound APIs) in a simple way, lowering integration complexity and allowing developers to focus on Network Applications (Network Apps) or services development. + +OpenCAPIF SDK provides a set of libraries to enable either CAPIF provider and invoker roles, and other functions to simplify procedures calls towards OpenCAPIF entity. + +Current version of OpenCAPIF SDK is compatible with following publicly available releases: +- [OpenCAPIF Release 1.0](https://ocf.etsi.org/documentation/v1.0.0-release/) +- OpenCAPIF Release 2.0 +This document serves as the [main bootstrap reference](#networkapp-developer-path) to start working with OpenCAPIF SDK. For advanced users, refer to [OpenCAPIF full documentation](./doc/sdk_full_documentation.md) section to dig into all available features. +# Table of Contents -## Getting started + 1. [Repository structure](#repository-structure) + 2. [Network App developers](#network-app-developers) + 3. [OpenCAPIF SDK summary](#opencapif-sdk-summary) + 4. [OpenCAPIF SDK requirements](#opencapif-sdk-requirements) + 5. [OpenCAPIF sdk installation](#opencapif-sdk-installation) + 6. [OpenCAPIF SDK data schema](#opencapif-sdk-data-schema) + 7. [OpenCAPIF SDK Configuration](./doc/sdk_configuration.md) + 8. [Network App developer path](#network-app-developer-path) + 1. [Provider Network App](#provider-network-app) + * [Provider Network App sample](#provider-network-app-sample) + 2. [Invoker Network App](#invoker-network-app) + * [Provider Network App sample](#provider-network-app-sample) + 9. [**OpenCAPIF SDK full documentation**](./doc/sdk_full_documentation.md) + 10. [OpenCAPIF SDK known issues](#opencapif-sdk-known-issues) -To make it easy for you to get started with GitLab, here's a list of recommended next steps. -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! +# Repository structure -## Add your files + pesp_capif_sdk + ├── config + ├── doc + │   └── images + ├── installation + ├── network_app_samples + │   ├── network_app_invoker_sample + │   │   └── postman + │   └── network_app_provider_sample + ├── samples + ├── scripts + ├── opencapif_sdk + └── test -- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: +- [config](./config/): contains OpenCAPIF SDK configuration files samples. These samples illustrate the structure of the configuration files ir order to use SDK properly. Go to the [configuration section](./doc/sdk_configuration.md) for more details, +- [doc](./doc/): contains documentation related files to this README, +- [installation](./installation/): this folder stores the Python [requeriments.txt](./installation/requirements.txt) file that is required to complete the [SDK developers section](./doc/sdk_developers.md), +- [network_app_samples](./network_app_samples/): this folder contains both provider and invoker Network App samples explained further in this document at [network app developer path](#network-app-developer-path), +- [samples](./samples/): contains sample files related to SDK configuration, API definitions and SDK configuration via environment variables, +- [scripts](./scripts/): single scripts to individually test functionality though command line. For more information on how to use these go to the [full documentation section](./doc/sdk_full_documentation.md), +- [opencapif_sdk](./opencapif_sdk/): where SDK code is stored, +- [test](./test/): contains a file named test.py containing tests to ensure all SDK flows work properly. +# Network App developers + +In the scope of CAPIF, a Network App (Network Application) refers to an external application or service that interacts with the 3GPP network via standardized APIs. These Network Apps typically leverage the capabilities and services provided by the underlying mobile network infrastructure, such as network slicing, quality of service (QoS), or location services. + +Network Apps can be developed by third-party service providers, network operators, or other stakeholders to offer a wide range of services, including enhanced communication features, IoT solutions, or content delivery, and they use CAPIF as the unified framework for securely discovering, accessing, and utilizing 3GPP network APIs. + +Next image illustrates how CAPIF works and where the SDK provides means to integrate with it: + +![CAPIF-illustration](./doc/images/flows_capif_illustration.jpg) + +For that purpose Network Apps play 2 different roles when interacting with CAPIF: +- **Invoker**: a Network App acting as an Invoker is responsible for consuming APIs exposed by other services. This role represents an external application or service that calls the 3GPP northbound APIs to utilize the network’s functionalities. + +- **Provider**: a Network App acting as a Provider is responsible for exposing its own APIs/services for use by Invokers. This role represents an entity that offers services through APIs, making them available to other external applications or Invokers.A provider also is distinguished for having three parts. + + - The **AMF (API Management Function)**, supplies the API provider domain with administrative capabilities. Some of these capabilities include, auditing the service API invocation logs received from the CCF, on-boarding/off-boarding new API invokers and monitoring the status of the service APIs.One provider can have only one AMF. + + - The **APF (API Publishing Function)**, is responsible for the publication of the service APIs to CCF in order to enable the discovery capability to the API Invokers.One provider can have multiple APFs. + + - The **AEF (API Exposing Function)**, is responsible for the exposure of the service APIs. Assuming that API Invokers are authorized by the CCF, AEF validates the authorization and subsequently provides the direct communication entry points to the service APIs. AEF may also authorize API invokers and record the invocations in log files.One provider can have multiple AEFs + +OpenCAPIF SDK brings a set of functions to integrate with the 5G Core's function CAPIF, as defined in [3GPP Technical Specification (TS) 29.222 V18.5.0 Common API Framework for 3GPP Northbound APIs](https://www.etsi.org/deliver/etsi_ts/129200_129299/129222/18.05.00_60/ts_129222v180500p.pdf). This section shows the mapping between the Python functions available in this SDK and the CAPIF OpenAPI APIs defined the reference standard: + +| **3GPP CAPIF API** | **OpenCAPIF SDK function** | **Description** | +|-------------------------------------------------------|-------------------------------------------------------------|-------------------------------------------------------------| +| /onboardedInvokers (POST) | [onboard_invoker()](./doc/sdk_full_documentation.md#invoker-onboarding) | Registers a new invoker. | +| /onboardedInvokers/{onboardingId} (PUT) | [update_invoker()](./doc/sdk_full_documentation.md#update-and-offboard-invoker) | Updates an existing invoker for a specific `onboardingId`. | +| /onboardedInvokers/{onboardingId} (DELETE) | [offboard_invoker()](./doc/sdk_full_documentation.md#update-and-offboard-invoker) | Deletes an invoker for a specific `onboardingId`. | +| registrations (POST) | [onboard_provider()](./doc/sdk_full_documentation.md#provider-onboarding) | Registers a new service provider. | +| /registrations/{registrationId} (PUT) | [update_provider()](./doc/sdk_full_documentation.md#update-and-offboard-provider) | Updates a service provider's registration for a specific `registrationId`. | +| /registrations/{registrationId} (DELETE) | [offboard_provider()](./doc/sdk_full_documentation.md#update-and-offboard-provider) | Deletes a service provider's registration for a specific `registrationId`. | +| /allServiceAPIs (GET) | [discover()](./doc/sdk_full_documentation.md#discover-process) | Retrieves a list of all available service APIs. | +| /trustedInvokers (PUT//POST) | [get_tokens()](./doc/sdk_full_documentation.md#discover-process) | Registers or updates trusted invokers. | +| /securities/{securityId}/token (GET) | [get_tokens()](./doc/sdk_full_documentation.md#obtain-invoker-tokens) | Retrieves a security token for a specific `securityId`. This JWT token is used to query the targeted services. | +| /{apfId}/service-apis(POST) | [publish_services()](./doc/sdk_full_documentation.md#services-publishing) | Registers a new service API into the system for a specific `apfId` | +| /{apfId}/service-apis/{serviceApiId} (DELETE) | [unpublish_service()](./doc/sdk_full_documentation.md#services-deletion) | Deletes a service API from the system for a specific `apfId`and `serviceApiId` | +| /{apfId}/service-apis/{serviceApiId} (PUT) | [update_service()](./doc/sdk_full_documentation.md#services-update) | Updates the details of an existing service API for a specific `apfId`and `serviceApiId` | +| /{apfId}/service-apis/{serviceApiId} (GET) | [get_service()](./doc/sdk_full_documentation.md#get-services) | Retrieves the details of a specific service API for a specific `apfId` and `serviceApiId` | +| /{apfId}/service-apis (GET) | [get_all_services()](./doc/sdk_full_documentation.md#get-all-services) | Retrieves a list of all available service APIs for a specific `apfId` | +| /aef-security/v1/check-authentication (POST) | [check_authentication()](./doc/sdk_full_documentation.md#check_authentication) | This custom operation allows the API invoker to confirm the `supported_features` from the API exposing function(AEF) | + +NOTE: Above mentioned CAPIF APIs are defined in these 3GPP references: +- [CAPIF Invoker API specification](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_API_Invoker_Management_API.yaml) +- [CAPIF Provider API specification](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_API_Provider_Management_API.yaml) +- [CAPIF Discover API specification](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Discover_Service_API.yaml) +- [CAPIF Publish API specification](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Publish_Service_API.yaml) +- [CAPIF Security API specification](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Security_API.yaml) +- [AEF Security API specification](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_AEF_Security_API.yaml) + +NOTE: In the [3GPP Technical Specification (TS) 29.222 V18.5.0 Common API Framework for 3GPP Northbound APIs](https://www.etsi.org/deliver/etsi_ts/129200_129299/129222/18.05.00_60/ts_129222v180500p.pdf) the `service` concept is understood as equal as the `API` concept. + + +## OpenCAPIF SDK requirements + +To use the OpenCAPIF SDK, a registered user account within the target CAPIF instance is required. + +**Contact the administrator to obtain the required predefined credentials (CAPIF username and password).** + +## OpenCAPIF SDK installation + +To install the OpenCAPIF SDK source code for developing purposes there is an available section: [OpenCAPIF SDK developers](./doc/sdk_developers.md). + +To use the SDK, binary installer for the latest version is available at the [Python Package Index (Pipy)](https://pypi.org/project/opencapif-sdk/) + +```console +pip install opencapif_sdk +``` + +## OpenCAPIF SDK Data Schema + +Here is a visual look on the variables of the CAPIF sdk referenced in: +- [Important information for Invoker Consumer](#important-information-for-invoker-consumer) +- [Important information for Provider Consumer](#important-information-for-provider-consumers) + +![sdk_data_schema](./doc/images/flows_data_schema.png) + +# Network App developer path + +The Network App Developer Path guides the programmer through building and integrating Network Apps using CAPIF. This path is divided into two key sections: [Invoker Network App](#invoker-network-app) and [Provider Network App](#provider-network-app). Each section covers the essential flow and functions for developing Network Apps interaction with CAPIF, whether the user is acting as an invoker consuming services or a provider offering them. By following this path, developers will gain a comprehensive understanding of how to effectively use the SDK within the CAPIF ecosystem. + +Here is a good explanation about how a usual flow of a Network App should work: [usual flow example](https://ocf.etsi.org/documentation/latest/testing/postman/) + +## Provider Network App + +A Network App development running as a Provider would typically follow this process step by step, making use of the SDK: + +![PROVIDER_PATH](./doc/images/flows_provider_path.jpg) + +Now, it is described in 4 simple steps how a Provider can be developed in just some code lines, below snippet. It describes the usual flow a Provider would follow to publish an API service. + +```python + import opencapif_sdk + + provider = opencapif_sdk.capif_provider_connector(config_file="path/to/capif_sdk_config.json") + provider.onboard_provider() + + #translator = opencapif_sdk.api_schema_translator("./path/to/openapi.yaml") + #translator.build("api_description_name",ip="0.0.0.0",port=9090) + provider.api_description_path = "./api_description_name.json" + + APF = provider.provider_capif_ids["APF-1"] + + AEF1 = provider.provider_capif_ids["AEF-1"] + AEF2 = provider.provider_capif_ids["AEF-2"] + + provider.publish_req['publisher_apf_id'] = APF + provider.publish_req['publisher_aefs_ids'] = [AEF1, AEF2] + provider.publish_services() ``` -cd existing_repo -git remote add origin https://labs.etsi.org/rep/ocf/sdk.git -git branch -M main -git push -uf origin main + +Code is next explained step by step: + +1. **Create a Provider object:** \ + Initialize the provider by creating an instance of the `capif_provider_connector` class, passing the required [configuration](./doc/sdk_configuration.md) file: + + Make sure that the configuration file is filled before creating the instance. + + +2. **Onboard the Provider:** \ + Register the provider with the CAPIF system to enable the publication of APIs: + + In this phase, the SDK creates and stores all the necessary files for using CAPIF as a provider, such as the authorization certificate, the server certificate and each of the APFs and AEFs certificates .Furthermore creates a file named `provider_capif_ids.json`, which stores important information about the provider. + +3. **Prepare API details:** \ + In the `provider_folder`, more specifically in the `capif_username` folder, it will be sotres the provider API details file. This file contains all the APFs and AEFs IDs that have already onboarded with this `capif_username`. + + It is also important to have previously prepared the **API schema description** file of the API to be published. **This file must follow the [CAPIF_Publish_Service_API](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Publish_Service_API.yaml) 3GPP specification.** + + If the **API** is defined in an Openapi.yaml format, the sdk has a facility which creates automatically the **API schema description**.For using this functionality uncomment the translator lines. More information:[Translator functionality](./doc/sdk_full_documentation.md#openapi-translation) + + Choose one APF and the AEF identifiers, and fulfill the `publish_req` structure and the `api_description_path`. + + The `provider_capif_ids` variable is a dictionary which contains key-values of all the APFs and AEFs stored as name: ID. + + This `publish_req` field can also be filled with object variables already stored at provider object. + +5. **Publish the services:** \ + Use the `publish_services()` method to register the APIs with the CAPIF framework. In this phase, the SDK does the publishing of the provided API specification. + + **At the end of this step, the API will be available for Invokers to be consumed.** + +Now, Provider Network App is ready to receive requests from Invokers. + +### Provider Network App sample + +This repository provides an implementation sample of a [Provider-Network App](./network_app_samples/network_app_provider_sample/network_app_provider.py). + +In this sample, the provider publishes two APIs and starts running the servers of each API on local environment. + +### Important information for Provider consumers + +Within the `provider_folder`, the SDK stores the created folders named with prefix of the provided `capif_username` that has been registered from administrator. At each folder, there will be found the following files: + +- `provider_capif_ids.json`: contains all the APFs and AEFs ids that have already onboarded with this `capif_username`, +- `capif__.json`: if it is already published or updated an API, it will contain a copy of the last payload, +- `service_received.json`: if it is already used to get an API or get all APIs functionality, it will contain the response of last request, +- `provider_service_ids.json`: contains the currently published APIs with their `api_id`. + +All the configuration values are available within the object `capif_provider_connector`. + +The `provider_service_ids` variable stores the `provider_service_ids.json` content in a dictionary form. + +The `provider_capif_ids` variable stores the `provider_capif_ids.json` content in a dictionary form. + + +## Invoker Network App + +A Network App development running as an Invoker would typically follow this process step by step, making use of the SDK: + +![INVOKER_PATH](./doc/images/flows_invoker_path.jpg) + +Now, it is described in some simple steps how an Invoker can be developed in just some code lines. Find below the code snippet. It describes the usual flow an Invoker would follow to consume APIs from CAPIF. + +```python + import opencapif_sdk + + invoker = opencapif_sdk.capif_invoker_connector(config_file="path/to/the/capif_sdk_config.json") + invoker.onboard_invoker() + service_discoverer = opencapif_sdk.service_discoverer(config_file="path/to/the/capif_sdk_config.json") + service_discoverer.discover() + service_discoverer.get_tokens() + jwt_token=service_discoverer.token ``` -## Integrate with your tools +Code is next explained step by step: + +1. **Create an Invoker object:** \ + Initialize the invoker by creating an instance of the `capif_invoker_connector` class, passing the required [configuration](./doc/sdk_configuration.md) file. + + Make sure that the configuration file is filled out before creating the instance. + +2. **Onboard the Invoker**: \ + Register the target invoker with the CAPIF system to enable access to APIs. -- [ ] [Set up project integrations](https://labs.etsi.org/rep/ocf/sdk/-/settings/integrations) + In this phase, the SDK creates and stores all the necessary files for using CAPIF as a invoker, such as the authorization certificate and the server certificate.Furthermore,it creates a file named `capif_api_security_context_details.json` , which stores important information about the invoker. -## Collaborate with your team +3. **Create a Service Discoverer object:** \ + Initialize the service discovery mechanism to search for available services(APIs) in CAPIF. -- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) -- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) +4. **Discover available services:** \ + Use the `discover()` method to retrieve a list of available APIs. In this phase, the SDK finds all the available APIs for the invoker. Consequently, it saves the most important information and stores it within the `capif_api_security_context_details.json`. -## Test and Deploy + **DISCLAIMER:** If it is the first time the user runs `discover()`, it will show a warning alert like following: -Use the built-in continuous integration in GitLab. + WARNING - Received 404 error, redirecting to register security service -- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) -- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) -- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) -- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) + This alert is expected because the SDK tries to update the security context first. If a 404 error is received, it means the security context is not created yet, so the next step for the SDK is to register a new security service. -*** +5. **Retrieve security tokens:** \ + Use the `get_tokens()` method to obtain the necessary tokens for authenticating API requests. + -# Editing this README +**At the end of this flow, the invoker has been onboarded and it is ready to use target APIs.** All required information, including the access_token to use the available APIs, is stored at `capif_api_security_context_details.json` file. This file is placed in the invoker_folder path, specifically in the folder that corresponds to the capif_username used in the `capif_sdk_config.json`. A sample of the [capif_api_security_context_details](./samples/capif_api_security_context_details_sample.json) is also available. -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template. +Now, Invoker Network App can use access tokens to consume real services. -## Suggestions for a good README -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. +### Invoker Network App sample -## Name -Choose a self-explaining name for your project. +Here is a code sample of the implementation of an [Invoker-Network App](./network_app_samples/network_app_invoker_sample/network_app_invoker.py). -## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. +In this sample, the invoker will discover the APIs published by the sample provider shown in this document and will return the access token for querying the APIs. This sample is prepared to run after the [Provider-Network App](./network_app_samples/network_app_provider_sample/network_app_provider.py). -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. +Make sure that the [Provider-Network App](./network_app_samples/network_app_provider_sample/network_app_provider.py) is running before following this implementation. -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. +For testing APIs availability, after running both samples([Provider-Network App](./network_app_samples/network_app_provider_sample/network_app_provider.py) and [Invoker-Network App](./network_app_samples/network_app_invoker_sample/network_app_invoker.py)) the invoker app will return the access token. -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. +Also, in the same Invoker-Network folder is available a [Postman structure](./network_app_samples/network_app_invoker_sample/postman/).In order to test these APIs, the access token returned in the Invoker-Network App must be set in the Postman environment, more specifically in the `access_token` variable. -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. +Another alternative is to import the [Postman structure](./network_app_samples/network_app_invoker_sample/postman/) in your own postman account and fill the `postman_api_key` and the `environment_id` fields within the [Invoker-Network App](./network_app_samples/network_app_invoker_sample/network_app_invoker.py). Here is an example of these two fields that need to be fulfilled. + +```python + # Your Postman API Key + postman_api_key = "AAAA-your-apikey" + + # Postman Environment ID + environment_id = "your-environment-id-must-be-here" +``` -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. +### Important information for Invoker consumer -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. +In the `invoker_folder`, it will be located several folders with each `capif_username` it has been onboarded as a provider. For each folder, it will be found: -## Contributing -State if you are open to contributions and what your requirements are for accepting them. +- `capif_api_security_context_details.json`: This file contains the information of the invoker. It will contain: + + 1. The `api_invoker_id`, + 2. If the Service Discovery Functionality has already been used , it will be found all the available APIs with their information, + 3. If the Service Get Token functionality has already been used , it will be found the access token for using the APIs that has already been discovered. -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. +The `token` variable is also available for retrieving the JWT token after the `get_tokens()` method. -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. +The `invoker_capif_details` variable stores the `capif_api_security_context_details.json` content in a dictionary form. -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. +# OpenCAPIF SDK known issues -## License -For open source projects, say how it is licensed. +There are some features which **are not currently available at latest OpenCAPIF SDK release**. Those are assumed to be technical debt and might be available in future releases: -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. + - [CAPIF Access control policy management](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Access_Control_Policy_API.yaml) + - [CAPIF Auditing API management](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Auditing_API.yaml) + - [CAPIF Events API management](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Events_API.yaml) + - [CAPIF Logging API management](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Logging_API_Invocation_API.yaml) + - [CAPIF Routing info API management](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Routing_Info_API.yaml) + - [CAPIF Security API management](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Security_API.yaml) + - /trustedInvokers/{apiInvokerId}/delete (POST) + - /trustedInvokers/{apiInvokerId} (GET) + - /trustedInvokers/{apiInvokerId} (DELETE) diff --git a/config/capif_sdk_config.json b/config/capif_sdk_config.json new file mode 100644 index 0000000000000000000000000000000000000000..34844f5a344cae2d12a0110e4ccfc61b4de0ce5f --- /dev/null +++ b/config/capif_sdk_config.json @@ -0,0 +1,65 @@ +{ + "capif_host": "", + "register_host": "", + "capif_https_port": "", + "capif_register_port": "", + "capif_username": "", + "capif_password": "", + "debug_mode": "", + "invoker": { + "invoker_folder": "", + "capif_callback_url": "", + "supported_features":"", + "check_authentication":{ + "ip":"", + "port":"" + }, + "cert_generation": { + "csr_common_name": "", + "csr_organizational_unit": "", + "csr_organization": "", + "crs_locality": "", + "csr_state_or_province_name": "", + "csr_country_name": "", + "csr_email_address": "" + }, + "discover_filter": { + "api-name": "", + "api-version": "", + "comm-type": "", + "protocol": "", + "aef-id": "", + "data-format": "", + "api-cat": "", + "preferred-aef-loc": "", + "req-api-prov-name": "", + "supported-features": "", + "api-supported-features": "", + "ue-ip-addr": "", + "service-kpis": "" + } + }, + "provider": { + "provider_folder": "", + "apfs": "", + "aefs": "", + "publish_req": { + "service_api_id": "", + "publisher_apf_id": "", + "publisher_aefs_ids": [ + "", + "" + ] + }, + "cert_generation": { + "csr_common_name": "", + "csr_organizational_unit": "", + "csr_organization": "", + "crs_locality": "", + "csr_state_or_province_name": "", + "csr_country_name": "", + "csr_email_address": "" + }, + "api_description_path": "" + } +} diff --git a/config/capif_sdk_register.json b/config/capif_sdk_register.json new file mode 100644 index 0000000000000000000000000000000000000000..25367f06a80eef832e2d21d3bab8f57e9d25407f --- /dev/null +++ b/config/capif_sdk_register.json @@ -0,0 +1,10 @@ +{ + "register_host": "", + "capif_register_port": "", + "capif_register_username": "", + "capif_register_password": "", + "capif_username":"", + "capif_password":"", + "config_path":"", + "uuid":"" +} \ No newline at end of file diff --git a/doc/README_pipy.md b/doc/README_pipy.md new file mode 100644 index 0000000000000000000000000000000000000000..57eacbc1799d6b0faa5f8940d6d9376fcf9fec19 --- /dev/null +++ b/doc/README_pipy.md @@ -0,0 +1,211 @@ +# OpenCAPIF SDK + +This repository develops a Python Software Development Kit(SDK) which focuses on connecting to OpenCAPIF (Common API Framework for 3GPP Northbound APIs) in a simple way, lowering integration complexity and allowing developers to focus on Network Applications (Network Apps) or services development. + +OpenCAPIF SDK provides a set of libraries to enable either CAPIF provider and invoker roles, and other functions to simplify procedures calls towards OpenCAPIF entity. + +Current version of OpenCAPIF SDK is compatible with following publicly available releases: +- [OpenCAPIF Release 1.0](https://ocf.etsi.org/documentation/v1.0.0-release/) +- OpenCAPIF Release 2.0 + +# Network App developers + +In the scope of CAPIF, a Network App (Network Application) refers to an external application or service that interacts with the 3GPP network via standardized APIs. These Network Apps typically leverage the capabilities and services provided by the underlying mobile network infrastructure, such as network slicing, quality of service (QoS), or location services. + +Network Apps can be developed by third-party service providers, network operators, or other stakeholders to offer a wide range of services, including enhanced communication features, IoT solutions, or content delivery, and they use CAPIF as the unified framework for securely discovering, accessing, and utilizing 3GPP network APIs. + +For that purpose Network Apps play 2 different roles when interacting with CAPIF: +- **Invoker**: a Network App acting as an Invoker is responsible for consuming APIs exposed by other services. This role represents an external application or service that calls the 3GPP northbound APIs to utilize the network’s functionalities. + +- **Provider**: a Network App acting as a Provider is responsible for exposing its own APIs/services for use by Invokers. This role represents an entity that offers services through APIs, making them available to other external applications or Invokers.A provider also is distinguished for having three parts. + + - The **AMF (API Management Function)**, supplies the API provider domain with administrative capabilities. Some of these capabilities include, auditing the service API invocation logs received from the CCF, on-boarding/off-boarding new API invokers and monitoring the status of the service APIs.One provider can have only one AMF. + + - The **APF (API Publishing Function)**, is responsible for the publication of the service APIs to CCF in order to enable the discovery capability to the API Invokers.One provider can have multiple APFs. + + - The **AEF (API Exposing Function)**, is responsible for the exposure of the service APIs. Assuming that API Invokers are authorized by the CCF, AEF validates the authorization and subsequently provides the direct communication entry points to the service APIs. AEF may also authorize API invokers and record the invocations in log files.One provider can have multiple AEFs + +OpenCAPIF SDK brings a set of functions to integrate with the 5G Core's function CAPIF, as defined in [3GPP Technical Specification (TS) 29.222 V18.5.0 Common API Framework for 3GPP Northbound APIs](https://www.etsi.org/deliver/etsi_ts/129200_129299/129222/18.05.00_60/ts_129222v180500p.pdf). This section shows the mapping between the Python functions available in this SDK and the CAPIF OpenAPI APIs defined the reference standard: + +| **3GPP CAPIF API** | **OpenCAPIF SDK function** | **Description** | +|-------------------------------------------------------|-------------------------------------------------------------|-------------------------------------------------------------| +| /onboardedInvokers (POST) | [onboard_invoker()](./doc/sdk_full_documentation.md#invoker-onboarding) | Registers a new invoker. | +| /onboardedInvokers/{onboardingId} (PUT) | [update_invoker()](./doc/sdk_full_documentation.md#update-and-offboard-invoker) | Updates an existing invoker for a specific `onboardingId`. | +| /onboardedInvokers/{onboardingId} (DELETE) | [offboard_invoker()](./doc/sdk_full_documentation.md#update-and-offboard-invoker) | Deletes an invoker for a specific `onboardingId`. | +| registrations (POST) | [onboard_provider()](./doc/sdk_full_documentation.md#provider-onboarding) | Registers a new service provider. | +| /registrations/{registrationId} (PUT) | [update_provider()](./doc/sdk_full_documentation.md#update-and-offboard-provider) | Updates a service provider's registration for a specific `registrationId`. | +| /registrations/{registrationId} (DELETE) | [offboard_provider()](./doc/sdk_full_documentation.md#update-and-offboard-provider) | Deletes a service provider's registration for a specific `registrationId`. | +| /allServiceAPIs (GET) | [discover()](./doc/sdk_full_documentation.md#discover-process) | Retrieves a list of all available service APIs. | +| /trustedInvokers (PUT//POST) | [get_tokens()](./doc/sdk_full_documentation.md#discover-process) | Registers or updates trusted invokers. | +| /securities/{securityId}/token (GET) | [get_tokens()](./doc/sdk_full_documentation.md#obtain-invoker-tokens) | Retrieves a security token for a specific `securityId`. This JWT token is used to query the targeted services. | +| /{apfId}/service-apis(POST) | [publish_services()](./doc/sdk_full_documentation.md#services-publishing) | Registers a new service API into the system for a specific `apfId` | +| /{apfId}/service-apis/{serviceApiId} (DELETE) | [unpublish_service()](./doc/sdk_full_documentation.md#services-deletion) | Deletes a service API from the system for a specific `apfId`and `serviceApiId` | +| /{apfId}/service-apis/{serviceApiId} (PUT) | [update_service()](./doc/sdk_full_documentation.md#services-update) | Updates the details of an existing service API for a specific `apfId`and `serviceApiId` | +| /{apfId}/service-apis/{serviceApiId} (GET) | [get_service()](./doc/sdk_full_documentation.md#get-services) | Retrieves the details of a specific service API for a specific `apfId` and `serviceApiId` | +| /{apfId}/service-apis (GET) | [get_all_services()](./doc/sdk_full_documentation.md#get-all-services) | Retrieves a list of all available service APIs for a specific `apfId` | +| /aef-security/v1/check-authentication (POST) | [check_authentication()](./doc/sdk_full_documentation.md#check_authentication) | This custom operation allows the API invoker to confirm the `supported_features` from the API exposing function(AEF) | + +NOTE: Above mentioned CAPIF APIs are defined in these 3GPP references: +- [CAPIF Invoker API specification](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_API_Invoker_Management_API.yaml) +- [CAPIF Provider API specification](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_API_Provider_Management_API.yaml) +- [CAPIF Discover API specification](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Discover_Service_API.yaml) +- [CAPIF Publish API specification](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Publish_Service_API.yaml) +- [CAPIF Security API specification](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Security_API.yaml) +- [AEF Security API specification](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_AEF_Security_API.yaml) + +NOTE: In the [3GPP Technical Specification (TS) 29.222 V18.5.0 Common API Framework for 3GPP Northbound APIs](https://www.etsi.org/deliver/etsi_ts/129200_129299/129222/18.05.00_60/ts_129222v180500p.pdf) the `service` concept is understood as equal as the `API` concept. + + +## OpenCAPIF SDK requirements + +To use the OpenCAPIF SDK, a registered user account within the target CAPIF instance is required. + +**Contact the administrator to obtain the required predefined credentials (CAPIF username and password).** + +## OpenCAPIF SDK installation + +To use the SDK, binary installer for the latest version is available at the [Python Package Index (Pipy)](https://pypi.org/project/opencapif-sdk/) + +```console +pip install opencapif_sdk +``` +## Configuration via `capif_sdk_config.json` + +### Common Fields for Invoker and Provider + +Regardless of the role (Invoker or Provider), the following fields are mandatory: + +- `capif_host` +- `register_host` +- `capif_https_port` +- `capif_register_port` +- `capif_username` +- `capif_password` +- `debug_mode` + +### Network App Invoker + +When configuring the SDK as a **Network App Invoker**, the following fields must be provided: + +- `invoker_folder` +- `capif_callback_url` +- `supported_features` +- `cert_generation` (fields such as `csr_common_name`, `csr_country_name`, etc.) + +**Optional:** +- `discover_filter`: useful to enable the discovery of specific APIs. Some fields under `discover_filter` structure required to be configured when using discovery filters. Check devoted section below, +- `check_authentication_data`: useful to use `check_authentication()` function to validate features from a target provider, it will be required to fill up the `ip` and `port` parameters within the `check_authentication_data` variable. + +### Network App Provider + +For SDK configuration as a **Network App Provider**, the following fields are required: + +- `provider_folder` +- `cert_generation` (fields such as `csr_common_name`, `csr_country_name`, etc.) +- `APFs` +- `AEFs` +- `publish_req` +- `api_description_path` + +## Configuration of `discover_filter` + +The `discover_filter` section adheres to the parameters defined in the GET request schema of the [Discover Services API](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Discover_Service_API.yaml). + +To use the service discovery functionality, the `discover_filter` fields should be populated with the desired filters. **It is important to note that fields such as `api-name` must contain only one entry of each type (i.e., no lists are allowed in api-name).** + +For instance if the invoker fill the `api-name` field, the `discover()` functionality will retrieve only one API, the one that matches the exact name of the `api-name`. + +Before running the Invoker Service Discovery Functionality, the Invoker must be onboarded to CAPIF. + +## Configuration of `publish_req` + +This section is mandatory when using the [CAPIF Publish Service API](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Publish_Service_API.yaml). The following fields are required: + +- `service_api_id`: Example: `"02eff6e1b3a8f7c8044a92ee8a30bd"` +- `publisher_apf_id`: Example: `"APFa165364a379035d14311deadc04332"` +- `publisher_aefs_ids`: An array of selected AEF IDs. Example: `["AEFfa38f0e855bffb420e4994ecbc8fb9", "AEFe8bfa711f4f0c95ba0b382508e6382"]` + +The `api_description_path` must point to the Publish API to be shared, and it should follow the [ServiceAPIDescription](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Publish_Service_API.yaml) schema. + +To obtain this schema, opencapif_sdk has a facility to translate Openapi structures to ServiceAPIDescription schemas. + +If the `publisher_aefs_ids` do not match the `aefProfiles` in the API description, an error will be raised by the SDK. + +## Descriptions of `capif_sdk_config` Fields + +This file can also be populated using [environment variables](../samples/enviroment_variables_sample.txt). + +- `invoker_folder`: The path (relative or absolute) where invoker information (certificates, keys, etc.) is stored. +- `provider_folder`: The path (relative or absolute) where provider information is stored. +- `capif_host`: The domain name of the CAPIF host. +- `register_host`: The domain name of the register host. +- `capif_https_port`: The CAPIF host port number. +- `capif_register_port`: The register host port number. +- `capif_callback_url`: The URL used by CAPIF to send invoker notifications ([currently unavailable](sdk-issues.md)). +- `cert_generation`: Fields for certificate generation, with `csr_country_name` requiring a two-letter country code. +- `capif_username`: The CAPIF username. +- `capif_password`: The CAPIF password. +- `apfs`: The number of APFs to be onboarded as a provider (e.g., `5`). +- `aefs`: The number of AEFs to be onboarded as a provider (e.g., `2`). +- `debug_mode`: A boolean value to enable or disable SDK logs (e.g., `True` or `False`). +- `discover_filter`: Fields for configuring invoker service discovery. +- `publish_req`: Fields required for API publishing. +- `api_description_path`: The path to the [ServiceAPIDescription](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Publish_Service_API.yaml) JSON file. +- `check_authentication_data`: The `ip` and `port` of the target Provider's AEF to get their supported features from. + + + +## Important information for Provider consumers + +Within the `provider_folder`, the SDK stores the created folders named with prefix of the provided `capif_username` that has been registered from administrator. At each folder, there will be found the following files: + +- `provider_capif_ids.json`: contains all the APFs and AEFs ids that have already onboarded with this `capif_username`, +- `capif__.json`: if it is already published or updated an API, it will contain a copy of the last payload, +- `service_received.json`: if it is already used to get an API or get all APIs functionality, it will contain the response of last request, +- `provider_service_ids.json`: contains the currently published APIs with their `api_id`. + +All the configuration values are available within the object `capif_provider_connector`. + +The `provider_service_ids` variable stores the `provider_service_ids.json` content in a dictionary form. + +The `provider_capif_ids` variable stores the `provider_capif_ids.json` content in a dictionary form. + +## Important information for Invoker consumer + +In the `invoker_folder`, it will be located several folders with each `capif_username` it has been onboarded as a provider. For each folder, it will be found: + +- `capif_api_security_context_details.json`: This file contains the information of the invoker. It will contain: + + 1. The `api_invoker_id`, + 2. If the Service Discovery Functionality has already been used , it will be found all the available APIs with their information, + 3. If the Service Get Token functionality has already been used , it will be found the access token for using the APIs that has already been discovered. + +The `token` variable is also available for retrieving the JWT token after the `get_tokens()` method. + +The `invoker_capif_details` variable stores the `capif_api_security_context_details.json` content in a dictionary form. + +## Openapi translation + +The `api_description_path` must point to the Publish API to be shared, and it should follow the [ServiceAPIDescription](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Publish_Service_API.yaml) schema. + +This schema could be obtained by applying this code. +```python + import opencapif_sdk + + translator = api_schema_translator("./path/to/openapi.yaml") + translator.build("api_description_name",ip="0.0.0.0",port=9090) +``` +This code will read `openapi.yaml`, ensure the structure of it and translate the content into ServiceAPIDescription schema, then will create a .json named `api_description_name`. Also it is necessary to fill the ip and port fields to create correctly the schema. +# OpenCAPIF SDK known issues + +There are some features which **are not currently available at latest OpenCAPIF SDK release**. Those are assumed to be technical debt and might be available in future releases: + + - [CAPIF Access control policy management](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Access_Control_Policy_API.yaml) + - [CAPIF Auditing API management](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Auditing_API.yaml) + - [CAPIF Events API management](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Events_API.yaml) + - [CAPIF Logging API management](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Logging_API_Invocation_API.yaml) + - [CAPIF Routing info API management](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Routing_Info_API.yaml) + - [CAPIF Security API management](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Security_API.yaml) + - /trustedInvokers/{apiInvokerId}/delete (POST) + - /trustedInvokers/{apiInvokerId} (GET) + - /trustedInvokers/{apiInvokerId} (DELETE) diff --git a/doc/images/OpenCAPIF_icon.jpg b/doc/images/OpenCAPIF_icon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8ae5c10e186e12481f1e65f36b69ac722f057775 Binary files /dev/null and b/doc/images/OpenCAPIF_icon.jpg differ diff --git a/doc/images/capif_provider_details_example.png b/doc/images/capif_provider_details_example.png new file mode 100644 index 0000000000000000000000000000000000000000..a22b797c2bd2328a0b0e63bdedad98e6b46e94ff Binary files /dev/null and b/doc/images/capif_provider_details_example.png differ diff --git a/doc/images/flows_capif_illustration.jpg b/doc/images/flows_capif_illustration.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8a1c1302980d9386586246b789198fbf0a3da6ec Binary files /dev/null and b/doc/images/flows_capif_illustration.jpg differ diff --git a/doc/images/flows_data_schema.png b/doc/images/flows_data_schema.png new file mode 100644 index 0000000000000000000000000000000000000000..d2dbeda12ae9502a563b4a5a173c1569a72bc72e Binary files /dev/null and b/doc/images/flows_data_schema.png differ diff --git a/doc/images/flows_invoker_check_authentication.jpg b/doc/images/flows_invoker_check_authentication.jpg new file mode 100644 index 0000000000000000000000000000000000000000..29a348a902dd47f492e6bb2a29102b107cb2a003 Binary files /dev/null and b/doc/images/flows_invoker_check_authentication.jpg differ diff --git a/doc/images/flows_invoker_discover.jpg b/doc/images/flows_invoker_discover.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8d74e9b3c716136fde6d5ab7ee9a0af4302ff148 Binary files /dev/null and b/doc/images/flows_invoker_discover.jpg differ diff --git a/doc/images/flows_invoker_get_tokens.jpg b/doc/images/flows_invoker_get_tokens.jpg new file mode 100644 index 0000000000000000000000000000000000000000..744496ced00b34ab994e7f04c0021d1075119ecb Binary files /dev/null and b/doc/images/flows_invoker_get_tokens.jpg differ diff --git a/doc/images/flows_invoker_onboard.jpg b/doc/images/flows_invoker_onboard.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ba7f69e258d420cd0cdb61b351f2ced7c554c3fb Binary files /dev/null and b/doc/images/flows_invoker_onboard.jpg differ diff --git a/doc/images/flows_invoker_path.jpg b/doc/images/flows_invoker_path.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2cba9d6a1f7db7967501e5aec2845d223c979d55 Binary files /dev/null and b/doc/images/flows_invoker_path.jpg differ diff --git a/doc/images/flows_invoker_update_offboard.jpg b/doc/images/flows_invoker_update_offboard.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7944c6bd51a18fb028fd7b074a2058a09cfc9860 Binary files /dev/null and b/doc/images/flows_invoker_update_offboard.jpg differ diff --git a/doc/images/flows_provider_onboard.jpg b/doc/images/flows_provider_onboard.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b352ea3de7944f4ca573b6c091fa26993fa33739 Binary files /dev/null and b/doc/images/flows_provider_onboard.jpg differ diff --git a/doc/images/flows_provider_path.jpg b/doc/images/flows_provider_path.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f80c48277993e7b9073c2d8c0892054bdd921e38 Binary files /dev/null and b/doc/images/flows_provider_path.jpg differ diff --git a/doc/images/flows_provider_publish_functions.jpg b/doc/images/flows_provider_publish_functions.jpg new file mode 100644 index 0000000000000000000000000000000000000000..38dec6ee58618f3e540c340f4e1eefd3a492e116 Binary files /dev/null and b/doc/images/flows_provider_publish_functions.jpg differ diff --git a/doc/images/flows_provider_update_offboard.jpg b/doc/images/flows_provider_update_offboard.jpg new file mode 100644 index 0000000000000000000000000000000000000000..09ad5c7e1633eda2df037d15eb0b3979213355eb Binary files /dev/null and b/doc/images/flows_provider_update_offboard.jpg differ diff --git a/doc/images/flows_sdk_with_register.jpg b/doc/images/flows_sdk_with_register.jpg new file mode 100644 index 0000000000000000000000000000000000000000..862a3533dd1b5114d0363c8480034dd0b2d0ed80 Binary files /dev/null and b/doc/images/flows_sdk_with_register.jpg differ diff --git a/doc/images/flows_updated_opencapif.jpg b/doc/images/flows_updated_opencapif.jpg new file mode 100644 index 0000000000000000000000000000000000000000..eb7c724cabe315da35c08f5025f2f4a1bf5daedd Binary files /dev/null and b/doc/images/flows_updated_opencapif.jpg differ diff --git a/doc/images/opencapif_icon.jpg b/doc/images/opencapif_icon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8ae5c10e186e12481f1e65f36b69ac722f057775 Binary files /dev/null and b/doc/images/opencapif_icon.jpg differ diff --git a/doc/images/publish_req_example.png b/doc/images/publish_req_example.png new file mode 100644 index 0000000000000000000000000000000000000000..ace48252f76e40edebbfe82717a010009d87851a Binary files /dev/null and b/doc/images/publish_req_example.png differ diff --git a/doc/sdk_configuration.md b/doc/sdk_configuration.md new file mode 100644 index 0000000000000000000000000000000000000000..d9193cb49086ddfa1b16e9c92f836afa2a23a9b5 --- /dev/null +++ b/doc/sdk_configuration.md @@ -0,0 +1,117 @@ +# OpenCAPIF SDK Configuration + +Before starting the configuration process, it is required that both the [requirements](../README.md) and the [installation](./sdk_developers.md) sections are completed. + +## Table of Contents +- [Configuration via capif_sdk_config.json](#configuration-via-capif_sdk_configjson) + - [As a Network App Invoker](#network-app-invoker) + - [As a Network App Provider](#network-app-provider) + - [Descriptions of capif_sdk_config Fields](#descriptions-of-capif_sdk_config-fields) +- [Configuration via capif_sdk_register.json](#configuration-via-capif-sdk-registerjson) + +## Configuration via `capif_sdk_config.json` + +A sample configuration file can be found [here](../samples/config_sample.json). + +### Common Fields for Invoker and Provider + +Regardless of the role (Invoker or Provider), the following fields are mandatory: + +- `capif_host` +- `register_host` +- `capif_https_port` +- `capif_register_port` +- `capif_username` +- `capif_password` +- `debug_mode` + +### Network App Invoker + +When configuring the SDK as a **Network App Invoker**, the following fields must be provided: + +- `invoker_folder` +- `capif_callback_url` +- `supported_features` +- `cert_generation` (fields such as `csr_common_name`, `csr_country_name`, etc.) + +**Optional:** +- `discover_filter`: useful to enable the discovery of specific APIs. Some fields under [`discover_filter`](#configuration-of-discover_filter) structure required to be configured when using discovery filters. Check devoted section below, +- `check_authentication_data`: useful to use `check_authentication()` function to validate features from a target provider, it will be required to fill up the `ip` and `port` parameters within the `check_authentication_data` variable. + +### Network App Provider + +For SDK configuration as a **Network App Provider**, the following fields are required: + +- `provider_folder` +- `cert_generation` (fields such as `csr_common_name`, `csr_country_name`, etc.) +- `APFs` +- `AEFs` +- [`publish_req`](#configuration-of-publish_req) +- `api_description_path` + +## Configuration of `discover_filter` + +The `discover_filter` section adheres to the parameters defined in the GET request schema of the [Discover Services API](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Discover_Service_API.yaml). + +To use the service discovery functionality, the `discover_filter` fields should be populated with the desired filters. **It is important to note that fields such as `api-name` must contain only one entry of each type (i.e., no lists are allowed in api-name).** + +For instance if the invoker fill the `api-name` field, the `discover()` functionality will retrieve only one API, the one that matches the exact name of the `api-name`. + +Before running the Invoker Service Discovery Functionality, the Invoker must be onboarded to CAPIF. + +## Configuration of `publish_req` + +This section is mandatory when using the [CAPIF Publish Service API](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Publish_Service_API.yaml). The following fields are required: + +- `service_api_id`: Example: `"02eff6e1b3a8f7c8044a92ee8a30bd"` +- `publisher_apf_id`: Example: `"APFa165364a379035d14311deadc04332"` +- `publisher_aefs_ids`: An array of selected AEF IDs. Example: `["AEFfa38f0e855bffb420e4994ecbc8fb9", "AEFe8bfa711f4f0c95ba0b382508e6382"]` + +The `api_description_path` must point to the Publish API to be shared, and it should follow the [ServiceAPIDescription](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Publish_Service_API.yaml) schema. + +To obtain this schema, opencapif_sdk has a facility to translate Openapi structures to ServiceAPIDescription schemas. +More information:[Translator functionality](./sdk_full_documentation.md#openapi-translation) + +If the `publisher_aefs_ids` do not match the `aefProfiles` in the API description, an error will be raised by the SDK. + +## Descriptions of `capif_sdk_config` Fields + +This file can also be populated using [environment variables](../samples/enviroment_variables_sample.txt). + +- `invoker_folder`: The path (relative or absolute) where invoker information (certificates, keys, etc.) is stored. +- `provider_folder`: The path (relative or absolute) where provider information is stored. +- `capif_host`: The domain name of the CAPIF host. +- `register_host`: The domain name of the register host. +- `capif_https_port`: The CAPIF host port number. +- `capif_register_port`: The register host port number. +- `capif_callback_url`: The URL used by CAPIF to send invoker notifications ([currently unavailable](sdk-issues.md)). +- `cert_generation`: Fields for certificate generation, with `csr_country_name` requiring a two-letter country code. +- `capif_username`: The CAPIF username. +- `capif_password`: The CAPIF password. +- `apfs`: The number of APFs to be onboarded as a provider (e.g., `5`). +- `aefs`: The number of AEFs to be onboarded as a provider (e.g., `2`). +- `debug_mode`: A boolean value to enable or disable SDK logs (e.g., `True` or `False`). +- [`discover_filter`](#configuration-of-discover_filter): Fields for configuring invoker service discovery. +- [`publish_req`](#configuration-of-publish_req): Fields required for API publishing. +- `api_description_path`: The path to the [ServiceAPIDescription](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Publish_Service_API.yaml) JSON file. +- `check_authentication_data`: The `ip` and `port` of the target Provider's AEF to get their supported features from. + + +## Configuration via `capif_sdk_register.json` + +To use this SDK in a local environment for creating and removing users, the following fields must be populated. Note that this feature is not included in the SDK, but instructions can be found in the [CAPIF official repository](https://labs.etsi.org/rep/ocf/capif/-/tree/REL1?ref_type=heads). + +- `register_host`: The domain name of the register host. +- `capif_register_port`: The port number of the register host. +- `capif_register_username`: The CAPIF admin username. +- `capif_register_password`: The CAPIF admin password. +- `capif_username`: The CAPIF user username. +- `capif_password`: The CAPIF user password. +- `config_path`: The absolute path to the configuration files folder. +- `uuid`: The UUID required for de-registering a user (only mandatory for de-registration). + +This file is used for the following functionalities: +- Register and login. +- Deregister and login. + +All fields are required except for `uuid`, which is only mandatory for de-registration. It is recommended to store the `uuid` returned from the registration process for future use. diff --git a/doc/sdk_developers.md b/doc/sdk_developers.md new file mode 100644 index 0000000000000000000000000000000000000000..1d5d977dd2431c035b33cd47cf0c099910278c90 --- /dev/null +++ b/doc/sdk_developers.md @@ -0,0 +1,78 @@ + +# OpenCAPIF SDK Development Environment Installation + +Before proceeding, ensure you have fulfilled the necessary [requirements](../README.md). + +Follow the steps below to install the OpenCAPIF SDK for development purposes: + +## Requisites + +- Developers must have the following tools installed: + - **pyenv** + +## Installation Steps + +### 1. Set Up a Python Environment with `pyenv` + +Follow these steps to create and activate a virtual environment using `pyenv`: + +```bash +# Install Python 3.12 using pyenv +pyenv install 3.12 + +# Create a virtual environment for the SDK +pyenv virtualenv 3.12 pesp_sdk_env + +# Activate the virtual environment +source path/to/.pyenv/versions/pesp_sdk_env/bin/activate +``` + +OPTIONAL step: sometimes Apple Mac shells raise issues while finding the shell path. If this happens, try this command: + + ```console + export PATH="$HOME/.pyenv/bin:$PATH" + eval "$(pyenv init --path)" + eval "$(pyenv init -)" + eval "$(pyenv virtualenv-init -)" + ``` +OPTIONAL step: For Arm64 architecture sometimes will appear this problem + + ```Last 10 log lines: + __locale_localeconv in _localemodule.o + __locale_localeconv in _localemodule.o + __locale_localeconv in _localemodule.o + __locale_localeconv in _localemodule.o + "_libintl_textdomain", referenced from: + __locale_textdomain in _localemodule.o + ld: symbol(s) not found for architecture arm64 + clang: error: linker command failed with exit code 1 (use -v to see invocation) + make: *** [Programs/_freeze_module] Error 1 + make: *** Waiting for unfinished jobs....´´´ + + +This article explains how to solve it [here](https://laict.medium.com/install-python-on-macos-11-m1-apple-silicon-using-pyenv-12e0729427a9) + + +2. Clone GitHub repository: + +```console +git clone https://github.com/Telefonica/pesp_capif_sdk.git +``` + +```console +#Then move to the pesp_capif_sdk folder + +cd /the/path/to/pesp_capif_sdk +``` + +3. Install the Python requirements listed in [requirements.txt](../installation/requirements.txt) file: + +```console +cd installation + +python -m pip install --upgrade pip + +pip install -r requirements.txt +``` + +**Congratulations!** The installation of OpenCAPIF SDK has finished. \ No newline at end of file diff --git a/doc/sdk_full_documentation.md b/doc/sdk_full_documentation.md new file mode 100644 index 0000000000000000000000000000000000000000..d7f2b168f5c5887288a07cfc07bb18b90400e317 --- /dev/null +++ b/doc/sdk_full_documentation.md @@ -0,0 +1,269 @@ + +# OpenCAPIF SDK full documentation + +The OpenCAPIF SDK facilitates the integration of applications with the CAPIF NF. It offers various features for manual usage, automated scripting, and direct integration into application code. + +This documentation provides a step-by-step guide for utilizing the SDK, detailing its functionalities. Before proceeding, ensure all prerequisites are met, and review the available testing modes for the SDK. + +## Getting Started + +Before using the SDK, the following steps should be completed: +- Meet the [requirements](../README.md), +- Follow the [installation instructions](./sdk_developers.md), +- Configure the SDK by completing the relevant sections in the [configuration guide](./sdk_configuration.md), depending on the CAPIF role the Network App will assume. + + +## Available SDK Usage Modes + +![GENERAL CAPIF USAGE FLOW](./images/flows_updated_opencapif.jpg) + +The repository provides two modes for utilizing the OpenCAPIF SDK: + +1. **Development Mode**: The SDK can be imported directly into code for development purposes. Sample applications using the SDK are available in the [network_app_samples](../network_app_samples/) folder. + +2. **Manual Mode**: A set of Python [scripts](../scripts/) can be used to manually test each integration step. For manual usage, it is necessary to complete the utilities file with absolute paths from the target environment to finalize SDK configuration. + +**IMPORTANT**: All SDK configuration files must be filled out based on the intended role and features. Further details can be found in the [Configuration Section](./sdk_configuration.md). + +**NOTE**: The register file is not required for SDK usage, only for SDK consumers that wish to create their `capif_username`. + +## Table of Contents + +As outlined in the [Network App developers section](../README.md), the OpenCAPIF SDK supports two primary roles: + +- [Provider Network App](#provider-network-app) + - [Important Information for Providers](#important-information-for-providers) + - [Provider Onboarding](#provider-onboarding) + - [Service Publishing](#service-publishing) + - [Service Deletion](#service-deletion) + - [Service Updates](#service-update) + - [Get Published Services](#get-services) + - [Get All Published Services](#get-all-services) + - [Update and Offboard Provider](#update-and-offboard-provider) +- [Invoker Network App](#invoker-network-app) + - [Important Information for Invokers](#important-information-for-invokers) + - [Invoker Onboarding](#invoker-onboarding) + - [Service Discovery](#service-discovery) + - [Obtain JWT Tokens](#obtain-jwt-tokens) + - [Check authentication](#check-authentication) + - [Update and Offboard Invoker](#update-and-offboard-invoker) +- [Other Features](#other-features) + - [Openapi translation](#openapi-translation) + - [CAPIF Registration and Login](#capif-registration-and-login) + - [CAPIF Deregistration and Logout](#capif-registration-and-login) + +## Provider Network App + +The OpenCAPIF SDK enables efficient implementation of invoker functionality for Network App. This section details the SDK features related to CAPIF providers. + +### Important Information for Providers + +Within the `provider_folder`, directories are created based on the registered `capif_username`. Each folder contains: + +- `provider_service_ids.json`: Contains all APFs and AEFs IDs onboarded with the associated username. +- `capif__.json`: Stores the last payload for any published or updated API. +- `service_received.json`: Stores responses for Get API or Get All APIs. +- `provider_capif_ids.json`: A list of currently published APIs along with their IDs. + +All the configuration values are available within the object capif_provider_connector. + +The provider_capif_ids variable stores the `provider_service_ids.json` content in a dictionary form. + +### Provider Onboarding + +OpenCAPIF SDK references: +- **Function**: `onboard_provider()` +- **Script**: `provider_capif_connector.py` + +The SDK simplifies the onboarding process, allowing providers to register multiple APFs and AEFs. All APFs, AEFs, and AMF certificates are created and stored in `provider_service_ids.json`. + +![Provider_onboard](./images/flows_provider_onboard.jpg) + +### Service Publishing + +OpenCAPIF SDK references: +- **Function**: `publish_services()` +- **Script**: `provider_publish_api.py` + +The SDK streamlines API publishing with the option to select specific APFs and AEFs. A copy of the uploaded API is stored in `capif__.json`, and the `provider_capif_ids.json` is updated with the API name and its ID. + +It is also important to have previously prepared the **API schema description** file of the API to be published. **This file must follow the [CAPIF_Publish_Service_API](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Publish_Service_API.yaml) 3GPP specification.** + +To obtain this schema, opencapif_sdk has a facility to translate Openapi structures to ServiceAPIDescription schemas. +More information:[Translator functionality](#openapi-translation) + +**Important**: The SDK will automatically add in the `custom operations`, within the API description path, the capabilities for exposing the [AEF_security](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_AEF_Security_API.yaml) methods. The developer must code this API endpoints. + +**Required SDK inputs**: +- publisher_apf_id +- publisher_aefs_ids +- api_description_path + +### Service Deletion + +OpenCAPIF SDK references: +- **Function**: `unpublish_service()` +- **Script**: `provider_unpublish_api.py` + +The SDK simplifies API deletion. Service deletion requires prior onboarding and service publication. + +**Required SDK inputs**: +- publisher_apf_id +- publisher_aefs_ids + +### Service Update + +OpenCAPIF SDK references: +- **Function**: `update_service()` +- **Script**: `provider_update_api.py` + +This function enables to update a previously registered API. Selecting APFs and AEFs is possible. Onboarding and service publishing are prerequisites. + +**Required SDK inputs**: +- service_api_id +- publisher_apf_id +- publisher_aefs_ids + +### Get Services + +OpenCAPIF SDK references: +- **Function**: `get_service()` +- **Script**: `provider_get_published_api.py` + +Retrieve information of a previously published service, stored in `service_received.json`. Prior onboarding and service publication are necessary. + +**Required SDK inputs**: +- service_api_id +- publisher_apf_id + +### Get All Services + +OpenCAPIF SDK references: +- **Function**: `get_all_services()` +- **Script**: `provider_get_all_published_api.py` + +Retrieve information about all previously published services in `service_received.json`. Ensure you are [onboarded as a provider](#provider-onboarding) and have [published services](#services-publishing). + +**Required SDK input**: +- publisher_apf_id + +![Provider_publish](./images/flows_provider_publish_functions.jpg) + +### Update and Offboard Provider + +OpenCAPIF SDK references: +- **Functions**: `update_provider()` and `offboard_provider()` +- **Scripts**: `provider_capif_connector_update.py` and `provider_capif_connector_offboarding.py` + +`update_provider()`: The provider updates his features such as `APFs`, `AEFs`, etc... +`offboard_provider()`: The provider offboards from CAPIF, this will cause the erase of the published APIs that were currently exposed. + +The provider must be onboarded before using these features. + +![Provider_update-offboard](./images/flows_provider_update_offboard.jpg) + +## Invoker Network App + +The OpenCAPIF SDK enables efficient implementation of invoker functionality for Network App. This section details the SDK features related to CAPIF invokers. + +### Important Information for Invokers + +Within the `invoker_folder`, directories are created based on the registered `capif_username`. These directories contain: + +- `capif_api_security_context_details.json`: Stores details about the invoker, including: + 1. `api_invoker_id` + 2. Discovered APIs and their information (if Service Discovery has been used) + 3. JWT access tokens for discovered APIs (if Service Get Token functionality has been used) + +The `token` variable is also available for retrieving the JWT token after the get_tokens() functionality. + +The invoker_capif_details variable stores the `capif_api_security_context_details.json` content. + +### Invoker onboarding + +OpenCAPIF SDK references: +- **Function**: `onboard_invoker()` +- **Script**: `invoker_capif_connector.py` + +The SDK streamlines the invoker onboarding process, storing the `api_invoker_id` in the `capif_api_security_context_details.json`. + +![Invoker_onboard](./images/flows_invoker_onboard.jpg) + +### Service Discovery + +OpenCAPIF SDK references: +- **Function**: `discover()` +- **Script**: `invoker_service_discover.py` + +The [discover_filter](./sdk_configuration.md) can be used to retrieve access to APIs. The invoker must be onboarded before using this function. Discovered APIs and their information are stored in `capif_api_security_context_details.json`. + +**Note**: A 404 warning may be received during the first discovery run, prompting registration for the security service. + +Use the [discover_filter](./sdk_configuration.md) to retrieve access to target APIs. Ensure you are [onboarded as an invoker](#invoker-onboarding) before using this feature. + +![Invoker_discover](./images/flows_invoker_discover.jpg) + +### Obtain JWT Tokens + +OpenCAPIF SDK references: +- **Function**: `get_tokens()` +- **Script**: `invoker_service_get_token.py` + +The SDK facilitates JWT token creation for secure access to target APIs. This process stores JWT access token in `capif_api_security_context_details.json`. + +![Invoker_get_token](./images/flows_invoker_get_tokens.jpg) + +### Check authentication + +OpenCAPIF SDK references: +- **Function**: `check_authentication()` + +The SDK allows the Network App Invoker to check the `supported_features` from the target Provider's API exposing function (AEF). + +It is mandatory to have obtained the [JWT token](#obtain-jwt-tokens) previously. + +**Required SDK inputs**: +- check_authentication_data + +![Invoker_check_authentication](./images/flows_invoker_check_authentication.jpg) + +### Update and Offboard Invoker + +OpenCAPIF SDK references: +- **Functions**: `update_invoker()` and `offboard_invoker()` +- **Scripts**: `invoker_capif_connector_update.py` and `invoker_capif_connector_offboarding.py` + +Onboarding is required before utilizing these functions. + +![Invoker_update-offboard](./images/flows_invoker_update_offboard.jpg) + +## Other Features + +### Openapi translation + +The `api_description_path` must point to the Publish API to be shared, and it should follow the [ServiceAPIDescription](https://github.com/jdegre/5GC_APIs/blob/Rel-18/TS29222_CAPIF_Publish_Service_API.yaml) schema. + +This schema could be obtained by applying this code. +```python + import opencapif_sdk + + translator = opencapif_sdk.api_schema_translator("./path/to/openapi.yaml") + translator.build("api_description_name",ip="0.0.0.0",port=9090) +``` +This code will read `openapi.yaml`, ensure the structure of it and translate the content into ServiceAPIDescription schema, then will create a .json named `api_description_name`. Also it is necessary to fill the ip and port fields to create correctly the schema. + +### CAPIF Registration and Login + +OpenCAPIF SDK reference: +* **Script**: `register_and_login.py` + +Simplifies the login process for admin users and creates a CAPIF user. + +### CAPIF Deregistration and Logout + +OpenCAPIF SDK reference: +* **Script**: `deregister_and_login.py` + +Simplifies the logout process for admin users and removes a CAPIF user. + +![Register picture](./images/flows_sdk_with_register.jpg) diff --git a/installation/requirements.txt b/installation/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..416b0d8a86ea9611aa47aa177d13eabe0d77ff34 --- /dev/null +++ b/installation/requirements.txt @@ -0,0 +1,24 @@ +requests==2.32.3 +PyYAML==6.0.1 +cryptography==38.0.4 +pyOpenSSL==22.1.0 +urllib3==2.2.2 +certifi==2024.7.4 +idna==3.7 +Flask==3.0.3 +Flask-JWT-Extended==4.6.0 +Jinja2==3.1.4 +MarkupSafe==2.1.5 +six==1.16.0 +typing-extensions==3.10.0.2 +Werkzeug==3.0.4 +pytest==8.3.2 +flake8==3.9.2 +coverage==4.5.4 +mccabe==0.6.1 +pycodestyle==2.7.0 +pyflakes==2.3.1 +python-dateutil==2.9.0.post0 +jinja2-time==0.2.0 +text-unidecode==1.3 +binaryornot==0.4.4 diff --git a/network_app_samples/network_app_invoker_sample/capif_sdk_config_sample.json b/network_app_samples/network_app_invoker_sample/capif_sdk_config_sample.json new file mode 100644 index 0000000000000000000000000000000000000000..c68a7ac08d0962dc5216c860d26698b231dc8a07 --- /dev/null +++ b/network_app_samples/network_app_invoker_sample/capif_sdk_config_sample.json @@ -0,0 +1,65 @@ +{ + "capif_host": "", + "register_host": "", + "capif_https_port": "", + "capif_register_port": "", + "capif_username": "", + "capif_password": "", + "debug_mode": "", + "invoker":{ + "invoker_folder": "", + "capif_callback_url": "", + "supported_features":"", + "check_authentication_data":{ + "ip":"", + "port":"" + }, + "cert_generation":{ + "csr_common_name": "", + "csr_organizational_unit": "", + "csr_organization": "", + "csr_locality": "", + "csr_state_or_province_name": "", + "csr_country_name": "", + "csr_email_address": "" + }, + "discover_filter": { + "api-name": "", + "api-version": "", + "comm-type": "", + "protocol": "", + "aef-id": "", + "data-format": "", + "api-cat": "", + "preferred-aef-loc": "", + "req-api-prov-name": "", + "supported-features": "", + "api-supported-features": "", + "ue-ip-addr": "", + "service-kpis": "" + } + }, + "provider":{ + "provider_folder": "", + "apfs": "", + "aefs": "", + "publish_req": { + "service_api_id": "", + "publisher_apf_id": "", + "publisher_aefs_ids": [ + "", + "" + ] + }, + "cert_generation":{ + "csr_common_name": "", + "csr_organizational_unit": "", + "csr_organization": "", + "csr_locality": "", + "csr_state_or_province_name": "", + "csr_country_name": "", + "csr_email_address": "" + }, + "api_description_path": "" + } +} diff --git a/network_app_samples/network_app_invoker_sample/network_app_invoker.py b/network_app_samples/network_app_invoker_sample/network_app_invoker.py new file mode 100644 index 0000000000000000000000000000000000000000..865bd2db0c09bf47b3f89787aa86d60a8f1ea094 --- /dev/null +++ b/network_app_samples/network_app_invoker_sample/network_app_invoker.py @@ -0,0 +1,105 @@ +import json +import time +import requests +from opencapif_sdk import capif_invoker_connector, service_discoverer + +# Path to the OpenCAPIF SDK configuration file +capif_sdk_config_path = "./capif_sdk_config_sample.json" + +import requests +import json + +def update_token(access_token): + """ + Update the Postman environment with the new access token. + """ + # Your Postman API Key + postman_api_key = "PMAK-66fd117ac0a4eb0001e7881b-ecf2b89675a6a1bfd0856838e3b1aa552a" + + # Postman Environment ID + environment_id = "36540500-bfaa8442-180f-4247-892d-b0605581b6a6" + + # Postman API URL to modify environments + url = f"https://api.getpostman.com/environments/{environment_id}" + + # Headers for authentication and content type + headers = { + "X-Api-Key": postman_api_key, + "Content-Type": "application/json" + } + + # Get the current environment details + response = requests.get(url, headers=headers) + + # Check if the request was successful + if response.status_code == 200: + environment_data = response.json() + + # Check if the variable already exists + variables = environment_data['environment']['values'] + variable_found = False + for var in variables: + if var['key'] == "ACCESS TOKEN": + + # Update both initial and current value + var['value'] = access_token # Set initial value + var['current_value'] = access_token # Set current value + variable_found = True + break + + if not variable_found: + # If the variable doesn't exist, append it + variables.append({ + "key": "ACCESS TOKEN", + "value": access_token, # Initial value + "current_value": access_token, # Current value + "enabled": True + }) + + # Send the PUT request to update the environment + updated_data = { + "environment": { + "name": environment_data['environment']['name'], + "values": variables + } + } + + # Update the environment in Postman + put_response = requests.put(url, headers=headers, data=json.dumps(updated_data)) + + if put_response.status_code == 200: + print("Access token updated successfully.") + else: + print(f"Error updating the variable: {put_response.status_code}") + else: + print(f"Error getting the environment: {response.status_code}") + + +if __name__ == "__main__": + # Initialize the CAPIF invoker + capif_connector = capif_invoker_connector(config_file=capif_sdk_config_path) + capif_connector.onboard_invoker() + print("INVOKER ONBOARDING COMPLETED") + # Now, with the certificates available, proceed with discovery + discoverer = service_discoverer(config_file=capif_sdk_config_path) + discoverer.discover() + while True: + discoverer.get_tokens() + + """ details = discoverer.invoker_capif_details + lenght = len(details["registered_security_contexes"]) + ip = details["registered_security_contexes"][lenght-1]["aef_profiles"][0]["ip"] + port = details["registered_security_contexes"][lenght-1]["aef_profiles"][0]["port"] + discoverer.check_authentication_data.update({"ip": ip, "port": port}) + discoverer.check_authentication() """ + + # Load API details from the JSON file + token = discoverer.token + + # Update the Postman token with the retrieved access token + update_token(token) + + print(token) + + # Wait for 599 seconds before the next update + time.sleep(599) diff --git a/network_app_samples/network_app_invoker_sample/postman/Demo-SDK.postman_collection.json b/network_app_samples/network_app_invoker_sample/postman/Demo-SDK.postman_collection.json new file mode 100644 index 0000000000000000000000000000000000000000..cb8f1d1a49df6864db637cfbb14c6fdea79d6de5 --- /dev/null +++ b/network_app_samples/network_app_invoker_sample/postman/Demo-SDK.postman_collection.json @@ -0,0 +1,537 @@ +{ + "info": { + "_postman_id": "4bc02799-96ca-4e10-a1fa-bb880f37ffb4", + "name": "Demo-SDK", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "36540500", + "_collection_link": "https://crimson-escape-958452.postman.co/workspace/My-Workspace~446d9f89-afac-41d1-869c-90d989948481/collection/36540500-4bc02799-96ca-4e10-a1fa-bb880f37ffb4?action=share&source=collection_link&creator=36540500" + }, + "item": [ + { + "name": "Resilience", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{ACCESS TOKEN}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8088/resilience", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8088", + "path": [ + "resilience" + ] + } + }, + "response": [] + }, + { + "name": "Resilience", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{ACCESS TOKEN}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"resilience_level\": \"high\"\n}\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8088/resilience", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8088", + "path": [ + "resilience" + ] + } + }, + "response": [] + }, + { + "name": "Specific Resilience", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{ACCESS TOKEN}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8088/resilience/1", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8088", + "path": [ + "resilience", + "1" + ], + "query": [ + { + "key": "", + "value": null, + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "Specific Resilience", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{ACCESS TOKEN}}", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"level\": \"high\"\n}\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8088/resilience/1", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8088", + "path": [ + "resilience", + "1" + ] + } + }, + "response": [] + }, + { + "name": "Specific Resilience", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{ACCESS TOKEN}}", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n\"name\": \"echeva_0\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8088/resilience/1", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8088", + "path": [ + "resilience", + "1" + ] + } + }, + "response": [] + }, + { + "name": "Resilience slice", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{ACCESS TOKEN}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8088/slice/resilience", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8088", + "path": [ + "slice", + "resilience" + ] + } + }, + "response": [] + }, + { + "name": "Resilience slice", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{ACCESS TOKEN}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"resilience_config\": \"custom\"\n}\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8088/slice/resilience", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8088", + "path": [ + "slice", + "resilience" + ] + } + }, + "response": [] + }, + { + "name": "Qos", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{ACCESS TOKEN}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8888/1/qos", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8888", + "path": [ + "1", + "qos" + ] + } + }, + "response": [] + }, + { + "name": "Qos", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{ACCESS TOKEN}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"level\": \"high\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8088/slice/resilience", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8088", + "path": [ + "slice", + "resilience" + ] + } + }, + "response": [] + }, + { + "name": "Tsn", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{ACCESS TOKEN}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8888/profile", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8888", + "path": [ + "profile" + ] + } + }, + "response": [] + }, + { + "name": "Tsn", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{ACCESS TOKEN}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"parameter\": \"value\"\n}\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8888/Network_apply", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8888", + "path": [ + "Network_apply" + ] + } + }, + "response": [] + }, + { + "name": "Slice", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{ACCESS TOKEN}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8888/slice", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8888", + "path": [ + "slice" + ] + } + }, + "response": [] + }, + { + "name": "Slice", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{ACCESS TOKEN}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"status\": \"active\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8888/slice", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8888", + "path": [ + "slice" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/network_app_samples/network_app_invoker_sample/postman/SDK-DEMO.postman_environment.json b/network_app_samples/network_app_invoker_sample/postman/SDK-DEMO.postman_environment.json new file mode 100644 index 0000000000000000000000000000000000000000..d543aa8a70e6f05c597c742665434ae093adad06 --- /dev/null +++ b/network_app_samples/network_app_invoker_sample/postman/SDK-DEMO.postman_environment.json @@ -0,0 +1,14 @@ +{ + "id": "bfaa8442-180f-4247-892d-b0605581b6a6", + "name": "SDK-DEMO", + "values": [ + { + "key": "ACCESS TOKEN", + "value": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTcyNzg2MzI1MSwianRpIjoiMDhlM2FhMmItNTI3ZC00NDFiLTljOWYtNWQ3Yjk1NDUyZGQ1IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6IklOVjc2NzFiMDEwNWNhNWUzZjM2MGJlYTQ3NDY5NzI5YiIsIm5iZiI6MTcyNzg2MzI1MSwiZXhwIjoxNzI3ODYzODUxLCJpc3MiOiJJTlY3NjcxYjAxMDVjYTVlM2YzNjBiZWE0NzQ2OTcyOWIiLCJzY29wZSI6IjNncHAjQUVGNzJlN2Y5NDRhMjBmZTA3MmU5ZGIyNjM1MzNiNjQ2OlRlc3Q2NjtBRUY1NjA4YjYwNmFiMDhjZmQ3MTExMDE4ZTM5MzY2NjY6VGVzdDc3O0FFRmQ4MzZlOWI0OTE2MTY3M2UzZWU2NjMyYzQwOTUyMDpEZXBsb3ltZW50O0FFRjQ2MTBjNjNmY2I5NDA1MmViODg0N2FiZDJjNWVkNzpEZXBsb3ltZW50MTtBRUZlNGQyOTA2MTczOWM4MGFjZWIyNjkwNDM1NDIwM2E6a3M4NTAwX2dhdGV3YXk7QUVGNGNhMmNmYjk3MDBmOTE3YjhjY2UxMzY4NTk5YzYwOmtzODUwMF9nYXRld2F5X2RldmVsb3BtZW50O0FFRmY5OTExMTY2ZGFkZDcyZDlmMjMwNWU5MTIxNzc2ZDpUZXN0LTI7QUVGODViZWYwNWIzMGIxZTlkMzJjNWI3Y2I2ZWM3ZDAwOlRlc3QtMjtBRUZjNjhiYjA5MDU0NmRmYzExZDM5ODZhZGJjY2JiMTI6VGVzdC0yO0FFRmExMDBiMWE3NmNlMGE1ZTg0OTVjNmViZjQzYmY0YzpkZW1vX2FwaV9PQ0Y7QUVGZjVlMzA0YWU5YjM3NDk2N2Y1NDdhMDk2MmJkZmQyOjZHLXJlc2lsaWVuY2U7QUVGZDhmZmI2ZTJlMjFmZDQ5N2JlZDExZGE0MzNiYjA5OjZHLXJlc2lsaWVuY2U7QUVGZjVlMzA0YWU5YjM3NDk2N2Y1NDdhMDk2MmJkZmQyOjVHLU5ldEFwcC1BUEk7QUVGZDhmZmI2ZTJlMjFmZDQ5N2JlZDExZGE0MzNiYjA5OjVHLU5ldEFwcC1BUEk7QUVGZWU1YjU5ZGYyMzFlZjFjZDQwMjRkNmQ2Y2Q1NWU0OjVHLU5ldEFwcC1BUEkifQ.SrPqTUSvzVnO8LWvpfWNIpyFrTOJlK7cTcjxbc8_tDUEC1Ot1sujCJLdXBd0RjatnqTNWnGw063wON7gxFkwMcr3AxTw7Lnf4K7r31VWCfZB5odTP5prRrTpSZPhuTdEsMCdSL-vh56VbJJU0IAqG71cm08BXcrfBF08zYJpVBcTvNAi1J96-bo0QlbH2WYfHXEkcUirJ_DSosTCMyQIS2tTwoUd6GBDQvnKorjgGGyogP4QDZQYjiNOPUFmWmDmDVjAwncplqxfppukanQgBU_M1c9JeQYiNrkz3X8dhRlVqdwGXpqOBntWyt8mrQAf7GkWjYcMhYm17dyb5tpcPg", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2024-10-02T11:52:57.845Z", + "_postman_exported_using": "Postman/11.14.1" +} \ No newline at end of file diff --git a/network_app_samples/network_app_provider_sample/capif_sdk_config_sample.json b/network_app_samples/network_app_provider_sample/capif_sdk_config_sample.json new file mode 100644 index 0000000000000000000000000000000000000000..f2c2c796dae3df8f6348c38bef715ea5aaa32040 --- /dev/null +++ b/network_app_samples/network_app_provider_sample/capif_sdk_config_sample.json @@ -0,0 +1,64 @@ +{ + "capif_host": "", + "register_host": "", + "capif_https_port": "", + "capif_register_port": "", + "capif_username": "", + "capif_password": "", + "debug_mode": "", + "invoker":{ + "invoker_folder": "", + "capif_callback_url": "", + "check_authentication_data":{ + "ip":"", + "port":"" + }, + "cert_generation":{ + "csr_common_name": "", + "csr_organizational_unit": "", + "csr_organization": "", + "csr_locality": "", + "csr_state_or_province_name": "", + "csr_country_name": "", + "csr_email_address": "" + }, + "discover_filter": { + "api-name": "", + "api-version": "", + "comm-type": "", + "protocol": "", + "aef-id": "", + "data-format": "", + "api-cat": "", + "preferred-aef-loc": "", + "req-api-prov-name": "", + "supported-features": "", + "api-supported-features": "", + "ue-ip-addr": "", + "service-kpis": "" + } + }, + "provider":{ + "provider_folder": "", + "cert_generation":{ + "csr_common_name": "", + "csr_organizational_unit": "", + "csr_organization": "", + "csr_locality": "", + "csr_state_or_province_name": "", + "csr_country_name": "", + "csr_email_address": "" + }, + "apfs": "2", + "aefs": "3", + "publish_req": { + "service_api_id": "", + "publisher_apf_id": "", + "publisher_aefs_ids": [ + "", + "" + ] + }, + "api_description_path": "" + } +} diff --git a/network_app_samples/network_app_provider_sample/nef_upf_vendor_1.json b/network_app_samples/network_app_provider_sample/nef_upf_vendor_1.json new file mode 100755 index 0000000000000000000000000000000000000000..8cc2fa42c0ccad2f63b4ba09d6b636db5f8fd791 --- /dev/null +++ b/network_app_samples/network_app_provider_sample/nef_upf_vendor_1.json @@ -0,0 +1,330 @@ +{ + "apiName": "6G-resilience", + "aefProfiles": [ + { + "aefId": "AEF6fc1d116574bcc797ecd74b8902b35", + "versions": [ + { + "apiVersion": "6G_Resilience_v1", + "expiry": "2100-11-30T10:32:02.004Z", + "resources": [ + { + "resourceName": "RESILIENCE_MANAGEMENT", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/resilience", + "custOpName": "http_post", + "operations": [ + "GET", + "POST" + ], + "description": "Endpoint to manage resilience functionalities in 6G networks" + }, + { + "resourceName": "RESILIENCE_SINGLE", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/resilience/{profileId}", + "custOpName": "http_get", + "operations": [ + "GET", + "PUT", + "DELETE" + ], + "description": "Endpoint to manage a single resilience profile" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "Custom operation for managing 6G resilience parameters" + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + } + ] + } + ], + "protocol": "HTTP_1_1", + "dataFormat": "JSON", + "securityMethods": [ + "Oauth" + ], + "interfaceDescriptions": [ + { + "ipv4Addr": "localhost", + "port": 8088, + "securityMethods": [ + "Oauth" + ] + } + ] + }, + { + "aefId": "AEFa8e49e01d179c79e6a4750a8363f1d", + "versions": [ + { + "apiVersion": "6G_Resilience_v1", + "expiry": "2100-11-30T10:32:02.004Z", + "resources": [ + { + "resourceName": "SLICE_RESILIENCE", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/slice/resilience", + "custOpName": "http_post", + "operations": [ + "GET", + "POST" + ], + "description": "Endpoint for managing resilience in network slices" + }, + { + "resourceName": "SLICE_SINGLE_RESILIENCE", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/slice/{sliceId}/resilience", + "custOpName": "http_get", + "operations": [ + "GET", + "PUT", + "DELETE" + ], + "description": "Endpoint for managing a single slice's resilience" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "Custom operation for configuring resilience in network slices" + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + } + ] + } + ], + "protocol": "HTTP_1_1", + "dataFormat": "JSON", + "securityMethods": [ + "Oauth", + "PSK" + ], + "interfaceDescriptions": [ + { + "ipv4Addr": "localhost", + "port": 8088, + "securityMethods": [ + "Oauth" + ] + } + ] + } + ], + "description": "API of dummy Network-App to test", + "supportedFeatures": "fffff", + "shareableInfo": { + "isShareable": true, + "capifProvDoms": [ + "string" + ] + }, + "serviceAPICategory": "string", + "apiSuppFeats": "fffff", + "pubApiPath": { + "ccfIds": [ + "string" + ] + }, + "ccfId": "string" +} \ No newline at end of file diff --git a/network_app_samples/network_app_provider_sample/nef_upf_vendor_2.json b/network_app_samples/network_app_provider_sample/nef_upf_vendor_2.json new file mode 100755 index 0000000000000000000000000000000000000000..5feec72a09b690011fb88feb452d9f30908d1391 --- /dev/null +++ b/network_app_samples/network_app_provider_sample/nef_upf_vendor_2.json @@ -0,0 +1,454 @@ +{ + "apiName": "5G-Network-App-API", + "aefProfiles": [ + { + "aefId": "AEF6fc1d116574bcc797ecd74b8902b35", + "versions": [ + { + "apiVersion": "QoS_v1", + "expiry": "2100-11-30T10:32:02.004Z", + "resources": [ + { + "resourceName": "QOS_MANAGEMENT", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/{scsAsId}/qos", + "custOpName": "http_post", + "operations": [ + "GET", + "POST" + ], + "description": "Endpoint to manage QoS levels for network traffic" + }, + { + "resourceName": "QOS_PROFILE_SINGLE", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/{scsAsId}/qos/{profileId}", + "custOpName": "http_get", + "operations": [ + "GET", + "PUT", + "DELETE" + ], + "description": "Endpoint to manage single QoS profile" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "Custom operation for managing QoS parameters" + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + } + ] + } + ], + "protocol": "HTTP_1_1", + "dataFormat": "JSON", + "securityMethods": [ + "Oauth", + "PSK" + ], + "interfaceDescriptions": [ + { + "ipv4Addr": "localhost", + "port": 8888, + "securityMethods": [ + "Oauth" + ] + } + ] + }, + { + "aefId": "AEFa8e49e01d179c79e6a4750a8363f1d", + "versions": [ + { + "apiVersion": "TSN_v1", + "expiry": "2100-11-30T10:32:02.004Z", + "resources": [ + { + "resourceName": "TSN_LIST_PROFILES", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/profile", + "custOpName": "http_get", + "operations": [ + "GET" + ], + "description": "Endpoint for retrieving the list of available TSN profiles" + }, + { + "resourceName": "TSN_DETAIL_PROFILE", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/profile?name={profileName}", + "custOpName": "http_get", + "operations": [ + "GET" + ], + "description": "Endpoint for retrieving information about a single TSN profile" + }, + { + "resourceName": "TSN_APPLY_CONFIGURATION", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/apply", + "custOpName": "http_post", + "operations": [ + "POST" + ], + "description": "Endpoint for configuring TSN connection parameters" + }, + { + "resourceName": "TSN_CLEAR_CONFIGURATION", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/clear", + "custOpName": "http_post", + "operations": [ + "POST" + ], + "description": "Endpoint for removing a previous TSN connection configuration" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "Custom operation for managing TSN profiles" + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + } + ] + } + ], + "protocol": "HTTP_1_1", + "dataFormat": "JSON", + "securityMethods": [ + "Oauth" + ], + "interfaceDescriptions": [ + { + "ipv4Addr": "localhost", + "port": 8888, + "securityMethods": [ + "Oauth" + ] + } + ] + }, + { + "aefId": "AEFc1a1d48f93449af79b5289cc3b1ae1", + "versions": [ + { + "apiVersion": "NetworkSlicing_v1", + "expiry": "2100-11-30T10:32:02.004Z", + "resources": [ + { + "resourceName": "SLICE_MANAGEMENT", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/slice", + "custOpName": "http_post", + "operations": [ + "GET", + "POST" + ], + "description": "Endpoint for managing network slices" + }, + { + "resourceName": "SLICE_SINGLE", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/slice/{sliceId}", + "custOpName": "http_get", + "operations": [ + "GET", + "PUT", + "DELETE" + ], + "description": "Endpoint for managing a single network slice" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "Custom operation for configuring network slices" + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + } + ] + } + ], + "protocol": "HTTP_1_1", + "dataFormat": "JSON", + "securityMethods": [ + "Oauth", + "PSK" + ], + "interfaceDescriptions": [ + { + "ipv4Addr": "localhost", + "port": 8888, + "securityMethods": [ + "Oauth" + ] + } + ] + } + ], + "description": "API of dummy Network-App to test", + "supportedFeatures": "fffff", + "shareableInfo": { + "isShareable": true, + "capifProvDoms": [ + "string" + ] + }, + "serviceAPICategory": "string", + "apiSuppFeats": "fffff", + "pubApiPath": { + "ccfIds": [ + "string" + ] + }, + "ccfId": "string" +} \ No newline at end of file diff --git a/network_app_samples/network_app_provider_sample/network_app_provider.py b/network_app_samples/network_app_provider_sample/network_app_provider.py new file mode 100644 index 0000000000000000000000000000000000000000..3750e497c8f7fd5ee98481c3a4639dac0dea95d3 --- /dev/null +++ b/network_app_samples/network_app_provider_sample/network_app_provider.py @@ -0,0 +1,365 @@ +import threading +from opencapif_sdk import capif_provider_connector +from OpenSSL import crypto +from flask_jwt_extended import jwt_required, JWTManager +from flask import Flask, jsonify, request, redirect +import json +from werkzeug import serving + + + +capif_sdk_config_path = "./capif_sdk_config_sample.json" + +resilience_app = Flask("resilience_app") +jwt_flask = JWTManager(resilience_app) + + +def config_resilience(): + # JWT Configuration + resilience_app.config['JWT_ALGORITHM'] = 'RS256' + resilience_app.config['JWT_PUBLIC_KEY'] = "YourPublicKeyHere" + + with open("/Users/IDB0128/Documents/OpenCapif/test_provider_certificate_folder/echeva_0/capif_cert_server.pem", "rb") as cert_file: + cert = cert_file.read() + + crtObj = crypto.load_certificate(crypto.FILETYPE_PEM, cert) + pubKeyObject = crtObj.get_pubkey() + pubKeyString = crypto.dump_publickey(crypto.FILETYPE_PEM, pubKeyObject) + + resilience_app.config['JWT_ALGORITHM'] = 'RS256' + resilience_app.config['JWT_PUBLIC_KEY'] = pubKeyString + + +# Dummy data for resilience management (In practice, this would connect to a database or service) +resilience_profiles = { + "1": {"profileId": 1, "status": "active"}, + "2": {"profileId": 2, "status": "inactive"} +} + + +@resilience_app.route("/resilience", methods=["GET", "POST"]) +@jwt_required() +def manage_resilience(): + """Endpoint to manage resilience functionalities in 6G networks""" + if request.method == "GET": + return jsonify({"message": "GET request for resilience management received."}) + + if request.method == "POST": + data = request.get_json() + return jsonify({"message": "Resilience management POST request received", "data": data}) + + +@resilience_app.route("/resilience/", methods=["GET", "PUT", "DELETE"]) +@jwt_required() +def manage_single_resilience(profileId): + """Endpoint to manage a single resilience profile""" + if request.method == "GET": + profile = resilience_profiles.get(profileId, None) + if profile: + return jsonify({"profile": profile}) + else: + return jsonify({"message": f"Profile {profileId} not found"}), 404 + + if request.method == "PUT": + data = request.get_json() + resilience_profiles[profileId] = data + return jsonify({"message": f"Profile {profileId} updated", "data": data}) + + if request.method == "DELETE": + if profileId in resilience_profiles: + del resilience_profiles[profileId] + return jsonify({"message": f"Profile {profileId} deleted"}) + else: + return jsonify({"message": f"Profile {profileId} not found"}), 404 + + +@resilience_app.route("/slice/resilience", methods=["GET", "POST"]) +@jwt_required() +def manage_slice_resilience(): + """Endpoint for managing resilience in network slices""" + if request.method == "GET": + return jsonify({"message": "GET request for slice resilience received."}) + + if request.method == "POST": + data = request.get_json() + return jsonify({"message": "POST request for slice resilience management received", "data": data}) + + +@resilience_app.route("/slice//resilience", methods=["GET", "PUT", "DELETE"]) +@jwt_required() +def manage_single_slice_resilience(sliceId): + """Endpoint for managing a single slice's resilience""" + if request.method == "GET": + return jsonify({"message": f"GET request for slice {sliceId} resilience received"}) + + if request.method == "PUT": + data = request.get_json() + return jsonify({"message": f"PUT request to update slice {sliceId}'s resilience", "data": data}) + + if request.method == "DELETE": + return jsonify({"message": f"DELETE request to remove slice {sliceId}'s resilience"}) + + +@resilience_app.route("/aef-security/v1/check-authentication", methods=["POST"]) +@jwt_required() +def custom_operation_check(): + if request.method == "POST": + try: + # Extraer el JSON del cuerpo del request + data = request.get_json() + + # Ejemplo de lógica para determinar si redirigir + # Este es un ejemplo ficticio, deberías reemplazarlo con la lógica real de tu caso + if "redirect_temporary" in data: + # Si se detecta "redirect_temporary", enviamos un 307 con un header Location + return redirect("https://alternative-uri.example.com", code=307) + elif "redirect_permanent" in data: + # Si se detecta "redirect_permanent", enviamos un 308 con un header Location + return redirect("https://alternative-uri.example.com", code=308) + + # Si no hay redirección, devolvemos el 200 OK con los datos + + return jsonify(data), 200 + + except Exception as e: + print("Error:", str(e)) + return jsonify({"error": str(e)}), 500 + + +Network_app = Flask("Network_app") + +# JWT Configuration +jwt_flask = JWTManager(Network_app) + + +def config_network(): + with open("/Users/IDB0128/Documents/OpenCapif/test_provider_certificate_folder/echeva_0/capif_cert_server.pem", "rb") as cert_file: + cert = cert_file.read() + + crtObj = crypto.load_certificate(crypto.FILETYPE_PEM, cert) + pubKeyObject = crtObj.get_pubkey() + pubKeyString = crypto.dump_publickey(crypto.FILETYPE_PEM, pubKeyObject) + + Network_app.config['JWT_ALGORITHM'] = 'RS256' + Network_app.config['JWT_PUBLIC_KEY'] = pubKeyString + +# Dummy data for QoS, TSN, and Slicing management + + +qos_profiles = { + "1": {"profileId": 1, "level": "high"}, + "2": {"profileId": 2, "level": "medium"} +} + +tsn_profiles = { + "profile1": {"name": "profile1", "config": "default"}, + "profile2": {"name": "profile2", "config": "custom"} +} + +network_slices = { + "slice1": {"sliceId": "slice1", "status": "active"}, + "slice2": {"sliceId": "slice2", "status": "inactive"} +} + +# QoS Management Endpoints ### + + +@Network_app.route("//qos", methods=["GET", "POST"]) +@jwt_required() +def manage_qos(scsAsId): + """Endpoint to manage QoS levels for network traffic""" + if request.method == "GET": + return jsonify({"message": "GET request for QoS management", "scsAsId": scsAsId}) + + if request.method == "POST": + data = request.get_json() + return jsonify({"message": "POST request to manage QoS", "scsAsId": scsAsId, "data": data}) + + +@Network_app.route("//qos/", methods=["GET", "PUT", "DELETE"]) +@jwt_required() +def manage_single_qos_profile(scsAsId, profileId): + """Endpoint to manage single QoS profile""" + if request.method == "GET": + profile = qos_profiles.get(profileId) + if profile: + return jsonify({"profile": profile, "scsAsId": scsAsId}) + return jsonify({"message": f"Profile {profileId} not found"}), 404 + + if request.method == "PUT": + data = request.get_json() + qos_profiles[profileId] = data + return jsonify({"message": f"Profile {profileId} updated", "scsAsId": scsAsId, "data": data}) + + if request.method == "DELETE": + if profileId in qos_profiles: + del qos_profiles[profileId] + return jsonify({"message": f"Profile {profileId} deleted"}) + return jsonify({"message": f"Profile {profileId} not found"}), 404 + +# TSN Management Endpoints ### + + +@Network_app.route("/profile", methods=["GET"]) +@jwt_required() +def list_tsn_profiles(): + """Endpoint for retrieving the list of available TSN profiles""" + return jsonify({"profiles": tsn_profiles}) + + +@Network_app.route("/profile", methods=["GET"]) +@jwt_required() +def tsn_profile_by_name(): + """Endpoint for retrieving information about a single TSN profile""" + profile_name = request.args.get('name') + profile = tsn_profiles.get(profile_name) + if profile: + return jsonify({"profile": profile}) + return jsonify({"message": f"Profile {profile_name} not found"}), 404 + + +@Network_app.route("/Network_apply", methods=["POST"]) +@jwt_required() +def Network_apply_tsn_configuration(): + """Endpoint for configuring TSN connection parameters""" + data = request.get_json() + return jsonify({"message": "TSN configuration Network_applied", "data": data}) + + +@Network_app.route("/clear", methods=["POST"]) +@jwt_required() +def clear_tsn_configuration(): + """Endpoint for removing a previous TSN connection configuration""" + return jsonify({"message": "TSN configuration cleared"}) + +# Network Slicing Management Endpoints ### + + +@Network_app.route("/slice", methods=["GET", "POST"]) +@jwt_required() +def manage_slices(): + """Endpoint for managing network slices""" + if request.method == "GET": + return jsonify({"slices": network_slices}) + + if request.method == "POST": + data = request.get_json() + slice_id = f"slice{len(network_slices) + 1}" + network_slices[slice_id] = data + return jsonify({"message": "Network slice added", "sliceId": slice_id, "data": data}) + + +@Network_app.route("/aef-security/v1/check-authentication", methods=["POST"]) +@jwt_required() +def custom_operation_check_2(): + if request.method == "POST": + try: + # Extraer el JSON del cuerpo del request + data = request.get_json() + + # Ejemplo de lógica para determinar si redirigir + # Este es un ejemplo ficticio, deberías reemplazarlo con la lógica real de tu caso + if "redirect_temporary" in data: + # Si se detecta "redirect_temporary", enviamos un 307 con un header Location + return redirect("https://alternative-uri.example.com", code=307) + elif "redirect_permanent" in data: + # Si se detecta "redirect_permanent", enviamos un 308 con un header Location + return redirect("https://alternative-uri.example.com", code=308) + + # Si no hay redirección, devolvemos el 200 OK con los datos + return jsonify(data), 200 + + except Exception as e: + print("Error:", str(e)) + return jsonify({"error": str(e)}), 500 + + +@Network_app.route("/slice/", methods=["GET", "PUT", "DELETE"]) +@jwt_required() +def manage_single_slice(sliceId): + """Endpoint for managing a single network slice""" + if request.method == "GET": + slice_ = network_slices.get(sliceId) + if slice_: + return jsonify({"slice": slice_}) + return jsonify({"message": f"Slice {sliceId} not found"}), 404 + + if request.method == "PUT": + data = request.get_json() + network_slices[sliceId] = data + return jsonify({"message": f"Slice {sliceId} updated", "data": data}) + + if request.method == "DELETE": + if sliceId in network_slices: + del network_slices[sliceId] + return jsonify({"message": f"Slice {sliceId} deleted"}) + return jsonify({"message": f"Slice {sliceId} not found"}), 404 + + +def run_resilience_app(): + serving.run_simple("0.0.0.0", 8088, resilience_app) + + +def run_network_app(): + serving.run_simple("0.0.0.0", 8888, Network_app) + + +if __name__ == "__main__": + try: + # Initialize the connector + capif_connector = capif_provider_connector(config_file=capif_sdk_config_path) + print("PROVIDER ONBOARDING COMPLETED") + capif_connector.onboard_provider() + + capif_connector.api_description_path = "./nef_upf_vendor_1.json" + + api6g = capif_connector.provider_capif_ids["APF-1"] + api5g = capif_connector.provider_capif_ids["APF-2"] + + Peñuelas = capif_connector.provider_capif_ids["AEF-1"] + Distrito = capif_connector.provider_capif_ids["AEF-2"] + Valladolid = capif_connector.provider_capif_ids["AEF-3"] + + capif_connector.publish_req['publisher_apf_id'] = api6g + + capif_connector.publish_req['publisher_aefs_ids'] = [Peñuelas, Distrito] + + capif_connector.publish_services() + + capif_connector.api_description_path = "./nef_upf_vendor_2.json" + + capif_connector.publish_req['publisher_apf_id'] = api5g + + capif_connector.publish_req['publisher_aefs_ids'] = [Peñuelas, Distrito, Valladolid] + + capif_connector.publish_services() + + capif_connector.publish_req['service_api_id'] = capif_connector.provider_capif_ids['5G-Network-App-API'] + + capif_connector.update_service() + + print("APIS PUBLISHED") + + capif_connector.publish_req['service_api_id'] = capif_connector.provider_service_ids['5G-Network-App-API'] + + capif_connector.get_service() + + config_network() + + config_resilience() + + t1 = threading.Thread(target=run_resilience_app) + t2 = threading.Thread(target=run_network_app) + t1.start() + t2.start() + t1.join() + t2.join() + + print("SERVERS WORKING") + + except FileNotFoundError as e: + print(f"Error: {e}") + except json.JSONDecodeError as e: + print(f"Error reading the JSON file: {e}") + except Exception as e: + print(f"Unexpected error: {e}") diff --git a/opencapif_sdk/__init__.py b/opencapif_sdk/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..71154d7238df851f051c6e107816f6794091a5f0 --- /dev/null +++ b/opencapif_sdk/__init__.py @@ -0,0 +1,6 @@ +from opencapif_sdk.capif_invoker_connector import capif_invoker_connector +from opencapif_sdk.capif_provider_connector import capif_provider_connector +from opencapif_sdk.service_discoverer import service_discoverer +from opencapif_sdk.api_schema_translator import api_schema_translator + +__all__ = ["capif_invoker_connector", "service_discoverer", "capif_provider_connector", "api_schema_translator"] \ No newline at end of file diff --git a/opencapif_sdk/api_schema_translator.py b/opencapif_sdk/api_schema_translator.py new file mode 100644 index 0000000000000000000000000000000000000000..7e29a763c7d768da77ad3a676bba63d221139ae3 --- /dev/null +++ b/opencapif_sdk/api_schema_translator.py @@ -0,0 +1,179 @@ +import json +import logging +import os +import re +import yaml + + +log_path = 'logs/builder_logs.log' + +log_dir = os.path.dirname(log_path) + +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +logging.basicConfig( + level=logging.NOTSET, # Minimum severity level to log + # Log message format + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(log_path), # Log to a file + logging.StreamHandler() # Also display in the console + ] +) + + +class api_schema_translator: + + REQUIRED_COMPONENTS = ["openapi", "info", "servers", "paths", "components"] + + def __init__(self, api_path): + self.api_path = os.path.abspath(api_path) + self.logger = logging.getLogger(self.__class__.__name__) + self.logger.setLevel(logging.DEBUG) + self.api_info = self.__load_api_file(self.api_path) + self.__validate_api_info() + + def build(self, api_name, ip, port): + if not self.__validate_ip_port(ip, port): + self.logger.error("Invalid IP or port. Aborting build.") + return + + api_data = { + "apiName": self.api_info["info"].get("title", api_name), + "aefProfiles": self.__build_aef_profiles(ip, port), + "description": self.api_info["info"].get("description", "No description provided"), + "supportedFeatures": "fffff", + "shareableInfo": { + "isShareable": True, + "capifProvDoms": ["string"] + }, + "serviceAPICategory": "string", + "apiSuppFeats": "fffff", + "pubApiPath": { + "ccfIds": ["string"] + }, + "ccfId": "string" + } + + with open(f"{api_name}.json", "w") as outfile: + json.dump(api_data, outfile, indent=4) + self.logger.info(f"API description saved to {api_name}.json") + + def __load_api_file(self, api_file: str): + """Loads the Swagger API configuration file and converts YAML to JSON format if necessary.""" + try: + with open(api_file, 'r') as file: + if api_file.endswith('.yaml') or api_file.endswith('.yml'): + yaml_content = yaml.safe_load(file) + return json.loads(json.dumps(yaml_content)) # Convert YAML to JSON format + elif api_file.endswith('.json'): + return json.load(file) + else: + self.logger.warning( + f"Unsupported file extension for {api_file}. Only .yaml, .yml, and .json are supported.") + return {} + except FileNotFoundError: + self.logger.warning( + f"Configuration file {api_file} not found. Using defaults or environment variables.") + return {} + except (json.JSONDecodeError, yaml.YAMLError) as e: + self.logger.error( + f"Error parsing the configuration file {api_file}: {e}") + return {} + + def __validate_api_info(self): + """Validates that all required components are present in the API specification.""" + missing_components = [comp for comp in self.REQUIRED_COMPONENTS if comp not in self.api_info] + if missing_components: + self.logger.warning(f"Missing components in API specification: {', '.join(missing_components)}") + else: + self.logger.info("All required components are present in the API specification.") + + def __build_aef_profiles(self, ip, port): + """Builds the aefProfiles section based on the paths and components in the API info.""" + aef_profiles = [] + + resources = [] + for path, methods in self.api_info.get("paths", {}).items(): + for method, details in methods.items(): + resource = { + "resourceName": details.get("summary", "Unnamed Resource"), + "commType": "REQUEST_RESPONSE", + "uri": path, + "custOpName": f"http_{method}", + "operations": [method.upper()], + "description": details.get("description", "") + } + resources.append(resource) + + # Example profile creation based on paths, customize as needed + aef_profile = { + "aefId": "", # Placeholder AEF ID + "versions": [ + { + "apiVersion": "v1", + "expiry": "2100-11-30T10:32:02.004Z", + "resources": resources, + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": ["POST"], + "description": "string" + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }, + { + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + } + ] + } + ], + "protocol": "HTTP_1_1", + "dataFormat": "JSON", + "securityMethods": ["Oauth"], + "interfaceDescriptions": [ + { + "ipv4Addr": ip, + "port": port, + "securityMethods": ["Oauth"] + } + ] + } + aef_profiles.append(aef_profile) + + return aef_profiles + + def __validate_ip_port(self, ip, port): + """Validates that the IP and port have the correct format.""" + ip_pattern = re.compile(r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$") + + # Validate IP + if not ip_pattern.match(ip): + self.logger.warning(f"Invalid IP format: {ip}. Expected IPv4 format.") + return False + + # Validate each octet in the IP address + if any(int(octet) > 255 or int(octet) < 0 for octet in ip.split(".")): + self.logger.warning(f"IP address out of range: {ip}. Each octet should be between 0 and 255.") + return False + + # Validate Port + if not (1 <= port <= 65535): + self.logger.warning(f"Invalid port number: {port}. Port should be between 1 and 65535.") + return False + + self.logger.info("IP and port have correct format.") + return True diff --git a/opencapif_sdk/capif_invoker_connector.py b/opencapif_sdk/capif_invoker_connector.py new file mode 100644 index 0000000000000000000000000000000000000000..528fb75d3688c939a231f9641eca2e12af47651c --- /dev/null +++ b/opencapif_sdk/capif_invoker_connector.py @@ -0,0 +1,476 @@ +import os +import logging +import shutil +from requests.auth import HTTPBasicAuth +import urllib3 +from OpenSSL.SSL import FILETYPE_PEM +from OpenSSL.crypto import ( + dump_certificate_request, + dump_privatekey, + PKey, + TYPE_RSA, + X509Req +) +import requests +import json +import warnings +from requests.exceptions import RequestsDependencyWarning +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) +warnings.filterwarnings("ignore", category=RequestsDependencyWarning) +# noqa: E501 +# Basic configuration of the logger functionality + +log_path = 'logs/sdk_logs.log' + +log_dir = os.path.dirname(log_path) + +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +logging.basicConfig( + level=logging.NOTSET, # Minimum severity level to log + # Log message format + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(log_path), # Log to a file + logging.StreamHandler() # Also display in the console + ] +) + + +class capif_invoker_connector: + """ + Τhis class is responsbile for onboarding an Invoker (ex. a Invoker) to CAPIF + """ + def __init__(self, config_file: str): + + config_file = os.path.abspath(config_file) + # Load configuration from file if necessary + config = self.__load_config_file(config_file) + + debug_mode = os.getenv('DEBUG_MODE', config.get('debug_mode', 'False')).strip().lower() + if debug_mode == "false": + debug_mode = False + else: + debug_mode = True + + # Initialize logger for this class + self.logger = logging.getLogger(self.__class__.__name__) + if debug_mode: + self.logger.setLevel(logging.DEBUG) + else: + self.logger.setLevel(logging.WARNING) + + # Set logging level for urllib based on debug_mode + urllib_logger = logging.getLogger("urllib3") + if not debug_mode: + urllib_logger.setLevel(logging.WARNING) + else: + urllib_logger.setLevel(logging.DEBUG) + + self.logger.info("Initializing capif_invoker_connector") + + # Assign values from environment variables or JSON configuration + invoker_config = config.get('invoker', {}) + invoker_general_folder = os.path.abspath(os.getenv('invoker_folder', invoker_config.get('invoker_folder', '')).strip()) + + capif_host = os.getenv('CAPIF_HOST', config.get('capif_host', '')).strip() + register_host = os.getenv('REGISTER_HOST', config.get('register_host', '')).strip() + capif_https_port = str(os.getenv('CAPIF_HTTPS_PORT', config.get('capif_https_port', '')).strip()) + capif_register_port = str(os.getenv('CAPIF_REGISTER_PORT', config.get('capif_register_port', '')).strip()) + capif_username = os.getenv('CAPIF_USERNAME', config.get('capif_username', '')).strip() + capif_invoker_password = os.getenv('CAPIF_PASSWORD', config.get('capif_password', '')).strip() + + capif_callback_url = os.getenv('INVOKER_CAPIF_CALLBACK_URL', invoker_config.get('capif_callback_url', '')).strip() + supported_features = os.getenv('INVOKER_FOLDER', invoker_config.get('supported_features', '')).strip() + check_authentication_data = invoker_config.get('check_authentication_data', {}) + self.check_authentication = { + "ip": os.getenv('INVOKER_CHECK_AUTHENTICATION_DATA_IP', check_authentication_data.get('ip', '')).strip(), + "port": os.getenv('INVOKER_CHECK_AUTHENTICATION_DATA_PORT', check_authentication_data.get('port', '')).strip() + } + + # Extract CSR configuration from the JSON + csr_config = invoker_config.get('cert_generation', {}) + csr_common_name = os.getenv('INVOKER_CSR_COMMON_NAME', csr_config.get('csr_common_name', '')).strip() + csr_organizational_unit = os.getenv('INVOKER_CSR_ORGANIZATIONAL_UNIT', csr_config.get('csr_organizational_unit', '')).strip() + csr_organization = os.getenv('INVOKER_CSR_ORGANIZATION', csr_config.get('csr_organization', '')).strip() + csr_locality = os.getenv('INVOKER_CSR_LOCALITY', csr_config.get('csr_locality', '')).strip() + csr_state_or_province_name = os.getenv('INVOKER_CSR_STATE_OR_PROVINCE_NAME', csr_config.get('csr_state_or_province_name', '')).strip() + csr_country_name = os.getenv('INVOKER_CSR_COUNTRY_NAME', csr_config.get('csr_country_name', '')).strip() + csr_email_address = os.getenv('INVOKER_CSR_EMAIL_ADDRESS', csr_config.get('csr_email_address', '')).strip() + + # Define the invoker folder path and create it if it doesn't exist + self.invoker_folder = os.path.join(invoker_general_folder, capif_username) + os.makedirs(self.invoker_folder, exist_ok=True) + self.supported_features = supported_features + + # Configure URLs for CAPIF HTTPS and register services + if len(capif_https_port) == 0 or int(capif_https_port) == 443: + self.capif_https_url = "https://" + capif_host.strip() + "/" + else: + self.capif_https_url = "https://" + capif_host.strip() + ":" + capif_https_port.strip() + "/" + + if len(capif_register_port) == 0: + self.capif_register_url = "https://" + register_host.strip() + ":8084/" + else: + self.capif_register_url = "https://" + register_host.strip() + ":" + capif_register_port.strip() + "/" + + # Ensure the callback URL ends with a slash + self.capif_callback_url = self.__add_trailing_slash_to_url_if_missing(capif_callback_url.strip()) + + # Assign final attributes for CAPIF connection and CSR details + self.capif_username = capif_username + self.capif_invoker_password = capif_invoker_password + + self.csr_common_name = "invoker_" + csr_common_name + self.csr_organizational_unit = csr_organizational_unit + self.csr_organization = csr_organization + self.csr_locality = csr_locality + self.csr_state_or_province_name = csr_state_or_province_name + self.csr_country_name = csr_country_name + self.csr_email_address = csr_email_address + self.invoker_capif_details_filename = "capif_api_security_context_details-" + self.capif_username + ".json" + + path = os.path.join( + self.invoker_folder, + self.invoker_capif_details_filename + ) + if os.path.exists(path): + self.invoker_capif_details = self.__load_invoker_api_details() + + self.logger.info("capif_invoker_connector initialized with the JSON parameters") + + def __load_config_file(self, config_file: str): + """Loads the configuration file.""" + try: + with open(config_file, 'r') as file: + return json.load(file) + except FileNotFoundError: + self.logger.warning( + f"Configuration file {config_file} not found. Using defaults or environment variables.") + return {} + + def __add_trailing_slash_to_url_if_missing(self, url): + if url[len(url) - 1] != "/": + url = url + "/" + return url + + def onboard_invoker(self) -> None: + self.logger.info("Registering and onboarding Invoker") + try: + public_key = self.__create_private_and_public_keys() + capif_postauth_info = self.__save_ca_root_and_get_auth() + capif_onboarding_url = capif_postauth_info["ccf_onboarding_url"] + capif_discover_url = capif_postauth_info["ccf_discover_url"] + capif_access_token = capif_postauth_info["access_token"] + api_invoker_id = self.__onboard_invoker_and_create_certificate( + public_key, capif_onboarding_url, capif_access_token + ) + self.__write_to_file(api_invoker_id, capif_discover_url) + self.logger.info("Invoker registered and onboarded successfully") + except Exception as e: + self.logger.error( + f"Error during Invoker registration and onboarding: {e}") + raise + + def __load_invoker_api_details(self): + self.logger.info("Loading Invoker API details") + path = os.path.join( + self.invoker_folder, + self.invoker_capif_details_filename + ) + with open( + path, "r" + ) as openfile: + return json.load(openfile) + + def __offboard_Invoker(self) -> None: + self.logger.info("Offboarding Invoker") + try: + invoker_capif_details = self.__load_invoker_api_details() + url = ( + self.capif_https_url + + "api-invoker-management/v1/onboardedInvokers/" + + invoker_capif_details["api_invoker_id"] + ) + + signed_key_crt_path = os.path.join( + self.invoker_folder, + invoker_capif_details["user_name"] + ".crt" + ) + + private_key_path = os.path.join( + self.invoker_folder, + "private.key" + ) + + path = os.path.join( + self.invoker_folder, + "ca.crt" + ) + response = requests.request( + "DELETE", + url, + cert=(signed_key_crt_path, private_key_path), + verify=path, + ) + response.raise_for_status() + self.logger.info("Invoker offboarded successfully") + except Exception as e: + self.logger.error( + f"Error during Invoker offboarding: {e} - Response: {response.text}") + raise + + def offboard_invoker(self) -> None: + self.logger.info("Offboarding and deregistering Invoker") + try: + self.__offboard_Invoker() + self.__remove_files() + self.logger.info( + "Invoker offboarded and deregistered successfully") + except Exception as e: + self.logger.error( + f"Error during Invoker offboarding and deregistering: {e}") + raise + + def __create_private_and_public_keys(self) -> str: + self.logger.info( + "Creating private and public keys for the Invoker cert") + try: + private_key_path = os.path.join(self.invoker_folder, "private.key") + + csr_file_path = os.path.join(self.invoker_folder, "cert_req.csr") + + key = PKey() + key.generate_key(TYPE_RSA, 2048) + + req = X509Req() + req.get_subject().CN = self.csr_common_name + req.get_subject().O = self.csr_organization + req.get_subject().OU = self.csr_organizational_unit + req.get_subject().L = self.csr_locality + req.get_subject().ST = self.csr_state_or_province_name + req.get_subject().C = self.csr_country_name + req.get_subject().emailAddress = self.csr_email_address + req.set_pubkey(key) + req.sign(key, "sha256") + + with open(csr_file_path, "wb+") as f: + f.write(dump_certificate_request(FILETYPE_PEM, req)) + public_key = dump_certificate_request(FILETYPE_PEM, req) + with open(private_key_path, "wb+") as f: + f.write(dump_privatekey(FILETYPE_PEM, key)) + + self.logger.info("Keys created successfully") + return public_key + except Exception as e: + self.logger.error(f"Error during key creation: {e}") + raise + + def __remove_files(self): + self.logger.info("Removing files generated") + try: + folder_path = self.invoker_folder + + if os.path.exists(folder_path): + # Removes all the content within the folder + for root, dirs, files in os.walk(folder_path): + for file in files: + os.remove(os.path.join(root, file)) + for dir in dirs: + shutil.rmtree(os.path.join(root, dir)) + os.rmdir(folder_path) + self.logger.info( + f"All contents in {folder_path} removed successfully") + else: + self.logger.warning(f"Folder {folder_path} does not exist.") + except Exception as e: + self.logger.error(f"Error during removing folder contents: {e}") + raise + + def __save_ca_root_and_get_auth(self): + self.logger.info( + "Saving CAPIF CA root file and getting auth token with user and password given by the CAPIF administrator") + try: + url = self.capif_register_url + "getauth" + + response = requests.request( + "GET", + url, + headers={"Content-Type": "application/json"}, + auth=HTTPBasicAuth(self.capif_username, + self.capif_invoker_password), + verify=False, + ) + + response.raise_for_status() + response_payload = json.loads(response.text) + ca_root_file_path = os.path.join(self.invoker_folder, "ca.crt") + ca_root_file = open(ca_root_file_path, "wb+") + ca_root_file.write(bytes(response_payload["ca_root"], "utf-8")) + self.logger.info( + "CAPIF CA root file saved and auth token obtained successfully") + return response_payload + except Exception as e: + self.logger.error( + f"Error during saving CAPIF CA root file and getting auth token: {e} - Response: {response.text}") + raise + + def __onboard_invoker_and_create_certificate( + self, public_key, capif_onboarding_url, capif_access_token + ): + self.logger.info( + "Onboarding Invoker to CAPIF and creating signed certificate by giving our public key to CAPIF") + try: + url = self.capif_https_url + capif_onboarding_url + payload_dict = { + "notificationDestination": self.capif_callback_url, + "supportedFeatures": self.supported_features, + "apiInvokerInformation": self.csr_common_name, + "websockNotifConfig": { + "requestWebsocketUri": True, + "websocketUri": "websocketUri", + }, + "onboardingInformation": {"apiInvokerPublicKey": str(public_key, "utf-8")}, + "requestTestNotification": True, + } + payload = json.dumps(payload_dict) + headers = { + "Authorization": "Bearer {}".format(capif_access_token), + "Content-Type": "application/json", + } + pathca = os.path.join(self.invoker_folder, "ca.crt") + response = requests.request( + "POST", + url, + headers=headers, + data=payload, + verify=pathca, + ) + response.raise_for_status() + response_payload = json.loads(response.text) + name = self.capif_username+".crt" + pathcsr = os.path.join(self.invoker_folder, name) + certification_file = open( + pathcsr, "wb" + ) + certification_file.write( + bytes( + response_payload["onboardingInformation"]["apiInvokerCertificate"], + "utf-8", + ) + ) + certification_file.close() + self.logger.info( + "Invoker onboarded and signed certificate created successfully") + return response_payload["apiInvokerId"] + except Exception as e: + self.logger.error( + f"Error during onboarding Invoker to CAPIF: {e} - Response: {response.text}") + raise + + def __write_to_file(self, api_invoker_id, discover_services_url): + self.logger.info( + "Writing API invoker ID and service discovery URL to file") + path = os.path.join(self.invoker_folder, + self.invoker_capif_details_filename) + try: + with open( + path, "w" + ) as outfile: + json.dump( + { + "user_name": self.capif_username, + "api_invoker_id": api_invoker_id, + "discover_services_url": discover_services_url, + }, + outfile, + ) + self.logger.info( + "API invoker ID and service discovery URL written to file successfully") + except Exception as e: + self.logger.error(f"Error during writing to file: {e}") + raise + + def update_invoker(self): + self.logger.info("Updating Invoker") + try: + + capif_postauth_info = self.__save_ca_root_and_get_auth() + capif_onboarding_url = capif_postauth_info["ccf_onboarding_url"] + capif_access_token = capif_postauth_info["access_token"] + path = self.invoker_folder + "/cert_req.csr" + with open(path, "rb") as file: + public_key = file.read() + + self.__update_invoker_to_capif_and_create_the_signed_certificate( + public_key, capif_onboarding_url, capif_access_token + ) + + self.logger.info("Invoker updated successfully") + except Exception as e: + self.logger.error(f"Error during Invoker updating Invoker: {e}") + raise + + def __update_invoker_to_capif_and_create_the_signed_certificate( + self, public_key, capif_onboarding_url, capif_access_token + ): + self.logger.info( + "Updating Invoker to CAPIF and creating signed certificate by giving our public key to CAPIF") + try: + path = self.invoker_folder + "/" + self.invoker_capif_details_filename + + with open(path, "r") as file: + invoker_details = file.read() + + invoker_details = json.loads(invoker_details) + + invokerid = invoker_details["api_invoker_id"] + url = self.capif_https_url + capif_onboarding_url + "/" + invokerid + payload_dict = { + "notificationDestination": self.capif_callback_url, + "supportedFeatures": self.supported_features, + "apiInvokerInformation": self.csr_common_name, + "websockNotifConfig": { + "requestWebsocketUri": True, + "websocketUri": "websocketUri", + }, + "onboardingInformation": {"apiInvokerPublicKey": str(public_key, "utf-8")}, + "requestTestNotification": True, + } + payload = json.dumps(payload_dict) + headers = { + "Authorization": "Bearer {}".format(capif_access_token), + "Content-Type": "application/json", + } + signed_key_crt_path = os.path.join( + self.invoker_folder, + self.capif_username + ".crt" + ) + + private_key_path = os.path.join( + self.invoker_folder, + "private.key" + ) + pathca = os.path.join(self.invoker_folder, "ca.crt") + response = requests.request( + "PUT", + url, + headers=headers, + data=payload, + cert=(signed_key_crt_path, private_key_path), + verify=pathca, + ) + + response.raise_for_status() + + self.logger.info( + "Invoker updated and signed certificate updated successfully") + + except Exception as e: + self.logger.error( + f"Error during updating Invoker to CAPIF: {e} - Response: {response.text}") + raise + + + diff --git a/opencapif_sdk/capif_provider_connector.py b/opencapif_sdk/capif_provider_connector.py new file mode 100644 index 0000000000000000000000000000000000000000..c96578e69977f802b7b5e626e4eda7ec34a54500 --- /dev/null +++ b/opencapif_sdk/capif_provider_connector.py @@ -0,0 +1,1332 @@ +from requests.exceptions import RequestsDependencyWarning +import warnings +import json +import requests +from OpenSSL.crypto import ( + dump_certificate_request, + dump_privatekey, + PKey, + TYPE_RSA, + X509Req +) +from OpenSSL.SSL import FILETYPE_PEM +import os +import logging +import shutil +import subprocess +from requests.auth import HTTPBasicAuth +import urllib3 + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + +warnings.filterwarnings("ignore", category=RequestsDependencyWarning) + +# Basic logger configuration + +log_path = 'logs/sdk_logs.log' + +log_dir = os.path.dirname(log_path) + +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +logging.basicConfig( + level=logging.NOTSET, # Minimum severity level to log + # Log message format + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(log_path), # Logs to a file + logging.StreamHandler() # Also outputs to the console + ] +) + + +class capif_provider_connector: + """ + Τhis class is responsible for onboarding an exposer (eg. NEF emulator) to CAPIF + """ + + def __init__(self, config_file: str): + """ + Initializes the CAPIFProvider connector with the parameters specified in the configuration file. + """ + # Load configuration from file if necessary + config_file = os.path.abspath(config_file) + self.config_path = os.path.dirname(config_file)+"/" + config = self.__load_config_file(config_file) + debug_mode = os.getenv('DEBUG_MODE', config.get('debug_mode', 'False')).strip().lower() + if debug_mode == "false": + debug_mode = False + else: + debug_mode = True + + # Initialize logger for this class + self.logger = logging.getLogger(self.__class__.__name__) + if debug_mode: + self.logger.setLevel(logging.DEBUG) + else: + self.logger.setLevel(logging.WARNING) + + # Set logging level for urllib based on debug_mode + urllib_logger = logging.getLogger("urllib3") + if not debug_mode: + urllib_logger.setLevel(logging.WARNING) + else: + urllib_logger.setLevel(logging.DEBUG) + + try: + # Retrieve provider configuration from JSON or environment variables + provider_config = config.get('provider', {}) + provider_general_folder = os.path.abspath( + os.getenv('PROVIDER_FOLDER', provider_config.get('provider_folder', '')).strip()) + + capif_host = os.getenv('CAPIF_HOST', config.get('capif_host', '')).strip() + capif_register_host = os.getenv('REGISTER_HOST', config.get('register_host', '')).strip() + capif_https_port = str(os.getenv('CAPIF_HTTPS_PORT', config.get('capif_https_port', '')).strip()) + capif_register_port = str(os.getenv('CAPIF_REGISTER_PORT', config.get('capif_register_port', '')).strip()) + capif_provider_username = os.getenv('CAPIF_USERNAME', config.get('capif_username', '')).strip() + capif_provider_password = os.getenv('CAPIF_PASSWORD', config.get('capif_password', '')).strip() + + # Get CSR (Certificate Signing Request) details from config or environment variables + cert_generation = provider_config.get('cert_generation', {}) + csr_common_name = os.getenv('PROVIDER_CSR_COMMON_NAME', cert_generation.get('csr_common_name', '')).strip() + csr_organizational_unit = os.getenv('PROVIDER_CSR_ORGANIZATIONAL_UNIT', cert_generation.get('csr_organizational_unit', '')).strip() + csr_organization = os.getenv('PROVIDER_CSR_ORGANIZATION', cert_generation.get('csr_organization', '')).strip() + csr_locality = os.getenv('PROVIDER_CSR_LOCALITY', cert_generation.get('csr_locality', '')).strip() + csr_state_or_province_name = os.getenv('PROVIDER_CSR_STATE_OR_PROVINCE_NAME', cert_generation.get('csr_state_or_province_name', '')).strip() + csr_country_name = os.getenv('PROVIDER_CSR_COUNTRY_NAME', cert_generation.get('csr_country_name', '')).strip() + csr_email_address = os.getenv('PROVIDER_CSR_EMAIL_ADDRESS', cert_generation.get('csr_email_address', '')).strip() + + # Retrieve provider specific values (APFs, AEFs) + apfs = os.getenv('PROVIDER_APFS', provider_config.get('apfs', '')).strip() + aefs = os.getenv('PROVIDER_AEFS', provider_config.get('aefs', '')).strip() + api_description_path = os.path.abspath(os.getenv('PROVIDER_API_DESCRIPTION_PATH', provider_config.get('api_description_path', '')).strip()) + + # Check required fields and log warnings/errors + if not capif_host: + self.logger.warning("CAPIF_HOST is not provided; defaulting to an empty string") + if not capif_provider_username: + self.logger.error("CAPIF_PROVIDER_USERNAME is required but not provided") + raise ValueError("CAPIF_PROVIDER_USERNAME is required") + + # Setup the folder to store provider files (e.g., certificates) + self.provider_folder = os.path.join(provider_general_folder, capif_provider_username) + os.makedirs(self.provider_folder, exist_ok=True) + + # Set attributes for provider credentials and configuration + self.capif_host = capif_host.strip() + self.capif_provider_username = capif_provider_username + self.capif_provider_password = capif_provider_password + self.capif_register_host = capif_register_host + self.capif_register_port = capif_register_port + self.csr_common_name = csr_common_name + self.csr_organizational_unit = csr_organizational_unit + self.csr_organization = csr_organization + self.csr_locality = csr_locality + self.csr_state_or_province_name = csr_state_or_province_name + self.csr_country_name = csr_country_name + self.csr_email_address = csr_email_address + self.aefs = int(aefs) + self.apfs = int(apfs) + + # Get publish request details from config or environment variables + publish_req_config = provider_config.get('publish_req', {}) + self.publish_req = { + "service_api_id": os.getenv('PUBLISH_REQ_SERVICE_API_ID', publish_req_config.get('service_api_id', '')).strip(), + "publisher_apf_id": os.getenv('PUBLISH_REQ_PUBLISHER_APF_ID', publish_req_config.get('publisher_apf_id', '')).strip(), + "publisher_aefs_ids": os.getenv('PUBLISH_REQ_PUBLISHER_AEFS_IDS', publish_req_config.get('publisher_aefs_ids', '')) + } + + # Set the path for the API description file + self.api_description_path = api_description_path + + # Set the CAPIF HTTPS port and construct CAPIF URLs + self.capif_https_port = str(capif_https_port) + + self.provider_capif_ids = {} + + path_prov_funcs=os.path.join(self.provider_folder,"provider_capif_ids.json") + if os.path.exists(path_prov_funcs): + self.provider_capif_ids=self.__load_provider_api_details() + + path_published=os.path.join(self.provider_folder,"provider_service_ids.json") + if os.path.exists(path_published): + self.provider_service_ids=self.__load_config_file(path_published) + + # Construct the CAPIF HTTPS URL + if len(self.capif_https_port) == 0 or int(self.capif_https_port) == 443: + self.capif_https_url = f"https://{capif_host.strip()}/" + else: + self.capif_https_url = f"https://{capif_host.strip()}:{self.capif_https_port.strip()}/" + + # Construct the CAPIF register URL + if len(capif_register_port) == 0: + self.capif_register_url = f"https://{capif_register_host.strip()}:8084/" + else: + self.capif_register_url = f"https://{capif_register_host.strip()}:{capif_register_port.strip()}/" + + # Log initialization success message + self.logger.info("capif_provider_connector initialized with the capif_sdk_config.json parameters") + + except Exception as e: + # Catch and log any exceptions that occur during initialization + self.logger.error(f"Error during initialization: {e}") + raise + + def __store_certificate(self) -> None: + # Retrieves and stores the cert_server.pem from CAPIF. + self.logger.info( + "Retrieving capif_cert_server.pem, this may take a few minutes.") + + cmd = f"openssl s_client -connect {self.capif_host}:{self.capif_https_port} | openssl x509 -text > {self.provider_folder}/capif_cert_server.pem" + + try: + # Redirects standard output and error to os.devnull to hide logs + with open(os.devnull, 'w') as devnull: + subprocess.run(cmd, shell=True, check=True, + stdout=devnull, stderr=devnull) + + cert_file = os.path.join( + self.provider_folder, "capif_cert_server.pem") + if os.path.exists(cert_file) and os.path.getsize(cert_file) > 0: + self.logger.info("cert_server.pem successfully generated!") + else: + self.logger.error("Failed to generate cert_server.pem.") + raise FileNotFoundError( + f"Certificate file not found at {cert_file}") + except subprocess.CalledProcessError as e: + self.logger.error(f"Command failed: {e}") + raise + except Exception as e: + self.logger.error(f"Error occurred: {e}") + raise + + def __load_config_file(self, config_file: str): + """Carga el archivo de configuración.""" + try: + with open(config_file, 'r') as file: + return json.load(file) + except FileNotFoundError: + self.logger.warning( + f"Configuration file {config_file} not found. Using defaults or environment variables.") + return {} + + def __create_private_and_public_keys(self, api_prov_func_role) -> bytes: + """ + Creates private and public keys in the certificates folder. + :return: The contents of the public key + """ + private_key_path = os.path.join( + self.provider_folder, f"{api_prov_func_role}_private_key.key") + csr_file_path = os.path.join( + self.provider_folder, f"{api_prov_func_role}_public.csr") + + # Create key pair + key = PKey() + key.generate_key(TYPE_RSA, 2048) + + # Create CSR + req = X509Req() + subject = req.get_subject() + subject.CN = api_prov_func_role.lower() + subject.O = self.csr_organization + subject.OU = self.csr_organizational_unit + subject.L = self.csr_locality + subject.ST = self.csr_state_or_province_name + subject.C = self.csr_country_name + subject.emailAddress = self.csr_email_address + + req.set_pubkey(key) + req.sign(key, "sha256") + + # Write CSR and private key to files + with open(csr_file_path, "wb") as csr_file: + public_key = dump_certificate_request(FILETYPE_PEM, req) + csr_file.write(public_key) + + with open(private_key_path, "wb") as private_key_file: + private_key_file.write(dump_privatekey(FILETYPE_PEM, key)) + + return public_key + + def __onboard_exposer_to_capif(self, access_token, capif_onboarding_url): + self.logger.info( + "Onboarding Provider to CAPIF and waiting signed certificate by giving our public keys to CAPIF") + + url = f"{self.capif_https_url}{capif_onboarding_url}" + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json", + } + + # Create the list of roles without indexing + roles = ["AMF"] + for n in range(1, self.aefs + 1): + roles.append("AEF") + + for n in range(1, self.apfs + 1): + roles.append("APF") + + # Build the payload with the non-indexed roles + payload = { + "apiProvFuncs": [ + {"regInfo": {"apiProvPubKey": ""}, "apiProvFuncRole": role, + "apiProvFuncInfo": f"{role.lower()}"} + for role in roles + ], + "apiProvDomInfo": "This is provider", + "suppFeat": "fff", + "failReason": "string", + "regSec": access_token, + } + + # Generate the indexed roles for certificate creation + indexedroles = ["AMF"] + for n in range(1, self.aefs + 1): + indexedroles.append(f"AEF-{n}") + + for n in range(1, self.apfs + 1): + indexedroles.append(f"APF-{n}") + + # Save the public keys and generate certificates with indexed roles + for i, api_func in enumerate(payload["apiProvFuncs"]): + # Generate public keys with the indexed role, but do not update the payload with the indexed role + public_key = self.__create_private_and_public_keys(indexedroles[i]) + + # Assign the public key to the payload + api_func["regInfo"]["apiProvPubKey"] = public_key.decode("utf-8") + try: + response = requests.post( + url, + headers=headers, + data=json.dumps(payload), + verify=os.path.join(self.provider_folder, "ca.crt"), + ) + response.raise_for_status() + self.logger.info( + "Provider onboarded and signed certificate obtained successfully") + return response.json() + except requests.exceptions.RequestException as e: + self.logger.error( + f"Onboarding failed: {e} - Response: {response.text}") + raise + + def __write_to_file(self, onboarding_response, capif_registration_id, publish_url): + self.logger.info("Saving the most relevant onboarding data") + + # Generate the indexed roles for correspondence + indexedroles = ["AMF"] + for n in range(1, self.aefs + 1): + indexedroles.append(f"AEF-{n}") + + for n in range(1, self.apfs + 1): + indexedroles.append(f"APF-{n}") + + # Save the certificates with the indexed names + for i, func_profile in enumerate(onboarding_response["apiProvFuncs"]): + role = indexedroles[i].lower() + cert_path = os.path.join(self.provider_folder, f"{role}.crt") + with open(cert_path, "wb") as cert_file: + cert_file.write( + func_profile["regInfo"]["apiProvCert"].encode("utf-8")) + + # Save the provider details + provider_details_path = os.path.join( + self.provider_folder, "provider_capif_ids.json") + with open(provider_details_path, "w") as outfile: + data = { + "capif_registration_id": capif_registration_id, + "publish_url": publish_url, + **{f"{indexedroles[i]}": api_prov_func["apiProvFuncId"] + for i, api_prov_func in enumerate(onboarding_response["apiProvFuncs"])} + } + for i, api_prov_func in enumerate(onboarding_response["apiProvFuncs"]): + self.provider_capif_ids[indexedroles[i]] = api_prov_func["apiProvFuncId"] + + json.dump(data, outfile, indent=4) + + self.logger.info("Data saved") + + def __save_capif_ca_root_file_and_get_auth_token(self): + url = f"{self.capif_register_url}getauth" + self.logger.info( + "Saving CAPIF CA root file and getting auth token with user and password given by the CAPIF administrator") + + try: + response = requests.get( + url, + headers={"Content-Type": "application/json"}, + auth=HTTPBasicAuth(self.capif_provider_username, + self.capif_provider_password), + verify=False + ) + response.raise_for_status() + + self.logger.info("Authorization acquired successfully") + + response_payload = response.json() + ca_root_file_path = os.path.join(self.provider_folder, "ca.crt") + + with open(ca_root_file_path, "wb") as ca_root_file: + ca_root_file.write(response_payload["ca_root"].encode("utf-8")) + + self.logger.info( + "CAPIF CA root file saved and auth token obtained successfully") + return response_payload + + except requests.exceptions.RequestException as e: + self.logger.error( + f"Error acquiring authorization: {e} - Response: {response.text}") + raise + + def onboard_provider(self) -> None: + """ + Retrieves and stores the certificate from CAPIF, acquires authorization, and registers the provider. + """ + # Store the certificate + self.__store_certificate() + + # Retrieve CA root file and get authorization token + capif_postauth_info = self.__save_capif_ca_root_file_and_get_auth_token() + + # Extract necessary information + capif_onboarding_url = capif_postauth_info["ccf_api_onboarding_url"] + access_token = capif_postauth_info["access_token"] + ccf_publish_url = capif_postauth_info["ccf_publish_url"] + + # Onboard provider to CAPIF + onboarding_response = self.__onboard_exposer_to_capif( + access_token, capif_onboarding_url + ) + + # Save onboarding details to file + capif_registration_id = onboarding_response["apiProvDomId"] + self.__write_to_file( + onboarding_response, capif_registration_id, ccf_publish_url + ) + + def publish_services(self) -> dict: + """ + Publishes services to CAPIF and returns the published services dictionary. + + :param service_api_description_json_full_path: The full path of the service_api_description.json containing + the endpoints to be published. + :return: The published services dictionary that was saved in CAPIF. + """ + self.logger.info("Starting the service publication process") + + # Load provider details + provider_details_path = os.path.join( + self.provider_folder, "provider_capif_ids.json") + self.logger.info( + f"Loading provider details from {provider_details_path}") + + provider_details = self.__load_provider_api_details() + + publish_url = provider_details["publish_url"] + + chosenAPFsandAEFs = self.publish_req + + APF_api_prov_func_id = chosenAPFsandAEFs["publisher_apf_id"] + AEFs_list = chosenAPFsandAEFs["publisher_aefs_ids"] + + apf_number = None + for key, value in provider_details.items(): + if value == APF_api_prov_func_id and key.startswith("APF-"): + apf_inter = key.split("-")[1] + # Obtain the APF number + apf_number = apf_inter.split("_")[0] + break + + if apf_number is None: + self.logger.error( + f"No matching APF found for publisher_apf_id: {APF_api_prov_func_id}") + raise ValueError("Invalid publisher_apf_id") + service_api_description_json_full_path = self.api_description_path + # Read and modify the API description + self.logger.info( + f"Reading and modifying service API description from {service_api_description_json_full_path}") + + try: + with open(service_api_description_json_full_path, "r") as service_file: + data = json.load(service_file) + + # Verifying that the number of AEFs is equal to the aefProfiles + if len(AEFs_list) != len(data.get("aefProfiles", [])): + self.logger.error( + "The number of AEFs in publisher_aefs_ids does not match the number of profiles in aefProfiles") + raise ValueError( + "Mismatch between number of AEFs and profiles") + + # Assigning each AEF + for profile, aef_id in zip(data.get("aefProfiles", []), AEFs_list): + profile["aefId"] = aef_id + for versions in profile["versions"]: + check = True + revoke = True + for custom in versions["custOperations"]: + if custom["custOpName"] == "check-authentication": + check = False + if custom["custOpName"] == "revoke-authentication": + revoke = False + # Si ambas condiciones ya son falsas, salir del bucle + if not check and not revoke: + break + # If 'check-authentication' custom operation doesn't exist, add it + if check: + versions["custOperations"].append({ + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }) + + # If 'revoke-authentication' custom operation doesn't exist, add it + if revoke: + versions["custOperations"].append({ + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + }) + + self.logger.info( + "Service API description modified successfully") + + # Saving changes into the file + with open(service_api_description_json_full_path, "w") as service_file: + json.dump(data, service_file, indent=4) + + except FileNotFoundError: + self.logger.error( + f"Service API description file not found: {service_api_description_json_full_path}") + raise + except json.JSONDecodeError as e: + self.logger.error( + f"Error decoding JSON from file {service_api_description_json_full_path}: {e}") + raise + except ValueError as e: + self.logger.error(f"Error with the input data: {e}") + raise + + # Publish services + url = f"{self.capif_https_url}{publish_url.replace('', APF_api_prov_func_id)}" + cert = ( + os.path.join(self.provider_folder, f"apf-{apf_number}.crt"), + os.path.join(self.provider_folder, + f"APF-{apf_number}_private_key.key"), + ) + + self.logger.info(f"Publishing services to URL: {url}") + + try: + response = requests.post( + url, + headers={"Content-Type": "application/json"}, + data=json.dumps(data), + cert=cert, + verify=os.path.join(self.provider_folder, "ca.crt"), + ) + response.raise_for_status() + self.logger.info("Services published successfully") + + # Save response to file + capif_response_text = response.text + + capif_response_json = json.loads(capif_response_text) + + # Default name if apiName is missing + file_name = capif_response_json.get("apiName", "default_name") + id = capif_response_json.get("apiId", "default_id") + output_path = os.path.join( + self.provider_folder, f"capif_{file_name}_{id}_api.json") + + with open(output_path, "w") as outfile: + outfile.write(capif_response_text) + self.logger.info(f"CAPIF response saved to {output_path}") + output_path = os.path.join( + + self.provider_folder, "provider_service_ids.json") + + # Read the existing file of published APIs + provider_service_ids = {} + if os.path.exists(output_path): + with open(output_path, "r") as outfile: + provider_service_ids = json.load(outfile) + + # Add the newly published API + + provider_service_ids[file_name] = id + + self.provider_service_ids = provider_service_ids + # Write the updated file of published APIs + with open(output_path, "w") as outfile: + json.dump(provider_service_ids, outfile, indent=4) + self.logger.info( + f"API '{file_name}' with ID '{id}' added to Published Apis.") + return json.loads(capif_response_text) + + except requests.RequestException as e: + self.logger.error( + f"Request to CAPIF failed: {e} - Response: {response.text}") + raise + except Exception as e: + self.logger.error( + f"Unexpected error during service publication: {e} - Response: {response.text}") + raise + + def unpublish_service(self) -> dict: + """ + Publishes services to CAPIF and returns the published services dictionary. + + :param service_api_description_json_full_path: The full path of the service_api_description.json containing + the endpoints to be published. + :return: The published services dictionary that was saved in CAPIF. + """ + self.logger.info("Starting the service unpublication process") + provider_details_path = os.path.join( + self.provider_folder, "provider_capif_ids.json") + self.logger.info( + f"Loading provider details from {provider_details_path}") + + provider_details = self.__load_provider_api_details() + publish_url = provider_details["publish_url"] + + # Load provider details + publish = self.publish_req + api_id = "/" + publish["service_api_id"] + APF_api_prov_func_id = publish["publisher_apf_id"] + AEFs_list = publish["publisher_aefs_ids"] + apf_number = None + for key, value in provider_details.items(): + if value == APF_api_prov_func_id and key.startswith("APF-"): + apf_inter = key.split("-")[1] + # Get the number of APFs + apf_number = apf_inter.split("_")[0] + break + + if apf_number is None: + self.logger.error( + f"No matching APF found for publisher_apf_id: {APF_api_prov_func_id}") + raise ValueError("Invalid publisher_apf_id") + + self.logger.info( + f"Loading provider details from {provider_details_path}") + + url = f"{self.capif_https_url}{publish_url.replace('', APF_api_prov_func_id)}{api_id}" + + cert = ( + os.path.join(self.provider_folder, f"apf-{apf_number}.crt"), + os.path.join(self.provider_folder, + f"apf-{apf_number}_private_key.key"), + ) + + self.logger.info(f"Unpublishing service to URL: {url}") + + try: + response = requests.delete( + url, + headers={"Content-Type": "application/json"}, + cert=cert, + verify=os.path.join(self.provider_folder, "ca.crt"), + ) + + response.raise_for_status() + + directory = self.provider_folder + + # Iterar sobre todos los archivos en el directorio + for filename in os.listdir(directory): + path = os.path.join(directory, filename) + + # Check if the file starts with 'CAPIF-' + + if filename.startswith("CAPIF-") and publish["service_api_id"] in filename: + + # Exit the loop if the file is deleted + os.remove(path) + break + + output_path = os.path.join( + + self.provider_folder, "provider_service_ids.json") + + # Read the existing file of published APIs + provider_service_ids = {} + if os.path.exists(output_path): + with open(output_path, "r") as outfile: + provider_service_ids = json.load(outfile) + + # API ID you want to delete + # Replace with the specific ID + api_id_to_delete = publish["service_api_id"] + + # Search and delete the API by its ID + api_name_to_delete = None + for name, id in provider_service_ids.items(): + if id == api_id_to_delete: + api_name_to_delete = name + break + + if api_name_to_delete: + del provider_service_ids[api_name_to_delete] + self.logger.info( + f"API with ID '{api_id_to_delete}' removed from Published Apis.") + else: + self.logger.warning( + f"API with ID '{api_id_to_delete}' not found in Published Apis.") + + # Write the updated file of published APIs + with open(output_path, "w") as outfile: + + json.dump(provider_service_ids, outfile, indent=4) + self.provider_service_ids = provider_service_ids + self.logger.info("Services unpublished successfully") + + except requests.RequestException as e: + self.logger.error( + f"Request to CAPIF failed: {e} - Response: {response.text}") + raise + except Exception as e: + self.logger.error( + f"Unexpected error during service unpublication: {e} - Response: {response.text}") + raise + + def get_service(self) -> dict: + """ + Publishes services to CAPIF and returns the published services dictionary. + + :param service_api_description_json_full_path: The full path of the service_api_description.json containing + the endpoints to be published. + :return: The published services dictionary that was saved in CAPIF. + """ + self.logger.info("Starting the service unpublication process") + + provider_details_path = os.path.join( + self.provider_folder, "provider_capif_ids.json") + self.logger.info( + f"Loading provider details from {provider_details_path}") + + provider_details = self.__load_provider_api_details() + publish_url = provider_details["publish_url"] + + chosenAPFsandAEFs = self.publish_req + + APF_api_prov_func_id = chosenAPFsandAEFs["publisher_apf_id"] + + api_id = "/" + chosenAPFsandAEFs["service_api_id"] + + apf_number = None + for key, value in provider_details.items(): + if value == APF_api_prov_func_id and key.startswith("APF-"): + apf_inter = key.split("-")[1] + # Get the number of apfs + apf_number = apf_inter.split("_")[0] + break + + if apf_number is None: + self.logger.error( + f"No matching APF found for publisher_apf_id: {APF_api_prov_func_id}") + raise ValueError("Invalid publisher_apf_id") + + url = f"{self.capif_https_url}{publish_url.replace('', APF_api_prov_func_id)}{api_id}" + + cert = ( + os.path.join(self.provider_folder, f"apf-{apf_number}.crt"), + os.path.join(self.provider_folder, + f"apf-{apf_number}_private_key.key"), + ) + + self.logger.info(f"Getting service to URL: {url}") + + try: + response = requests.get( + url, + headers={"Content-Type": "application/json"}, + cert=cert, + verify=os.path.join(self.provider_folder, "ca.crt"), + ) + + response.raise_for_status() + + self.logger.info("Service received successfully") + path = os.path.join(self.provider_folder, "service_received.json") + with open(path, 'w') as f: + json_data = json.loads(response.text) + json.dump(json_data, f, indent=4) + self.logger.info(f"Service saved in {path}") + + except requests.RequestException as e: + self.logger.error( + f"Request to CAPIF failed: {e} - Response: {response.text}") + raise + except Exception as e: + self.logger.error( + f"Unexpected error during service getter: {e} - Response: {response.text}") + raise + + def get_all_services(self) -> dict: + """ + Publishes services to CAPIF and returns the published services dictionary. + + :param service_api_description_json_full_path: The full path of the service_api_description.json containing + the endpoints to be published. + :return: The published services dictionary that was saved in CAPIF. + """ + self.logger.info("Starting the service publication process") + + # Load provider details + provider_details_path = os.path.join( + self.provider_folder, "provider_capif_ids.json") + self.logger.info( + f"Loading provider details from {provider_details_path}") + + provider_details = self.__load_provider_api_details() + publish_url = provider_details["publish_url"] + + chosenAPFsandAEFs = self.publish_req + + APF_api_prov_func_id = chosenAPFsandAEFs["publisher_apf_id"] + + apf_number = None + for key, value in provider_details.items(): + if value == APF_api_prov_func_id and key.startswith("APF-"): + apf_inter = key.split("-")[1] + # Get the number of APFs + apf_number = apf_inter.split("_")[0] + break + + if apf_number is None: + self.logger.error( + f"No matching APF found for publisher_apf_id: {APF_api_prov_func_id}") + raise ValueError("Invalid publisher_apf_id") + + # Read and modify the description of the API services + + # Publish services + url = f"{self.capif_https_url}{publish_url.replace('', APF_api_prov_func_id)}" + cert = ( + os.path.join(self.provider_folder, f"apf-{apf_number}.crt"), + os.path.join(self.provider_folder, + f"apf-{apf_number}_private_key.key"), + ) + + self.logger.info(f"Getting services to URL: {url}") + + try: + response = requests.get( + url, + headers={"Content-Type": "application/json"}, + cert=cert, + verify=os.path.join(self.provider_folder, "ca.crt"), + ) + response.raise_for_status() + self.logger.info("Services received successfully") + + path = os.path.join(self.provider_folder, "service_received.json") + with open(path, 'w') as f: + json_data = json.loads(response.text) + json.dump(json_data, f, indent=4) + self.logger.info(f"Services saved in {path}") + + # Save response to file + + except requests.RequestException as e: + self.logger.error( + f"Request to CAPIF failed: {e} - Response: {response.text}") + raise + except Exception as e: + self.logger.error( + f"Unexpected error during services reception: {e} - Response: {response.text}") + raise + + def update_service(self) -> dict: + """ + Publishes services to CAPIF and returns the published services dictionary. + + :param service_api_description_json_full_path: The full path of the service_api_description.json containing + the endpoints to be published. + :return: The published services dictionary that was saved in CAPIF. + """ + self.logger.info("Starting the service publication process") + + # Load provider details + # Load provider details + provider_details_path = os.path.join( + self.provider_folder, "provider_capif_ids.json") + self.logger.info( + f"Loading provider details from {provider_details_path}") + + provider_details = self.__load_provider_api_details() + publish_url = provider_details["publish_url"] + + chosenAPFsandAEFs = self.publish_req + + APF_api_prov_func_id = chosenAPFsandAEFs["publisher_apf_id"] + AEFs_list = chosenAPFsandAEFs["publisher_aefs_ids"] + + apf_number = None + for key, value in provider_details.items(): + if value == APF_api_prov_func_id and key.startswith("APF-"): + apf_inter = key.split("-")[1] + # Get the number of APFs + apf_number = apf_inter.split("_")[0] + break + + if apf_number is None: + self.logger.error( + f"No matching APF found for publisher_apf_id: {APF_api_prov_func_id}") + raise ValueError("Invalid publisher_apf_id") + + service_api_description_json_full_path = self.api_description_path + # Read and modify the description of the API services + self.logger.info( + f"Reading and modifying service API description from {service_api_description_json_full_path}") + + try: + with open(service_api_description_json_full_path, "r") as service_file: + data = json.load(service_file) + + # verify the aefs number corresponds to the aefProfiles + if len(AEFs_list) != len(data.get("aefProfiles", [])): + self.logger.error( + "The number of AEFs in publisher_aefs_ids does not match the number of profiles in aefProfiles") + raise ValueError( + "Mismatch between number of AEFs and profiles") + + # Asing the chosen AEFs + for profile, aef_id in zip(data.get("aefProfiles", []), AEFs_list): + profile["aefId"] = aef_id + for versions in profile["versions"]: + for custom in versions["custOperations"]: + check = True + revoke = True + if custom["custOpName"] == "check-authentication": + check = False + if custom["custOpName"] == "revoke-authentication ": + revoke = False + # If 'check-authentication' custom operation doesn't exist, add it + if check: + versions["custOperations"].append({ + "commType": "REQUEST_RESPONSE", + "custOpName": "check-authentication", + "operations": [ + "POST" + ], + "description": "Check authentication request." + }) + + # If 'revoke-authentication' custom operation doesn't exist, add it + if revoke: + versions["custOperations"].append({ + "commType": "REQUEST_RESPONSE", + "custOpName": "revoke-authentication", + "operations": [ + "POST" + ], + "description": "Revoke authorization for service APIs." + }) + self.logger.info( + "Service API description modified successfully") + + # Save changes + with open(service_api_description_json_full_path, "w") as service_file: + json.dump(data, service_file, indent=4) + + except FileNotFoundError: + self.logger.error( + f"Service API description file not found: {service_api_description_json_full_path}") + raise + except json.JSONDecodeError as e: + self.logger.error( + f"Error decoding JSON from file {service_api_description_json_full_path}: {e}") + raise + except ValueError as e: + self.logger.error(f"Error with the input data: {e}") + raise + api_id = "/" + chosenAPFsandAEFs["service_api_id"] + # Publish services + url = f"{self.capif_https_url}{publish_url.replace('', APF_api_prov_func_id)}{api_id}" + cert = ( + os.path.join(self.provider_folder, f"apf-{apf_number}.crt"), + os.path.join(self.provider_folder, + f"apf-{apf_number}_private_key.key"), + ) + + self.logger.info(f"Publishing services to URL: {url}") + + try: + response = requests.put( + url, + headers={"Content-Type": "application/json"}, + data=json.dumps(data), + cert=cert, + verify=os.path.join(self.provider_folder, "ca.crt"), + ) + response.raise_for_status() + self.logger.info("Services updated successfully") + + # Save response to file + capif_response_text = response.text + + capif_response_json = json.loads(capif_response_text) + + # Default name if apiName is missing + file_name = capif_response_json.get("apiName", "default_name") + id = capif_response_json.get("apiId", "default_id") + directory = self.provider_folder + + # Iterate over all files in the directory + for filename in os.listdir(directory): + path = os.path.join(directory, filename) + + # Check if the file starts with 'CAPIF-' + + if filename.startswith("CAPIF-") and id in filename: + + # Exit the loop if the file is deleted + os.remove(path) + break + + output_path = os.path.join( + self.provider_folder, f"capif_{file_name}_{id}_api.json") + + with open(output_path, "w") as outfile: + outfile.write(capif_response_text) + self.logger.info(f"CAPIF response saved to {output_path}") + output_path = os.path.join( + + self.provider_folder, "provider_service_ids.json") + + # Read the existing file of published APIs + provider_service_ids = {} + if os.path.exists(output_path): + with open(output_path, "r") as outfile: + provider_service_ids = json.load(outfile) + + keys_to_remove = [key for key, + value in provider_service_ids.items() if value == id] + for key in keys_to_remove: + del provider_service_ids[key] + # Add the new id of the published API + + provider_service_ids[file_name] = id + self.provider_service_ids = provider_service_ids + + # Update the file with the published APIs + with open(output_path, "w") as outfile: + json.dump(provider_service_ids, outfile, indent=4) + self.logger.info( + f"API '{file_name}' with ID '{id}' added to Published Apis.") + return json.loads(capif_response_text) + except requests.RequestException as e: + self.logger.error( + f"Request to CAPIF failed: {e} - Response: {response.text}") + raise + except Exception as e: + self.logger.error( + f"Unexpected error during service publication: {e} - Response: {response.text}") + raise + + def offboard_provider(self) -> None: + """ + Offboards and deregisters the NEF (Network Exposure Function). + """ + try: + self.offboard_nef() + self.__remove_files() + self.logger.info( + "Provider offboarded and deregistered successfully.") + except Exception as e: + self.logger.error( + f"Failed to offboard and deregister Provider: {e}") + raise + + def offboard_nef(self) -> None: + """ + Offboards the NEF (Network Exposure Function) from CAPIF. + """ + try: + self.logger.info("Offboarding the provider") + + # Load CAPIF API details + capif_api_details = self.__load_provider_api_details() + url = f"{self.capif_https_url}api-provider-management/v1/registrations/{capif_api_details['capif_registration_id']}" + + # Define certificate paths + cert_paths = ( + os.path.join(self.provider_folder, "amf.crt"), + os.path.join(self.provider_folder, "AMF_private_key.key") + ) + + # Send DELETE request to offboard the provider + response = requests.delete( + url, + cert=cert_paths, + verify=os.path.join(self.provider_folder, "ca.crt") + ) + + response.raise_for_status() + self.logger.info("Offboarding performed successfully") + + except requests.exceptions.RequestException as e: + self.logger.error( + f"Error offboarding Provider: {e} - Response: {response.text}") + raise + except Exception as e: + self.logger.error( + f"Unexpected error: {e} - Response: {response.text}") + raise + + def __remove_files(self): + self.logger.info("Removing files generated") + try: + folder_path = self.provider_folder + + if os.path.exists(folder_path): + # Deletes all content within the folder, including files and subfolders + for root, dirs, files in os.walk(folder_path): + for file in files: + os.remove(os.path.join(root, file)) + for dir in dirs: + shutil.rmtree(os.path.join(root, dir)) + os.rmdir(folder_path) + self.logger.info( + f"All contents in {folder_path} removed successfully.") + else: + self.logger.warning(f"Folder {folder_path} does not exist.") + except Exception as e: + self.logger.error(f"Error during removing folder contents: {e}") + raise + + def __load_provider_api_details(self) -> dict: + """ + Loads NEF API details from the CAPIF provider details JSON file. + + :return: A dictionary containing NEF API details. + :raises FileNotFoundError: If the CAPIF provider details file is not found. + :raises json.JSONDecodeError: If there is an error decoding the JSON file. + """ + file_path = os.path.join(self.provider_folder, + "provider_capif_ids.json") + + try: + with open(file_path, "r") as file: + return json.load(file) + except FileNotFoundError: + self.logger.error(f"File not found: {file_path}") + raise + except json.JSONDecodeError as e: + self.logger.error( + f"Error decoding JSON from file {file_path}: {e}") + raise + except Exception as e: + self.logger.error( + f"Unexpected error while loading NEF API details: {e}") + raise + + def update_provider(self): + self.certs_modifications() + + capif_postauth_info = self.__save_capif_ca_root_file_and_get_auth_token() + capif_onboarding_url = capif_postauth_info["ccf_api_onboarding_url"] + access_token = capif_postauth_info["access_token"] + ccf_publish_url = capif_postauth_info["ccf_publish_url"] + + onboarding_response = self.update_onboard( + capif_onboarding_url, access_token) + + capif_registration_id = onboarding_response["apiProvDomId"] + self.__write_to_file( + onboarding_response, capif_registration_id, ccf_publish_url + ) + + def certs_modifications(self): + api_details = self.__load_provider_api_details() + + apf_count = 0 + aef_count = 0 + + # Iterate over the dictionary keys (the fields of the JSON) + + for key in api_details.keys(): + if key.startswith("APF"): + apf_count += 1 + elif key.startswith("AEF"): + aef_count += 1 + + # Log the results (or return them, depending on what you need) + self.logger.info(f"Total APFs: {apf_count}, Total AEFs: {aef_count}") + APFscertstoremove = 0 + APFscertstoadd = 0 + AEFscertstoremove = 0 + AEFscertstoadd = 0 + # Calculate the difference of APFs + if apf_count != self.apfs: + diff = apf_count - self.apfs + if diff < 0: + self.APFscertstoadd = abs(diff) + else: + APFscertstoremove = diff + else: + APFscertstoremove = 0 + APFscertstoadd = 0 + + # Calculate the difference of AEFs + if aef_count != self.aefs: + diff = aef_count - self.aefs + if diff < 0: + self.AEFscertstoadd = abs(diff) + else: + AEFscertstoremove = diff + else: + AEFscertstoremove = 0 + AEFscertstoadd = 0 + + # Remove APF files in descending order if there are more APFs than there should be + if APFscertstoremove: + while apf_count > self.apfs: + # List files starting with "APF-" or "apf-" in the directory + file_path = os.path.join( + self.provider_folder, f"APF-{apf_count}_private_key.key") + os.remove(file_path) + self.logger.info( + f"Removed APF file: APF-{apf_count}_private_key.key") + + file_path = os.path.join( + self.provider_folder, f"APF-{apf_count}_public.csr") + os.remove(file_path) + self.logger.info( + f"Removed APF file: APF-{apf_count}_public.csr") + + file_path = os.path.join( + self.provider_folder, f"apf-{apf_count}.crt") + os.remove(file_path) + self.logger.info(f"Removed APF file: apf-{apf_count}.crt") + # Decrease the APF count + apf_count -= 1 + + # Remove AEF files in descending order if there are more AEFs than there should be + if AEFscertstoremove: + while aef_count > self.aefs: + # List files starting with "AEF-" or "aef-" in the directory + file_path = os.path.join( + self.provider_folder, f"AEF-{aef_count}_private_key.key") + os.remove(file_path) + self.logger.info( + f"Removed AEF file: AEF-{aef_count}_private_key.key") + + file_path = os.path.join( + self.provider_folder, f"AEF-{aef_count}_public.csr") + os.remove(file_path) + self.logger.info( + f"Removed AEF file: AEF-{aef_count}_public.csr") + + file_path = os.path.join( + self.provider_folder, f"aef-{aef_count}.crt") + os.remove(file_path) + self.logger.info(f"Removed AEF file: aef-{aef_count}.crt") + # Decrease the APF count + aef_count -= 1 + + def update_onboard(self, capif_onboarding_url, access_token): + self.logger.info( + "Onboarding Provider to CAPIF and waiting signed certificate by giving our public keys to CAPIF") + api_details = self.__load_provider_api_details() + capif_id = "/" + api_details["capif_registration_id"] + + url = f"{self.capif_https_url}{capif_onboarding_url}{capif_id}" + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json", + } + + # Create the list of unindexed roles + roles = ["AMF"] + for n in range(1, self.aefs + 1): + roles.append("AEF") + + for n in range(1, self.apfs + 1): + roles.append("APF") + + # Build the payload with unindexed roles + payload = { + "apiProvFuncs": [ + {"regInfo": {"apiProvPubKey": ""}, "apiProvFuncRole": role, + "apiProvFuncInfo": f"{role.lower()}"} + for role in roles + ], + "apiProvDomInfo": "This is provider", + "suppFeat": "fff", + "failReason": "string", + "regSec": access_token, + } + + # Generate the indexed roles for certificate creation + indexed_roles = ["AMF"] + [f"AEF-{n}" for n in range(1, self.aefs + 1)] + [ + f"APF-{n}" for n in range(1, self.apfs + 1)] + + # Iterate over each API provider function + for i, api_func in enumerate(payload["apiProvFuncs"]): + # Folder path for providers + folder_path = self.provider_folder + + # Check if the folder exists + if os.path.exists(folder_path): + found_key = False # Variable to control if a public key has already been found + + # Iterate over the files in the folder + for root, dirs, files in os.walk(folder_path): + for file_name in files: + if file_name.endswith(".csr"): + # Check if the file starts with the expected role + role_prefix = indexed_roles[i] + if any(file_name.startswith(prefix) and role_prefix == prefix for prefix in [f"APF-{i+1}", f"AEF-{i+1}", "AMF"]): + file_path = os.path.join(root, file_name) + + # Read the public key from the file + with open(file_path, "r") as csr_file: + api_func["regInfo"]["apiProvPubKey"] = csr_file.read( + ) + + found_key = True + break + + if found_key: + break + + # If a file with the public key is not found, generate a new key + if not found_key: + + public_key = self.__create_private_and_public_keys( + indexed_roles[i]) + api_func["regInfo"]["apiProvPubKey"] = public_key.decode( + "utf-8") + + cert = ( + os.path.join(self.provider_folder, "amf.crt"), + os.path.join(self.provider_folder, "AMF_private_key.key"), + ) + + try: + response = requests.put( + url, + headers=headers, + data=json.dumps(payload), + cert=cert, + verify=os.path.join(self.provider_folder, "ca.crt"), + ) + + response.raise_for_status() + self.logger.info( + "Provider onboarded and signed certificate obtained successfully") + return response.json() + except requests.exceptions.RequestException as e: + self.logger.error( + f"Onboarding failed: {e} - Response: {response.text}") + raise diff --git a/opencapif_sdk/service_discoverer.py b/opencapif_sdk/service_discoverer.py new file mode 100644 index 0000000000000000000000000000000000000000..9f079a17dd43ce117836c54b1581289566b7722e --- /dev/null +++ b/opencapif_sdk/service_discoverer.py @@ -0,0 +1,545 @@ +from requests.exceptions import RequestsDependencyWarning +import warnings +import json +import requests +import os +import logging +import urllib3 + +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + +warnings.filterwarnings("ignore", category=RequestsDependencyWarning) + +# Basic logger configuration + +log_path = 'logs/sdk_logs.log' + +log_dir = os.path.dirname(log_path) + +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +logging.basicConfig( + level=logging.NOTSET, # Minimum severity level to log + # Log message format + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(log_path), # Logs to a file + logging.StreamHandler() # Also outputs to the console + ] +) + + + +class service_discoverer: + class ServiceDiscovererException(Exception): + pass + + def __init__( + self, + config_file + ): + # Load configuration from file if necessary + config_file = os.path.abspath(config_file) + config = self.__load_config_file(config_file) + debug_mode = os.getenv('DEBUG_MODE', config.get('debug_mode', 'False')).strip().lower() + if debug_mode == "false": + debug_mode = False + else: + debug_mode = True + + # Initialize logger for this class + self.logger = logging.getLogger(self.__class__.__name__) + if debug_mode: + self.logger.setLevel(logging.DEBUG) + else: + self.logger.setLevel(logging.WARNING) + + # Set logging level for urllib based on debug_mode + urllib_logger = logging.getLogger("urllib3") + if not debug_mode: + urllib_logger.setLevel(logging.WARNING) + else: + urllib_logger.setLevel(logging.DEBUG) + + # Configuration path to store files + self.config_path = os.path.dirname(os.path.abspath(config_file)) + "/" + + # Retrieve host and port information from environment variables or config + capif_host = os.getenv('CAPIF_HOST', config.get('capif_host', '')).strip() + capif_https_port = str(os.getenv('CAPIF_HTTPS_PORT', config.get('capif_https_port', '')).strip()) + + # Get the folder for storing invoker certificates from environment or config + invoker_config = config.get('invoker', {}) + invoker_general_folder = os.path.abspath( + os.getenv('invoker_folder', invoker_config.get('invoker_folder', '')).strip() + ) + supported_features = os.getenv('INVOKER_FOLDER', invoker_config.get('supported_features', '')).strip() + check_authentication_data = invoker_config.get('check_authentication_data', {}) + self.check_authentication_data = { + "ip": os.getenv('INVOKER_CHECK_AUTHENTICATION_DATA_IP', check_authentication_data.get('ip', '')).strip(), + "port": os.getenv('INVOKER_CHECK_AUTHENTICATION_DATA_PORT', check_authentication_data.get('port', '')).strip() + } + # Retrieve CAPIF invoker username + capif_invoker_username = os.getenv('CAPIF_USERNAME', config.get('capif_username', '')).strip() + + # Extract discover filter configuration from JSON or environment variables + discover_filter_config = invoker_config.get('discover_filter', {}) + self.discover_filter = { + "api-name": os.getenv('DISCOVER_FILTER_API_NAME', discover_filter_config.get('api-name', '')).strip(), + "api-version": os.getenv('DISCOVER_FILTER_API_VERSION', discover_filter_config.get('api-version', '')).strip(), + "comm-type": os.getenv('DISCOVER_FILTER_COMM_TYPE', discover_filter_config.get('comm-type', '')).strip(), + "protocol": os.getenv('DISCOVER_FILTER_PROTOCOL', discover_filter_config.get('protocol', '')).strip(), + "aef-id": os.getenv('DISCOVER_FILTER_AEF_ID', discover_filter_config.get('aef-id', '')).strip(), + "data-format": os.getenv('DISCOVER_FILTER_DATA_FORMAT', discover_filter_config.get('data-format', '')).strip(), + "api-cat": os.getenv('DISCOVER_FILTER_API_CAT', discover_filter_config.get('api-cat', '')).strip(), + "preferred-aef-loc": os.getenv('DISCOVER_FILTER_PREFERRED_AEF_LOC', discover_filter_config.get('preferred-aef-loc', '')).strip(), + "req-api-prov-name": os.getenv('DISCOVER_FILTER_REQ_API_PROV_NAME', discover_filter_config.get('req-api-prov-name', '')).strip(), + "supported-features": os.getenv('DISCOVER_FILTER_SUPPORTED_FEATURES', discover_filter_config.get('supported-features', '')).strip(), + "api-supported-features": os.getenv('DISCOVER_FILTER_API_SUPPORTED_FEATURES', discover_filter_config.get('api-supported-features', '')).strip(), + "ue-ip-addr": os.getenv('DISCOVER_FILTER_UE_IP_ADDR', discover_filter_config.get('ue-ip-addr', '')).strip(), + "service-kpis": os.getenv('DISCOVER_FILTER_SERVICE_KPIS', discover_filter_config.get('service-kpis', '')).strip() + } + + # Store important attributes for CAPIF invocation + self.capif_invoker_username = capif_invoker_username + self.capif_host = capif_host + self.capif_https_port = capif_https_port + self.token = "" + self.supported_features = supported_features + + # Create invoker folder dynamically based on username and folder path + self.invoker_folder = os.path.join(invoker_general_folder, capif_invoker_username) + os.makedirs(self.invoker_folder, exist_ok=True) + + # Load CAPIF API details + + self.invoker_capif_details = self.__load_provider_api_details() + try: + self.token = self.invoker_capif_details["access_token"] + + except : + pass + + # Define paths for certificates, private keys, and CA root + self.signed_key_crt_path = os.path.join(self.invoker_folder, self.invoker_capif_details["user_name"] + ".crt") + self.private_key_path = os.path.join(self.invoker_folder, "private.key") + self.ca_root_path = os.path.join(self.invoker_folder, "ca.crt") + + # Log initialization success message + self.logger.info("ServiceDiscoverer initialized correctly") + + def get_api_provider_id(self): + return self.invoker_capif_details["api_provider_id"] + + def __load_config_file(self, config_file: str): + """Carga el archivo de configuración.""" + try: + with open(config_file, 'r') as file: + return json.load(file) + except FileNotFoundError: + self.logger.warning( + f"Configuration file {config_file} not found. Using defaults or environment variables.") + return {} + + def __load_provider_api_details(self): + try: + path = os.path.join( + self.invoker_folder, "capif_api_security_context_details-"+self.capif_invoker_username+".json") + with open( + path, + "r", + ) as openfile: + details = json.load(openfile) + self.logger.info("Api provider details correctly loaded") + return details + except Exception as e: + self.logger.error( + "Error while loading Api invoker details: %s", str(e)) + raise + + def _add_trailing_slash_to_url_if_missing(self, url): + if not url.endswith("/"): + url += "/" + return url + + def get_security_context(self): + self.logger.info("Getting security context for all API's filtered") + + self.logger.info("Trying to update security context") + self.__update_security_service() + self.__cache_security_context() + + def get_access_token(self): + """ + :param api_name: El nombre del API devuelto por descubrir servicios + :param api_id: El id del API devuelto por descubrir servicios + :param aef_id: El aef_id relevante devuelto por descubrir servicios + :return: El token de acceso (jwt) + """ + token_dic = self.__get_security_token() + self.logger.info("Access token successfully obtained") + return token_dic["access_token"] + + def __cache_security_context(self): + try: + path = os.path.join( + self.invoker_folder, "capif_api_security_context_details-"+self.capif_invoker_username+".json") + with open( + path, "w" + ) as outfile: + json.dump(self.invoker_capif_details, outfile) + self.logger.info("Security context saved correctly") + except Exception as e: + self.logger.error( + "Error when saving the security context: %s", str(e)) + raise + + def __update_security_service(self): + """ + Actualiza el servicio de seguridad. + + :param api_id: El id del API devuelto por descubrir servicios. + :param aef_id: El aef_id devuelto por descubrir servicios. + :return: None. + """ + url = f"https://{self.capif_host}:{self.capif_https_port}/capif-security/v1/trustedInvokers/{self.invoker_capif_details['api_invoker_id']}/update" + payload = { + "securityInfo": [], + "notificationDestination": "https://mynotificationdest.com", + "requestTestNotification": True, + "websockNotifConfig": { + "websocketUri": "string", + "requestWebsocketUri": True + }, + "supportedFeatures": "fff" + } + + number_of_apis = len( + self.invoker_capif_details["registered_security_contexes"]) + + for i in range(0, number_of_apis): + # Obtaining the values of api_id and aef_id for each API + aef_profiles = self.invoker_capif_details["registered_security_contexes"][i]['aef_profiles'] + api_id = self.invoker_capif_details["registered_security_contexes"][i]['api_id'] + for n in range(0, len(aef_profiles)): + aef_id = self.invoker_capif_details["registered_security_contexes"][i]['aef_profiles'][n]['aef_id'] + + security_info = { + "prefSecurityMethods": ["Oauth"], + "authenticationInfo": "string", + "authorizationInfo": "string", + "aefId": aef_id, + "apiId": api_id + } + payload["securityInfo"].append(security_info) + + try: + response = requests.post(url, + json=payload, + cert=(self.signed_key_crt_path, + self.private_key_path), + verify=self.ca_root_path) + response.raise_for_status() + self.logger.info("Security context correctly updated") + + except requests.exceptions.HTTPError as http_err: + if response.status_code == 404: + self.logger.warning( + "Received 404 exception from target CAPIF. This means it is the first time this CAPIF user is getting the JWT token, redirecting to register security service in CAPIF. The process continues correctly.") + self.__register_security_service() + else: + self.logger.error("HTTP error occurred: %s", str(http_err)) + raise + + except requests.RequestException as e: + self.logger.error( + "Error trying to update Security context: %s", str(e)) + raise + + def __register_security_service(self): + """ + :param api_id: El id del API devuelto por descubrir servicios + :param aef_id: El aef_id devuelto por descubrir servicios + :return: None + """ + + url = f"https://{self.capif_host}:{self.capif_https_port}/capif-security/v1/trustedInvokers/{self.invoker_capif_details['api_invoker_id']}" + payload = { + "securityInfo": [], + "notificationDestination": "https://mynotificationdest.com", + "requestTestNotification": True, + "websockNotifConfig": { + "websocketUri": "string", + "requestWebsocketUri": True + }, + "supportedFeatures": "fff" + } + + number_of_apis = len( + self.invoker_capif_details["registered_security_contexes"]) + + for i in range(0, number_of_apis): + # Obtaining the values of api_id and aef_id for each API + aef_profiles = self.invoker_capif_details["registered_security_contexes"][i]['aef_profiles'] + api_id = self.invoker_capif_details["registered_security_contexes"][i]['api_id'] + for n in range(0, len(aef_profiles)): + aef_id = self.invoker_capif_details["registered_security_contexes"][i]['aef_profiles'][n]['aef_id'] + + security_info = { + "prefSecurityMethods": ["Oauth"], + "authenticationInfo": "string", + "authorizationInfo": "string", + "aefId": aef_id, + "apiId": api_id + } + payload["securityInfo"].append(security_info) + + payload["securityInfo"].append(security_info) + + try: + response = requests.put(url, + json=payload, + cert=(self.signed_key_crt_path, + self.private_key_path), + verify=self.ca_root_path + ) + response.raise_for_status() + self.logger.info("Security service properly registered") + except requests.RequestException as e: + self.logger.error( + "Error when registering the security service: %s", str(e)) + raise + + def __get_security_token(self): + """ + :param api_name: El nombre del API devuelto por descubrir servicios + :param aef_id: El aef_id relevante devuelto por descubrir servicios + :return: El token de acceso (jwt) + """ + url = f"https://{self.capif_host}:{self.capif_https_port}/capif-security/v1/securities/{self.invoker_capif_details['api_invoker_id']}/token" + # Build the scope by concatenating aef_id and api_name separated by a ';' + scope_parts = [] + + # Iterate over the registered contexts and build the scope parts + for context in self.invoker_capif_details["registered_security_contexes"]: + api_name = context["api_name"] + for i in range(0, len(context['aef_profiles'])): + aef_id = context['aef_profiles'][i]['aef_id'] + scope_parts.append(f"{aef_id}:{api_name}") + + # Join all the scope parts with ';' and add the prefix '3gpp#' + scope = "3gpp#" + ";".join(scope_parts) + + payload = { + "grant_type": "client_credentials", + "client_id": self.invoker_capif_details["api_invoker_id"], + "client_secret": "string", + "scope": scope + } + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + } + + try: + response = requests.post(url, + headers=headers, + data=payload, + cert=(self.signed_key_crt_path, + self.private_key_path), + verify=self.ca_root_path + ) + response.raise_for_status() + response_payload = response.json() + self.logger.info("Security token successfully obtained") + return response_payload + except requests.RequestException as e: + self.logger.error( + "Error obtaining the security token: %s ", str(e)) + raise + + def discover_service_apis(self): + """ + Descubre los APIs de servicio desde CAPIF con filtros basados en un archivo JSON. + :return: Payload JSON con los detalles de los APIs de servicio + """ + # Load the parameters from the JSON file + + # Filter out parameters that are not empty + filters = self.discover_filter + + query_params = {k: v for k, v in filters.items() if v.strip()} + + # Form the URL with the query parameters + query_string = "&".join([f"{k}={v}" for k, v in query_params.items()]) + + url = f"https://{self.capif_host}:{self.capif_https_port}/{self.invoker_capif_details['discover_services_url']}{self.invoker_capif_details['api_invoker_id']}" + + if query_string: + url += f"&{query_string}" + + try: + response = requests.get( + url, + headers={"Content-Type": "application/json"}, + cert=(self.signed_key_crt_path, self.private_key_path), + verify=self.ca_root_path + ) + + response.raise_for_status() + response_payload = response.json() + self.logger.info("Service APIs successfully discovered") + return response_payload + except requests.RequestException as e: + self.logger.error("Error discovering service APIs: %s", str(e)) + raise + + def retrieve_api_description_by_name(self, api_name): + """ + Recupera la descripción del API por nombre. + :param api_name: Nombre del API + :return: Descripción del API + """ + self.logger.info( + "Retrieving the API description for api_name=%s", api_name) + capif_apifs = self.discover_service_apis() + endpoints = [api for api in capif_apifs["serviceAPIDescriptions"] + if api["apiName"] == api_name] + if not endpoints: + error_message = ( + f"Could not find available endpoints for api_name: {api_name}. " + "Make sure that a) your Invoker is registered and onboarded to CAPIF and " + "b) the NEF emulator has been registered and onboarded to CAPIF" + ) + self.logger.error(error_message) + raise ServiceDiscoverer.ServiceDiscovererException(error_message) + else: + self.logger.info("API description successfully retrieved") + return endpoints[0] + + def retrieve_specific_resource_name(self, api_name, resource_name): + """ + Recupera la URL para recursos específicos dentro de los APIs. + :param api_name: Nombre del API + :param resource_name: Nombre del recurso + :return: URL del recurso específico + """ + self.logger.info( + "Retrieving the URL for resource_name=%s in api_name=%s", resource_name, api_name) + api_description = self.retrieve_api_description_by_name(api_name) + version_dictionary = api_description["aefProfiles"][0]["versions"][0] + version = version_dictionary["apiVersion"] + resources = version_dictionary["resources"] + uris = [resource["uri"] + for resource in resources if resource["resourceName"] == resource_name] + + if not uris: + error_message = f"Could not find resource_name: {resource_name} at api_name {api_name}" + self.logger.error(error_message) + raise ServiceDiscoverer.ServiceDiscovererException(error_message) + else: + uri = uris[0] + if not uri.startswith("/"): + uri = "/" + uri + if api_name.endswith("/"): + api_name = api_name[:-1] + result_url = api_name + "/" + version + uri + self.logger.info( + "URL of the specific resource successfully retrieved: %s", result_url) + return result_url + + def save_security_token(self, token): + self.invoker_capif_details["access_token"] = token + self.__cache_security_context() + + def get_tokens(self): + + self.get_security_context() + token = self.get_access_token() + self.token = token + self.save_security_token(token) + + def discover(self): + endpoints = self.discover_service_apis() + + if len(endpoints) > 0: + self.save_api_discovered(endpoints) + else: + self.logger.error( + "No endpoints have been registered. Make sure a Provider has Published an API to CAPIF first") + + def save_api_discovered(self, endpoints): + self.invoker_capif_details["registered_security_contexes"] = [] + p = 0 + for service in endpoints["serviceAPIDescriptions"]: + api_id = service["apiId"] + api_name = service["apiName"] + aef_profiles = [] + self.invoker_capif_details["registered_security_contexes"].append({"api_name": api_name, "api_id": api_id, "aef_profiles": aef_profiles}) + for n in service["aefProfiles"]: + versions = n["versions"] + aef_id = n["aefId"] + for m in n["interfaceDescriptions"]: + ip = m["ipv4Addr"] + port = m["port"] + self.invoker_capif_details["registered_security_contexes"][p]['aef_profiles'].append( + {"aef_id": aef_id, "ip": ip, "port": port, "versions": versions}) + p += 1 + + self.save_api_details() + + def save_api_details(self): + try: + # Define the path to save the details + file_path = os.path.join( + self.invoker_folder, "capif_api_security_context_details-" + self.capif_invoker_username + ".json") + + # Save the details as a JSON file + with open(file_path, "w") as outfile: + json.dump(self.invoker_capif_details, outfile, indent=4) + + # Log the success of the operation + self.logger.info("API provider details correctly saved") + + except Exception as e: + # Log any errors that occur during the save process + self.logger.error( + "Error while saving API provider details: %s", str(e)) + raise + + def check_authentication(self): + print("hola") + self.logger.info("Checking authentication") + try: + invoker_details = self.__load_provider_api_details() + invoker_id = invoker_details["api_invoker_id"] + check_auth = self.check_authentication_data + url = "http://"+f"{check_auth['ip']}:{check_auth['port']}/" + "aef-security/v1/check-authentication" + + payload = { + "apiInvokerId": f"{invoker_id}", + "supportedFeatures": f"{self.supported_features}" + } + print(self.supported_features) + + headers = { + "Authorization": "Bearer {}".format(self.token), + "Content-Type": "application/json", + } + + response = requests.request( + "POST", + url, + headers=headers, + json=payload + ) + + print(response.text) + response.raise_for_status() + self.logger.info("Authentication of supported_features checked") + + except Exception as e: + self.logger.error( + f"Error during checking Invoker supported_features : {e} - Response: {response.text}") + raise diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..4434717117c6d031790d0be354ad745160896999 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,51 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "opencapif_sdk" +version = "0.1.15" +authors = [ + { name="JorgeEcheva", email="jorge.echevarriauribarri.practicas@telefonica.com" }, + { name="dgs-cgm", email="daniel.garciasanchez@telefonica.com" } +] +description = "This repository develops a Python Software Development Kit(SDK) which focuses on connecting to OpenCAPIF (Common API Framework for 3GPP Northbound APIs) in a simple way, lowering integration complexity and allowing developers to focus on Network Applications (Network Apps) or services development." +readme = "./doc/README_pipy.md" +license = { file="LICENSE" } +requires-python = ">=3.9" +keywords = ["pesp_capif_sdk","capif","sdk capif","opencapif_sdk"] +dependencies = [ + "requests==2.32.3", + "PyYAML==6.0.1", + "cryptography==38.0.4", + "pyOpenSSL==22.1.0", + "urllib3==2.2.2", + "certifi==2024.7.4", + "idna==3.7", + "Flask==3.0.3", + "Flask-JWT-Extended==4.6.0", + "Jinja2==3.1.4", + "MarkupSafe==2.1.5", + "six==1.16.0", + "typing-extensions>=4.8.0", + "Werkzeug==3.0.4", + "pytest==8.3.2", + "flake8==3.9.2", + "coverage==4.5.4", + "mccabe==0.6.1", + "pycodestyle==2.7.0", + "pyflakes==2.3.1", + "python-dateutil==2.9.0.post0", + "jinja2-time==0.2.0", + "text-unidecode==1.3", + "binaryornot==0.4.4" +] + + +classifiers = [ + "Programming Language :: Python :: 3", + "Operating System :: OS Independent", +] + +[project.urls] +"Homepage" = "https://github.com/Telefonica/pesp_capif_sdk" diff --git a/samples/capif_api_security_context_details_sample.json b/samples/capif_api_security_context_details_sample.json new file mode 100644 index 0000000000000000000000000000000000000000..ec3ee6d4c967ac71582bb626e82427ebd9895967 --- /dev/null +++ b/samples/capif_api_security_context_details_sample.json @@ -0,0 +1,1054 @@ +{ + "user_name": "echeva_0", + "api_invoker_id": "INV74eff9e863a1b0344bbe6e1557f40a", + "discover_services_url": "service-apis/v1/allServiceAPIs?api-invoker-id=", + "registered_security_contexes": [ + { + "api_name": "Test66", + "api_id": "feb57d7c92dfd2b94da3f53ceadf33", + "aef_profiles": [ + { + "aef_id": "AEF72e7f944a20fe072e9db263533b646", + "ip": "http://10.17.173.87", + "port": 8088, + "versions": [ + { + "apiVersion": "v1", + "expiry": "2025-08-27T09:16:41.278000+00:00", + "resources": [ + { + "resourceName": "dynamic-deployment", + "commType": "REQUEST_RESPONSE", + "uri": "/api/svc/v1/dynamic-deployment", + "custOpName": "string", + "operations": [ + "GET" + ], + "description": "Dynamic deployment" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "GET" + ], + "description": "string" + } + ] + } + ] + } + ] + }, + { + "api_name": "Test77", + "api_id": "de766971736ca69b578642f9942447", + "aef_profiles": [ + { + "aef_id": "AEF5608b606ab08cfd7111018e3936666", + "ip": "http://10.17.173.87", + "port": 8088, + "versions": [ + { + "apiVersion": "v1", + "expiry": "2025-08-27T09:17:30.918000+00:00", + "resources": [ + { + "resourceName": "dynamic-deployment", + "commType": "REQUEST_RESPONSE", + "uri": "/api/svc/v1/dynamic-deployment", + "custOpName": "string", + "operations": [ + "GET" + ], + "description": "Dynamic deployment" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "GET" + ], + "description": "string" + } + ] + } + ] + } + ] + }, + { + "api_name": "Deployment", + "api_id": "aabc425680791995b1fbb93a01c7d1", + "aef_profiles": [ + { + "aef_id": "AEFd836e9b49161673e3ee6632c409520", + "ip": "http://10.17.173.87", + "port": 8088, + "versions": [ + { + "apiVersion": "v1", + "expiry": "2025-08-28T08:00:21.178000+00:00", + "resources": [ + { + "resourceName": "dynamic-deployment", + "commType": "REQUEST_RESPONSE", + "uri": "/api/svc/v1/dynamic-deployment", + "custOpName": "string", + "operations": [ + "GET" + ], + "description": "Dynamic deployment" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "GET" + ], + "description": "string" + } + ] + } + ] + } + ] + }, + { + "api_name": "Deployment1", + "api_id": "dd0b9f983eeb2286431ed45980a4ef", + "aef_profiles": [ + { + "aef_id": "AEF4610c63fcb94052eb8847abd2c5ed7", + "ip": "http://10.17.173.87", + "port": 8088, + "versions": [ + { + "apiVersion": "v1", + "expiry": "2025-09-03T14:00:17.889000+00:00", + "resources": [ + { + "resourceName": "dynamic-deployment", + "commType": "REQUEST_RESPONSE", + "uri": "/api/svc/v1/dynamic-deployment", + "custOpName": "string", + "operations": [ + "GET" + ], + "description": "Dynamic deployment" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "GET" + ], + "description": "string" + } + ] + } + ] + } + ] + }, + { + "api_name": "ks8500_gateway_development", + "api_id": "8eb8d27d533731ae500e00acdab87a", + "aef_profiles": [ + { + "aef_id": "AEF4ca2cfb9700f917b8cce1368599c60", + "ip": "localhost", + "port": 443, + "versions": [ + { + "apiVersion": "v1", + "resources": [ + { + "resourceName": "runs", + "commType": "REQUEST_RESPONSE", + "uri": "/runs", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "Schedule a campaign run" + } + ] + } + ] + } + ] + }, + { + "api_name": "Test-2", + "api_id": "12eb4b45270b24c2e2e19ebf949ac0", + "aef_profiles": [ + { + "aef_id": "AEFf9911166dadd72d9f2305e9121776d", + "ip": "127.0.0.1", + "port": 8888, + "versions": [ + { + "apiVersion": "v1", + "expiry": "2100-11-30T10:32:02.004000+00:00", + "resources": [ + { + "resourceName": "MONITORING_SUBSCRIPTIONS", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/{scsAsId}/subscriptions", + "custOpName": "http_post", + "operations": [ + "GET", + "POST" + ], + "description": "Endpoint to manage monitoring subscriptions" + }, + { + "resourceName": "MONITORING_SUBSCRIPTION_SINGLE", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/{scsAsId}/subscriptions/{subscriptionId}", + "custOpName": "http_get", + "operations": [ + "GET", + "PUT", + "DELETE" + ], + "description": "Endpoint to manage single subscription" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "string" + } + ] + } + ] + }, + { + "aef_id": "AEF85bef05b30b1e9d32c5b7cb6ec7d00", + "ip": "127.0.0.1", + "port": 8899, + "versions": [ + { + "apiVersion": "v1", + "expiry": "2100-11-30T10:32:02.004000+00:00", + "resources": [ + { + "resourceName": "TSN_LIST_PROFILES", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/profile", + "custOpName": "http_get", + "operations": [ + "GET" + ], + "description": "Endpoint for retrieving the list of available TSN profiles" + }, + { + "resourceName": "TSN_DETAIL_PROFILE", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/profile?name={profileName}", + "custOpName": "http_get", + "operations": [ + "GET" + ], + "description": "Endpoint for retrieving information about a single TSN profile" + }, + { + "resourceName": "TSN_APPLY_CONFIGURATION", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/apply", + "custOpName": "http_post", + "operations": [ + "POST" + ], + "description": "Endpoint for configuring TSN connection parameters" + }, + { + "resourceName": "TSN_CLEAR_CONFIGURATION", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/clear", + "custOpName": "http_post", + "operations": [ + "POST" + ], + "description": "Endpoint for removing a previous TSN connection configuration" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "string" + } + ] + } + ] + }, + { + "aef_id": "AEFc68bb090546dfc11d3986adbccbb12", + "ip": "127.0.0.1", + "port": 8888, + "versions": [ + { + "apiVersion": "v1", + "expiry": "2100-11-30T10:32:02.004000+00:00", + "resources": [ + { + "resourceName": "MONITORING_SUBSCRIPTIONS", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/{scsAsId}/subscriptions", + "custOpName": "http_post", + "operations": [ + "GET", + "POST" + ], + "description": "Endpoint to manage monitoring subscriptions" + }, + { + "resourceName": "MONITORING_SUBSCRIPTION_SINGLE", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/{scsAsId}/subscriptions/{subscriptionId}", + "custOpName": "http_get", + "operations": [ + "GET", + "PUT", + "DELETE" + ], + "description": "Endpoint to manage single subscription" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "string" + } + ] + } + ] + } + ] + }, + { + "api_name": "demo_api_OCF", + "api_id": "e16c9dc483576f7b5fb3a2350294d9", + "aef_profiles": [ + { + "aef_id": "AEFa100b1a76ce0a5e8495c6ebf43bf4c", + "ip": "one_provider_gui", + "port": 8088, + "versions": [ + { + "apiVersion": "v1", + "expiry": "2025-10-02T08:27:50.894000+00:00", + "resources": [ + { + "resourceName": "create-vm-endpoint", + "commType": "REQUEST_RESPONSE", + "uri": "/hello", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "Endpoint to say hello" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "string" + } + ] + } + ] + } + ] + }, + { + "api_name": "ss-gm", + "api_id": "2c55c06867c55cf46bfc291150a95c", + "aef_profiles": [ + { + "aef_id": "AEF9415a9171d81ded2b30dc060a288b5", + "ip": "localhost", + "port": 8089, + "versions": [ + { + "apiVersion": "v1", + "expiry": "2025-11-30T10:32:02.004000+00:00", + "resources": [ + { + "resourceName": "VAL Group documents endpoint", + "commType": "REQUEST_RESPONSE", + "uri": "/group-documents", + "custOpName": "string", + "operations": [ + "POST", + "GET" + ], + "description": "VAL Group Documents" + }, + { + "resourceName": "Individual VAL Group Document endpoint", + "commType": "REQUEST_RESPONSE", + "uri": "/group-documents/{groupDocId}", + "custOpName": "string", + "operations": [ + "GET", + "PUT", + "DELETE", + "PATCH" + ], + "description": "Individual VAL Group Document" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "Text description of the custom operation" + } + ] + } + ] + } + ] + }, + { + "api_name": "ss-lair", + "api_id": "b68143d2c3e1b0156680f22cf911d9", + "aef_profiles": [ + { + "aef_id": "AEF9415a9171d81ded2b30dc060a288b5", + "ip": "localhost", + "port": 8086, + "versions": [ + { + "apiVersion": "v1", + "expiry": "2025-11-30T10:32:02.004000+00:00", + "resources": [ + { + "resourceName": "Location Information endpoint", + "commType": "REQUEST_RESPONSE", + "uri": "/location-retrievals", + "operations": [ + "GET" + ], + "description": "Retrieve the UE(s) information in an application defined proximity range of a location" + } + ] + } + ] + } + ] + }, + { + "api_name": "5G-Network-App", + "api_id": "c7c893d887f1b8d3b53c971489935d", + "aef_profiles": [ + { + "aef_id": "AEFba23b71a8e3665a596a37aeea0995f", + "ip": "localhost", + "port": 8888, + "versions": [ + { + "apiVersion": "QoS_v1", + "expiry": "2100-11-30T10:32:02.004000+00:00", + "resources": [ + { + "resourceName": "QOS_MANAGEMENT", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/{scsAsId}/qos", + "custOpName": "http_post", + "operations": [ + "GET", + "POST" + ], + "description": "Endpoint to manage QoS levels for network traffic" + }, + { + "resourceName": "QOS_PROFILE_SINGLE", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/{scsAsId}/qos/{profileId}", + "custOpName": "http_get", + "operations": [ + "GET", + "PUT", + "DELETE" + ], + "description": "Endpoint to manage single QoS profile" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "Custom operation for managing QoS parameters" + } + ] + } + ] + }, + { + "aef_id": "AEF0958564e9312e20113aa958afe8e91", + "ip": "localhost", + "port": 8888, + "versions": [ + { + "apiVersion": "TSN_v1", + "expiry": "2100-11-30T10:32:02.004000+00:00", + "resources": [ + { + "resourceName": "TSN_LIST_PROFILES", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/profile", + "custOpName": "http_get", + "operations": [ + "GET" + ], + "description": "Endpoint for retrieving the list of available TSN profiles" + }, + { + "resourceName": "TSN_DETAIL_PROFILE", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/profile?name={profileName}", + "custOpName": "http_get", + "operations": [ + "GET" + ], + "description": "Endpoint for retrieving information about a single TSN profile" + }, + { + "resourceName": "TSN_APPLY_CONFIGURATION", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/apply", + "custOpName": "http_post", + "operations": [ + "POST" + ], + "description": "Endpoint for configuring TSN connection parameters" + }, + { + "resourceName": "TSN_CLEAR_CONFIGURATION", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/clear", + "custOpName": "http_post", + "operations": [ + "POST" + ], + "description": "Endpoint for removing a previous TSN connection configuration" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "Custom operation for managing TSN profiles" + } + ] + } + ] + }, + { + "aef_id": "AEF42c0e50be3d517a47ca9a947eb61c5", + "ip": "localhost", + "port": 8888, + "versions": [ + { + "apiVersion": "NetworkSlicing_v1", + "expiry": "2100-11-30T10:32:02.004000+00:00", + "resources": [ + { + "resourceName": "SLICE_MANAGEMENT", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/slice", + "custOpName": "http_post", + "operations": [ + "GET", + "POST" + ], + "description": "Endpoint for managing network slices" + }, + { + "resourceName": "SLICE_SINGLE", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/slice/{sliceId}", + "custOpName": "http_get", + "operations": [ + "GET", + "PUT", + "DELETE" + ], + "description": "Endpoint for managing a single network slice" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "Custom operation for configuring network slices" + } + ] + } + ] + } + ] + }, + { + "api_name": "5G-App-Network", + "api_id": "88f0b4402c7e3b3b7386d54298de09", + "aef_profiles": [ + { + "aef_id": "AEF02edd275535f002f9a8ac3f1728a19", + "ip": "localhost", + "port": 8888, + "versions": [ + { + "apiVersion": "QoS_v1", + "expiry": "2100-11-30T10:32:02.004000+00:00", + "resources": [ + { + "resourceName": "QOS_MANAGEMENT", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/{scsAsId}/qos", + "custOpName": "http_post", + "operations": [ + "GET", + "POST" + ], + "description": "Endpoint to manage QoS levels for network traffic" + }, + { + "resourceName": "QOS_PROFILE_SINGLE", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/{scsAsId}/qos/{profileId}", + "custOpName": "http_get", + "operations": [ + "GET", + "PUT", + "DELETE" + ], + "description": "Endpoint to manage single QoS profile" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "Custom operation for managing QoS parameters" + } + ] + } + ] + }, + { + "aef_id": "AEF698cc1f41d41ee700bd1294d0d4e63", + "ip": "localhost", + "port": 8888, + "versions": [ + { + "apiVersion": "TSN_v1", + "expiry": "2100-11-30T10:32:02.004000+00:00", + "resources": [ + { + "resourceName": "TSN_LIST_PROFILES", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/profile", + "custOpName": "http_get", + "operations": [ + "GET" + ], + "description": "Endpoint for retrieving the list of available TSN profiles" + }, + { + "resourceName": "TSN_DETAIL_PROFILE", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/profile?name={profileName}", + "custOpName": "http_get", + "operations": [ + "GET" + ], + "description": "Endpoint for retrieving information about a single TSN profile" + }, + { + "resourceName": "TSN_APPLY_CONFIGURATION", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/apply", + "custOpName": "http_post", + "operations": [ + "POST" + ], + "description": "Endpoint for configuring TSN connection parameters" + }, + { + "resourceName": "TSN_CLEAR_CONFIGURATION", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/clear", + "custOpName": "http_post", + "operations": [ + "POST" + ], + "description": "Endpoint for removing a previous TSN connection configuration" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "Custom operation for managing TSN profiles" + } + ] + } + ] + }, + { + "aef_id": "AEF5a7180bc8e5191fea0d54a7e96282b", + "ip": "localhost", + "port": 8888, + "versions": [ + { + "apiVersion": "NetworkSlicing_v1", + "expiry": "2100-11-30T10:32:02.004000+00:00", + "resources": [ + { + "resourceName": "SLICE_MANAGEMENT", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/slice", + "custOpName": "http_post", + "operations": [ + "GET", + "POST" + ], + "description": "Endpoint for managing network slices" + }, + { + "resourceName": "SLICE_SINGLE", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/slice/{sliceId}", + "custOpName": "http_get", + "operations": [ + "GET", + "PUT", + "DELETE" + ], + "description": "Endpoint for managing a single network slice" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "Custom operation for configuring network slices" + } + ] + } + ] + } + ] + }, + { + "api_name": "6G-resilience", + "api_id": "59586353d430dc01edf966a03843ce", + "aef_profiles": [ + { + "aef_id": "AEF3c00fec00ce2e94df8e227210a33d3", + "ip": "localhost", + "port": 8088, + "versions": [ + { + "apiVersion": "6G_Resilience_v1", + "expiry": "2100-11-30T10:32:02.004000+00:00", + "resources": [ + { + "resourceName": "RESILIENCE_MANAGEMENT", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/resilience", + "custOpName": "http_post", + "operations": [ + "GET", + "POST" + ], + "description": "Endpoint to manage resilience functionalities in 6G networks" + }, + { + "resourceName": "RESILIENCE_SINGLE", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/resilience/{profileId}", + "custOpName": "http_get", + "operations": [ + "GET", + "PUT", + "DELETE" + ], + "description": "Endpoint to manage a single resilience profile" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "Custom operation for managing 6G resilience parameters" + } + ] + } + ] + }, + { + "aef_id": "AEF28a603120c743e8d615a55c143400c", + "ip": "localhost", + "port": 8088, + "versions": [ + { + "apiVersion": "6G_Resilience_v1", + "expiry": "2100-11-30T10:32:02.004000+00:00", + "resources": [ + { + "resourceName": "SLICE_RESILIENCE", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/slice/resilience", + "custOpName": "http_post", + "operations": [ + "GET", + "POST" + ], + "description": "Endpoint for managing resilience in network slices" + }, + { + "resourceName": "SLICE_SINGLE_RESILIENCE", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/slice/{sliceId}/resilience", + "custOpName": "http_get", + "operations": [ + "GET", + "PUT", + "DELETE" + ], + "description": "Endpoint for managing a single slice's resilience" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "Custom operation for configuring resilience in network slices" + } + ] + } + ] + } + ] + }, + { + "api_name": "5G-Network-App-Function", + "api_id": "f36d98c81dc7ecfe7ff97bd17ee549", + "aef_profiles": [ + { + "aef_id": "AEF3c00fec00ce2e94df8e227210a33d3", + "ip": "localhost", + "port": 8888, + "versions": [ + { + "apiVersion": "QoS_v1", + "expiry": "2100-11-30T10:32:02.004000+00:00", + "resources": [ + { + "resourceName": "QOS_MANAGEMENT", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/{scsAsId}/qos", + "custOpName": "http_post", + "operations": [ + "GET", + "POST" + ], + "description": "Endpoint to manage QoS levels for network traffic" + }, + { + "resourceName": "QOS_PROFILE_SINGLE", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/{scsAsId}/qos/{profileId}", + "custOpName": "http_get", + "operations": [ + "GET", + "PUT", + "DELETE" + ], + "description": "Endpoint to manage single QoS profile" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "Custom operation for managing QoS parameters" + } + ] + } + ] + }, + { + "aef_id": "AEF28a603120c743e8d615a55c143400c", + "ip": "localhost", + "port": 8888, + "versions": [ + { + "apiVersion": "TSN_v1", + "expiry": "2100-11-30T10:32:02.004000+00:00", + "resources": [ + { + "resourceName": "TSN_LIST_PROFILES", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/profile", + "custOpName": "http_get", + "operations": [ + "GET" + ], + "description": "Endpoint for retrieving the list of available TSN profiles" + }, + { + "resourceName": "TSN_DETAIL_PROFILE", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/profile?name={profileName}", + "custOpName": "http_get", + "operations": [ + "GET" + ], + "description": "Endpoint for retrieving information about a single TSN profile" + }, + { + "resourceName": "TSN_APPLY_CONFIGURATION", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/apply", + "custOpName": "http_post", + "operations": [ + "POST" + ], + "description": "Endpoint for configuring TSN connection parameters" + }, + { + "resourceName": "TSN_CLEAR_CONFIGURATION", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/clear", + "custOpName": "http_post", + "operations": [ + "POST" + ], + "description": "Endpoint for removing a previous TSN connection configuration" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "Custom operation for managing TSN profiles" + } + ] + } + ] + }, + { + "aef_id": "AEFc9de485870465cefd3ca91b9cee02b", + "ip": "localhost", + "port": 8888, + "versions": [ + { + "apiVersion": "NetworkSlicing_v1", + "expiry": "2100-11-30T10:32:02.004000+00:00", + "resources": [ + { + "resourceName": "SLICE_MANAGEMENT", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/slice", + "custOpName": "http_post", + "operations": [ + "GET", + "POST" + ], + "description": "Endpoint for managing network slices" + }, + { + "resourceName": "SLICE_SINGLE", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/slice/{sliceId}", + "custOpName": "http_get", + "operations": [ + "GET", + "PUT", + "DELETE" + ], + "description": "Endpoint for managing a single network slice" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "Custom operation for configuring network slices" + } + ] + } + ] + } + ] + } + ], + "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTcyOTc1ODUwNiwianRpIjoiY2FiNTNlMDMtNDI1Yy00MTA2LTkzNmYtZDE0ZGRjNjgyNjMyIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6IklOVjc0ZWZmOWU4NjNhMWIwMzQ0YmJlNmUxNTU3ZjQwYSIsIm5iZiI6MTcyOTc1ODUwNiwiZXhwIjoxNzI5NzU5MTA2LCJpc3MiOiJJTlY3NGVmZjllODYzYTFiMDM0NGJiZTZlMTU1N2Y0MGEiLCJzY29wZSI6IjNncHAjQUVGNzJlN2Y5NDRhMjBmZTA3MmU5ZGIyNjM1MzNiNjQ2OlRlc3Q2NjtBRUY1NjA4YjYwNmFiMDhjZmQ3MTExMDE4ZTM5MzY2NjY6VGVzdDc3O0FFRmQ4MzZlOWI0OTE2MTY3M2UzZWU2NjMyYzQwOTUyMDpEZXBsb3ltZW50O0FFRjQ2MTBjNjNmY2I5NDA1MmViODg0N2FiZDJjNWVkNzpEZXBsb3ltZW50MTtBRUY0Y2EyY2ZiOTcwMGY5MTdiOGNjZTEzNjg1OTljNjA6a3M4NTAwX2dhdGV3YXlfZGV2ZWxvcG1lbnQ7QUVGZjk5MTExNjZkYWRkNzJkOWYyMzA1ZTkxMjE3NzZkOlRlc3QtMjtBRUY4NWJlZjA1YjMwYjFlOWQzMmM1YjdjYjZlYzdkMDA6VGVzdC0yO0FFRmM2OGJiMDkwNTQ2ZGZjMTFkMzk4NmFkYmNjYmIxMjpUZXN0LTI7QUVGYTEwMGIxYTc2Y2UwYTVlODQ5NWM2ZWJmNDNiZjRjOmRlbW9fYXBpX09DRjtBRUY5NDE1YTkxNzFkODFkZWQyYjMwZGMwNjBhMjg4YjU6c3MtZ207QUVGOTQxNWE5MTcxZDgxZGVkMmIzMGRjMDYwYTI4OGI1OnNzLWxhaXI7QUVGYmEyM2I3MWE4ZTM2NjVhNTk2YTM3YWVlYTA5OTVmOjVHLU5ldHdvcmstQXBwO0FFRjA5NTg1NjRlOTMxMmUyMDExM2FhOTU4YWZlOGU5MTo1Ry1OZXR3b3JrLUFwcDtBRUY0MmMwZTUwYmUzZDUxN2E0N2NhOWE5NDdlYjYxYzU6NUctTmV0d29yay1BcHA7QUVGMDJlZGQyNzU1MzVmMDAyZjlhOGFjM2YxNzI4YTE5OjVHLUFwcC1OZXR3b3JrO0FFRjY5OGNjMWY0MWQ0MWVlNzAwYmQxMjk0ZDBkNGU2Mzo1Ry1BcHAtTmV0d29yaztBRUY1YTcxODBiYzhlNTE5MWZlYTBkNTRhN2U5NjI4MmI6NUctQXBwLU5ldHdvcms7QUVGM2MwMGZlYzAwY2UyZTk0ZGY4ZTIyNzIxMGEzM2QzOjZHLXJlc2lsaWVuY2U7QUVGMjhhNjAzMTIwYzc0M2U4ZDYxNWE1NWMxNDM0MDBjOjZHLXJlc2lsaWVuY2U7QUVGM2MwMGZlYzAwY2UyZTk0ZGY4ZTIyNzIxMGEzM2QzOjVHLU5ldHdvcmstQXBwLUZ1bmN0aW9uO0FFRjI4YTYwMzEyMGM3NDNlOGQ2MTVhNTVjMTQzNDAwYzo1Ry1OZXR3b3JrLUFwcC1GdW5jdGlvbjtBRUZjOWRlNDg1ODcwNDY1Y2VmZDNjYTkxYjljZWUwMmI6NUctTmV0d29yay1BcHAtRnVuY3Rpb24ifQ.F_fMx35rbOlf1je4UVoA4dvyJwaSuAWEboGPNY_ziDtNe6PF1c0VO3_I0Z_V9QQFHSMhOYjwcCK3m9eD0rsIY15Qlw9ER2OLhCoVDQlPZvrRorI7bOnt0Xf2rtQK-NNVR_91VwlBI7h86_cw2M7xqV7IKxYlAl9j49GzNzJtJSNzlW-JrJ_SdHgkPd9_Vbpv0_Nr4-CBKyn9gmZAvmWwA8g_vPoAFKIMQg00R5maQCfflqnvxF_yRDl3eZbO7o9Lrds-E-lFRuYEWHNl7ShhLmuHfcp9Xz7pLqWVlHbdHDJDcbQ4i1sKpLan7eXSM0oT6JdaMuzBrRA3tPPMfzb0Tw" +} \ No newline at end of file diff --git a/samples/config_sample.json b/samples/config_sample.json new file mode 100644 index 0000000000000000000000000000000000000000..1a75d7da7835b30ae7c2a0f20c0eb7824f5beb5d --- /dev/null +++ b/samples/config_sample.json @@ -0,0 +1,66 @@ +{ + "capif_host": "", + "register_host": "", + "capif_https_port": "", + "capif_register_port": "", + "capif_username": "", + "capif_password": "", + "debug_mode": "", + "invoker": { + "invoker_folder": "", + "capif_callback_url": "", + "supported_features":"", + "check_authentication_data":{ + "ip":"", + "port":"" + }, + "cert_generation": { + "csr_common_name": "", + "csr_organizational_unit": "", + "csr_organization": "", + "csr_locality": "", + "csr_state_or_province_name": "", + "csr_country_name": "", + "csr_email_address": "" + }, + "discover_filter": { + "api-name": "", + "api-version": "", + "comm-type": "", + "protocol": "", + "aef-id": "", + "data-format": "", + "api-cat": "", + "preferred-aef-loc": "", + "req-api-prov-name": "", + "supported-features": "", + "api-supported-features": "", + "ue-ip-addr": "", + "service-kpis": "" + } + }, + + "provider": { + "provider_folder": "", + "apfs": "", + "aefs": "", + "publish_req": { + "service_api_id": "", + "publisher_apf_id": "", + "publisher_aefs_ids": [ + "", + "" + ] + }, + "cert_generation":{ + "csr_common_name": "", + "csr_organizational_unit": "", + "csr_organization": "", + "csr_locality": "", + "csr_state_or_province_name": "", + "csr_country_name": "", + "csr_email_address": "" + }, + "api_description_path": "" + } +} diff --git a/samples/enviroment_variables_sample.txt b/samples/enviroment_variables_sample.txt new file mode 100644 index 0000000000000000000000000000000000000000..9741ada8af90ac1dcf0a063324583334b74eda3b --- /dev/null +++ b/samples/enviroment_variables_sample.txt @@ -0,0 +1,62 @@ +#GENERAL CONFIGURATION + +export CAPIF_HOST=your_capif_host +export REGISTER_HOST=your_register_host +export CAPIF_HTTPS_PORT=443 +export CAPIF_REGISTER_PORT=8084 +export CAPIF_USERNAME=your_capif_username +export CAPIF_PASSWORD=your_capif_password +export DEBUG_MODE=False + +# INVOKER +export INVOKER_CAPIF_CALLBACK_URL=https://your_callback_url + +# CSR (Certificate Signing Request) Configuration INVOKER +export INVOKER_CSR_COMMON_NAME=your_csr_common_name +export INVOKER_CSR_ORGANIZATIONAL_UNIT=your_organizational_unit +export INVOKER_CSR_ORGANIZATION=your_organization +export INVOKER_CSR_LOCALITY=your_locality +export INVOKER_CSR_STATE_OR_PROVINCE_NAME=your_state_or_province +export INVOKER_CSR_COUNTRY_NAME=your_country_name +export INVOKER_CSR_EMAIL_ADDRESS=your_email_address + +export INVOKER_CHECK_AUTHENTICATION_DATA_IP = +export INVOKER_CHECK_AUTHENTICATION_DATA_PORT = +#DISCOVER FILTER +export DISCOVER_FILTER_API_NAME="" +export DISCOVER_FILTER_API_VERSION="" +export DISCOVER_FILTER_COMM_TYPE="" +export DISCOVER_FILTER_PROTOCOL="" +export DISCOVER_FILTER_AEF_ID="" +export DISCOVER_FILTER_DATA_FORMAT="" +export DISCOVER_FILTER_API_CAT="" +export DISCOVER_FILTER_PREFERRED_AEF_LOC="" +export DISCOVER_FILTER_REQ_API_PROV_NAME="" +export DISCOVER_FILTER_SUPPORTED_FEATURES="" +export DISCOVER_FILTER_API_SUPPORTED_FEATURES="" +export DISCOVER_FILTER_UE_IP_ADDR="" +export DISCOVER_FILTER_SERVICE_KPIS="" + + +# Provider Configuration +export PROVIDER_FOLDER=/path/to/provider/folder + +# CSR (Certificate Signing Request) Configuration +export PROVIDER_CSR_COMMON_NAME=your_csr_common_name +export PROVIDER_CSR_ORGANIZATIONAL_UNIT=your_organizational_unit +export PROVIDER_CSR_ORGANIZATION=your_organization +export PROVIDER_CSR_LOCALITY=your_locality +export PROVIDER_CSR_STATE_OR_PROVINCE_NAME=your_state_or_province +export PROVIDER_CSR_COUNTRY_NAME=your_country_name +export PROVIDER_CSR_EMAIL_ADDRESS=your_email_address + +# Provider Specific Values +export PROVIDER_APFS=your_apfs_value +export PROVIDER_AEFS=your_aefs_value +export PROVIDER_API_DESCRIPTION_PATH=/path/to/api_description + +# Publish Request Configuration +export PUBLISH_REQ_SERVICE_API_ID=your_service_api_id +export PUBLISH_REQ_PUBLISHER_APF_ID=your_publisher_apf_id +export PUBLISH_REQ_PUBLISHER_AEFS_IDS=your_publisher_aefs_ids + diff --git a/samples/provider_api_description_sample.json b/samples/provider_api_description_sample.json new file mode 100755 index 0000000000000000000000000000000000000000..02d872f572fd2b2b245f883b15cd92ad77e2fcac --- /dev/null +++ b/samples/provider_api_description_sample.json @@ -0,0 +1,154 @@ +{ + "apiName": "Api-de-prueba-2", + "aefProfiles": [ + { + "aefId": "AEF07a01ccd74a160c195e69b4f116d66", + "versions": [ + { + "apiVersion": "v1", + "expiry": "2100-11-30T10:32:02.004Z", + "resources": [ + { + "resourceName": "MONITORING_SUBSCRIPTIONS", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/{scsAsId}/subscriptions", + "custOpName": "http_post", + "operations": [ + "GET", + "POST" + ], + "description": "Endpoint to manage monitoring subscriptions" + }, + { + "resourceName": "MONITORING_SUBSCRIPTION_SINGLE", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/{scsAsId}/subscriptions/{subscriptionId}", + "custOpName": "http_get", + "operations": [ + "GET", + "PUT", + "DELETE" + ], + "description": "Endpoint to manage single subscription" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "string" + } + ] + } + ], + "protocol": "HTTP_1_1", + "dataFormat": "JSON", + "securityMethods": [ + "Oauth" + ], + "interfaceDescriptions": [ + { + "ipv4Addr": "127.0.0.1", + "port": 8888, + "securityMethods": [ + "Oauth" + ] + } + ] + }, + { + "aefId": "AEFb5c206b46fc68c192aed6870899ea1", + "versions": [ + { + "apiVersion": "v1", + "expiry": "2100-11-30T10:32:02.004Z", + "resources": [ + { + "resourceName": "TSN_LIST_PROFILES", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/profile", + "custOpName": "http_get", + "operations": [ + "GET" + ], + "description": "Endpoint for retrieving the list of available TSN profiles" + }, + { + "resourceName": "TSN_DETAIL_PROFILE", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/profile?name={profileName}", + "custOpName": "http_get", + "operations": [ + "GET" + ], + "description": "Endpoint for retrieving information about a single TSN profile" + }, + { + "resourceName": "TSN_APPLY_CONFIGURATION", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/apply", + "custOpName": "http_post", + "operations": [ + "POST" + ], + "description": "Endpoint for configuring TSN connection parameters" + }, + { + "resourceName": "TSN_CLEAR_CONFIGURATION", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/clear", + "custOpName": "http_post", + "operations": [ + "POST" + ], + "description": "Endpoint for removing a previous TSN connection configuration" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "string" + } + ] + } + ], + "protocol": "HTTP_1_1", + "dataFormat": "JSON", + "securityMethods": [ + "Oauth" + ], + "interfaceDescriptions": [ + { + "ipv4Addr": "127.0.0.1", + "port": 8899, + "securityMethods": [ + "Oauth" + ] + } + ] + } + ], + "description": "API of dummy Network-App to test", + "supportedFeatures": "fffff", + "shareableInfo": { + "isShareable": true, + "capifProvDoms": [ + "string" + ] + }, + "serviceAPICategory": "string", + "apiSuppFeats": "fffff", + "pubApiPath": { + "ccfIds": [ + "string" + ] + }, + "ccfId": "string" +} \ No newline at end of file diff --git a/scripts/deregister_and_login.py b/scripts/deregister_and_login.py new file mode 100644 index 0000000000000000000000000000000000000000..d44d914cf9e2ee8a842a9be027f050e8065838a3 --- /dev/null +++ b/scripts/deregister_and_login.py @@ -0,0 +1,86 @@ +import json +import logging +import requests +import urllib3 +import utilities +from requests.auth import HTTPBasicAuth +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + +logging.basicConfig( + level=logging.INFO, # Minimum severity level to log + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', # Log message format + handlers=[ + logging.FileHandler("logs/register_logs.log"), # Logs to a file + logging.StreamHandler() # Also shows in the console + ] +) + + +def main(): + + variables = __load_config_file(config_file=utilities.get_register_file()) + log_result = __log_to_capif(variables) + admintoken = log_result["access_token"] + de_register_from_capif(admintoken, variables) + + logger.info("User eliminated successfully") + + +def __log_to_capif(variables): + logger.info("Logging in to CAPIF") + capif_register_url = "https://" + variables["register_host"].strip() + ":" + variables["capif_register_port"] + "/" + try: + url = capif_register_url + "login" + + response = requests.request( + "POST", + url, + headers={"Content-Type": "application/json"}, + auth=HTTPBasicAuth(variables["capif_register_username"], variables["capif_register_password"]), + verify=False, + ) + response.raise_for_status() + response_payload = json.loads(response.text) + logger.info("Logged in to CAPIF successfully") + return response_payload + except Exception as e: + logger.error(f"Error during login to CAPIF: {e}") + raise + + +def de_register_from_capif(admin_token, variables): + logger.info("Deleting CAPIF user") + capif_register_url = "https://" + variables["register_host"].strip() + ":" + variables["capif_register_port"] + "/" + + url = capif_register_url + "deleteUser/" + variables["uuid"] + + headers = { + "Authorization": "Bearer {}".format(admin_token), + "Content-Type": "application/json", + } + response = requests.request( + "DELETE", + url, + headers=headers, + data=None, + verify=False + ) + response.raise_for_status() + logger.info("User deleted") + + +def __load_config_file(config_file: str): + """Loads the configuration file.""" + try: + with open(config_file, 'r') as file: + return json.load(file) + except FileNotFoundError: + logger.warning(f"Configuration file {config_file} not found. Using defaults or environment variables.") + return {} + + +if __name__ == "__main__": + logger = logging.getLogger("CAPIF Register") + logger.info("Initializing CAPIF Register") + main() diff --git a/scripts/invoker_capif_connector.py b/scripts/invoker_capif_connector.py new file mode 100755 index 0000000000000000000000000000000000000000..688961cd74c83233c3f2068aa52bacb71292624b --- /dev/null +++ b/scripts/invoker_capif_connector.py @@ -0,0 +1,19 @@ +import sys +import os +import utilities +from opencapif_sdk import capif_invoker_connector + +def showcase_capif_connector(): + """ + This method showcases how one can use the CAPIFConnector class. + """ + + capif_connector = capif_invoker_connector(config_file=utilities.get_config_file()) + + capif_connector.onboard_invoker() + print("COMPLETED") + + +if __name__ == "__main__": + # Register invoker to CAPIF. This should happen exactly once + showcase_capif_connector() diff --git a/scripts/invoker_capif_connector_offboarding.py b/scripts/invoker_capif_connector_offboarding.py new file mode 100755 index 0000000000000000000000000000000000000000..52619f02888457ecbc1f590c3e314e090bd66247 --- /dev/null +++ b/scripts/invoker_capif_connector_offboarding.py @@ -0,0 +1,13 @@ +import sys +import os +import utilities +from opencapif_sdk import capif_invoker_connector + +def showcase_offboard_and_deregister_invoker(): + capif_connector = capif_invoker_connector(config_file=utilities.get_config_file()) + capif_connector.offboard_invoker() + print("COMPLETED") + + +if __name__ == "__main__": + showcase_offboard_and_deregister_invoker() diff --git a/scripts/invoker_capif_connector_update.py b/scripts/invoker_capif_connector_update.py new file mode 100644 index 0000000000000000000000000000000000000000..c97b717f39a898dfdefd2c2e5ab99453d0b6cd33 --- /dev/null +++ b/scripts/invoker_capif_connector_update.py @@ -0,0 +1,21 @@ +import sys +import os +import utilities +from opencapif_sdk import capif_invoker_connector + +def showcase_capif_connector(): + """ + This method showcases how one can use the CAPIFConnector class. + This class is intended for use within the evolved5G Command Line interface. + It is a low level class part of the SDK that is not required to use while creating invokers + """ + + capif_connector = capif_invoker_connector(config_file=utilities.get_config_file()) + + capif_connector.update_invoker() + print("COMPLETED") + + +if __name__ == "__main__": + # Register invoker to CAPIF. This should happen exactly once + showcase_capif_connector() diff --git a/scripts/invoker_service_discovery.py b/scripts/invoker_service_discovery.py new file mode 100755 index 0000000000000000000000000000000000000000..569e4c35e0364b9279356b5df8fbbee7caed785f --- /dev/null +++ b/scripts/invoker_service_discovery.py @@ -0,0 +1,13 @@ +import utilities +from opencapif_sdk import service_discoverer +def showcase_access_token_retrieval_from_capif(): + service_discoverer = service_discoverer(config_file=utilities.get_config_file()) + service_discoverer.discover() + + +if __name__ == "__main__": + # The following code assumes that you have already registered the net app to CAPIF. + # showcase_service_discovery() + # showcase_retrieve_endpoint_url_from_tsn() + showcase_access_token_retrieval_from_capif() + print("COMPLETED") diff --git a/scripts/invoker_service_get_token.py b/scripts/invoker_service_get_token.py new file mode 100755 index 0000000000000000000000000000000000000000..e2e92ffb7756132675a03a7077229a853d4749bb --- /dev/null +++ b/scripts/invoker_service_get_token.py @@ -0,0 +1,13 @@ +import utilities +from opencapif_sdk import service_discoverer +def showcase_access_token_retrieval_from_capif(): + service_discoverer = service_discoverer(config_file=utilities.get_config_file()) + service_discoverer.get_tokens() + + +if __name__ == "__main__": + # The following code assumes that you have already registered the net app to CAPIF. + # showcase_service_discovery() + # showcase_retrieve_endpoint_url_from_tsn() + showcase_access_token_retrieval_from_capif() + print("COMPLETED") diff --git a/scripts/provider_capif_connector.py b/scripts/provider_capif_connector.py new file mode 100755 index 0000000000000000000000000000000000000000..a893af5afc9fec51a9b1d8f70f6cf77dc8875cee --- /dev/null +++ b/scripts/provider_capif_connector.py @@ -0,0 +1,19 @@ +import utilities +from opencapif_sdk import capif_provider_connector +# Now import the classes from your sdk.py file + + +def showcase_capif_nef_connector(): + """ + + """ + capif_connector = capif_provider_connector(config_file=utilities.get_config_file()) + + capif_connector.onboard_provider() + + print("COMPLETED") + + +if __name__ == "__main__": + # Register a NEF to CAPIF. This should happen exactly once + showcase_capif_nef_connector() diff --git a/scripts/provider_capif_connector_offboarding.py b/scripts/provider_capif_connector_offboarding.py new file mode 100755 index 0000000000000000000000000000000000000000..36341a798a5b7c2af64392505400981c94c99553 --- /dev/null +++ b/scripts/provider_capif_connector_offboarding.py @@ -0,0 +1,11 @@ +import utilities +from opencapif_sdk import capif_provider_connector + +def offboard_capif_nef_connector(): + capif_connector = capif_provider_connector(config_file=utilities.get_config_file()) + capif_connector.offboard_provider() + print("COMPLETED") + + +if __name__ == "__main__": + offboard_capif_nef_connector() diff --git a/scripts/provider_capif_connector_update.py b/scripts/provider_capif_connector_update.py new file mode 100644 index 0000000000000000000000000000000000000000..5013f3b650c4db8e91d7e5a18799a9eaba2b27a6 --- /dev/null +++ b/scripts/provider_capif_connector_update.py @@ -0,0 +1,17 @@ +import utilities +from opencapif_sdk import capif_provider_connector + +def showcase_capif_nef_connector(): + """ + + """ + capif_connector = capif_provider_connector(config_file=utilities.get_config_file()) + + capif_connector.update_provider() + + print("COMPLETED") + + +if __name__ == "__main__": + # Register a NEF to CAPIF. This should happen exactly once + showcase_capif_nef_connector() diff --git a/scripts/provider_get_all_published_api.py b/scripts/provider_get_all_published_api.py new file mode 100644 index 0000000000000000000000000000000000000000..221dbd0fd9529daff84933a582ea2c11b6448e58 --- /dev/null +++ b/scripts/provider_get_all_published_api.py @@ -0,0 +1,16 @@ +import utilities +from opencapif_sdk import capif_provider_connector + +def showcase_capif_nef_connector(): + """ + + """ + capif_connector = capif_provider_connector(config_file=utilities.get_config_file()) + + capif_connector.get_all_services() + print("COMPLETED") + + +if __name__ == "__main__": + # Register a NEF to CAPIF. This should happen exactly once + showcase_capif_nef_connector() diff --git a/scripts/provider_get_published_api.py b/scripts/provider_get_published_api.py new file mode 100644 index 0000000000000000000000000000000000000000..8d3234053a5dcb849e08e958e817cbd45d883d87 --- /dev/null +++ b/scripts/provider_get_published_api.py @@ -0,0 +1,16 @@ +import utilities +from opencapif_sdk import capif_provider_connector + +def showcase_capif_nef_connector(): + """ + + """ + capif_connector = capif_provider_connector(config_file=utilities.get_config_file()) + + capif_connector.get_service() + print("COMPLETED") + + +if __name__ == "__main__": + # Register a NEF to CAPIF. This should happen exactly once + showcase_capif_nef_connector() diff --git a/scripts/provider_publish_api.py b/scripts/provider_publish_api.py new file mode 100644 index 0000000000000000000000000000000000000000..0487774d28cc89a5a7eb275cda76ed1ecee24913 --- /dev/null +++ b/scripts/provider_publish_api.py @@ -0,0 +1,16 @@ +import utilities +from opencapif_sdk import capif_provider_connector + +def showcase_capif_nef_connector(): + """ + + """ + capif_connector = capif_provider_connector(config_file=utilities.get_config_file()) + + capif_connector.publish_services() + print("COMPLETED") + + +if __name__ == "__main__": + # Register a NEF to CAPIF. This should happen exactly once + showcase_capif_nef_connector() diff --git a/scripts/provider_unpublish_api.py b/scripts/provider_unpublish_api.py new file mode 100644 index 0000000000000000000000000000000000000000..bb76152eb13a0720e3e92a1de6b08a7efb862d1e --- /dev/null +++ b/scripts/provider_unpublish_api.py @@ -0,0 +1,16 @@ +import utilities +from opencapif_sdk import capif_provider_connector + +def showcase_capif_nef_connector(): + """ + + """ + capif_connector = capif_provider_connector(config_file=utilities.get_config_file()) + + capif_connector.unpublish_service() + print("COMPLETED") + + +if __name__ == "__main__": + # Register a NEF to CAPIF. This should happen exactly once + showcase_capif_nef_connector() diff --git a/scripts/provider_update_api.py b/scripts/provider_update_api.py new file mode 100644 index 0000000000000000000000000000000000000000..1103e645779d09958ad7e0b89afd63cd7b4f764d --- /dev/null +++ b/scripts/provider_update_api.py @@ -0,0 +1,16 @@ +import utilities +from opencapif_sdk import capif_provider_connector + +def showcase_capif_nef_connector(): + """ + + """ + capif_connector = capif_provider_connector(config_file=utilities.get_config_file()) + + capif_connector.update_service() + print("COMPLETED") + + +if __name__ == "__main__": + # Register a NEF to CAPIF. This should happen exactly once + showcase_capif_nef_connector() diff --git a/scripts/register_and_login.py b/scripts/register_and_login.py new file mode 100644 index 0000000000000000000000000000000000000000..76646b7f331afac2b5bff5d86441786517450c6f --- /dev/null +++ b/scripts/register_and_login.py @@ -0,0 +1,89 @@ +import json +import logging +import requests +import urllib3 +import utilities +from requests.auth import HTTPBasicAuth +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +logging.basicConfig( + level=logging.INFO, # Minimum severity level to log + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', # Log message format + handlers=[ + logging.FileHandler("logs/register_logs.log"), # Log to a file + logging.StreamHandler() # Also display in the console + ] +) + +def main(): + variables = __load_config_file(config_file=utilities.get_register_file()) + log_result = __log_to_capif(variables) + admintoken = log_result["access_token"] + postcreation = __create_user(admintoken, variables) + uuid = postcreation["uuid"] + logger.info(uuid) + +def __log_to_capif(variables): + logger.info("Logging in to CAPIF") + capif_register_url = "https://" + variables["register_host"].strip() + ":" + variables["capif_register_port"] + "/" + try: + url = capif_register_url + "login" + + response = requests.request( + "POST", + url, + headers={"Content-Type": "application/json"}, + auth=HTTPBasicAuth(variables["capif_register_username"], variables["capif_register_password"]), + verify=False, + ) + response.raise_for_status() + response_payload = json.loads(response.text) + logger.info("Logged in to CAPIF successfully") + return response_payload + except Exception as e: + logger.error(f"Error during login to CAPIF: {e}") + raise + +def __create_user(admin_token, variables): + logger.info("Creating user in CAPIF") + capif_register_url = "https://" + variables["register_host"].strip() + ":" + variables["capif_register_port"] + "/" + try: + url = capif_register_url + "createUser" + payload = { + "username": variables["capif_username"], + "password": variables["capif_password"], + "description": "description", + "email": "csr_email_address@tid.es", + "enterprise": "csr_organization", + "country": "csr_locality", + "purpose": "SDK for SAFE 6G", + } + headers = { + "Authorization": "Bearer {}".format(admin_token), + "Content-Type": "application/json", + } + + response = requests.request( + "POST", url, headers=headers, data=json.dumps(payload), verify=False + ) + response.raise_for_status() + response_payload = json.loads(response.text) + logger.info("User created successfully") + return response_payload + except Exception as e: + logger.error(f"Error during user creation in CAPIF: {e}") + raise + +def __load_config_file(config_file: str): + """Load the configuration file.""" + try: + with open(config_file, 'r') as file: + return json.load(file) + except FileNotFoundError: + logger.warning(f"Configuration file {config_file} not found. Using defaults or environment variables.") + return {} + +if __name__ == "__main__": + logger = logging.getLogger("CAPIF Register") + logger.info("Initializing CAPIF Register") + main() diff --git a/scripts/utilities.py b/scripts/utilities.py new file mode 100755 index 0000000000000000000000000000000000000000..238f969ea14f4fefe6ce480a856f4c94046c5c1d --- /dev/null +++ b/scripts/utilities.py @@ -0,0 +1,8 @@ + + +def get_config_file() -> str: + return "../config/capif_sdk_config.json" + + +def get_register_file() -> str: + return "../config/capif_sdk_register.json" \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..ace150b0f9af230197c46cf70069740407c99245 --- /dev/null +++ b/setup.py @@ -0,0 +1,9 @@ +"""The setup script.""" + +from setuptools import setup, find_packages + +setup( + name='opencapif_sdk', + packages=find_packages(include=["opencapif_sdk"]), + version="0.1.15", +) \ No newline at end of file diff --git a/test/capif_sdk_config_sample_test.json b/test/capif_sdk_config_sample_test.json new file mode 100644 index 0000000000000000000000000000000000000000..49502a95d15470f10cd2c65a27b234f26ff1303f --- /dev/null +++ b/test/capif_sdk_config_sample_test.json @@ -0,0 +1,65 @@ +{ + "capif_host": "capif-prev.mobilesandbox.cloud", + "register_host": "registercapif-prev.mobilesandbox.cloud", + "capif_https_port": "36212", + "capif_register_port": "36211", + "capif_username": "echeva_0", + "capif_password": "echevapass", + "debug_mode": "True", + "invoker":{ + "invoker_folder": "/Users/IDB0128/Documents/OpenCapif/test_invoker_certificate_folder", + "capif_callback_url": "http://localhost:5000", + "supported_features":"fffffff", + "check_authorization":{ + "ip":"", + "port":"" + }, + "cert_generation":{ + "csr_common_name": "Echeva", + "csr_organizational_unit": "discovery", + "csr_organization": "telefonica", + "csr_locality": "madrid", + "csr_state_or_province_name": "madrid", + "csr_country_name": "ES", + "csr_email_address": "adios@gmail.com" + }, + "discover_filter": { + "api-name": "", + "api-version": "", + "comm-type": "", + "protocol": "", + "aef-id": "", + "data-format": "", + "api-cat": "", + "preferred-aef-loc": "", + "req-api-prov-name": "", + "supported-features": "", + "api-supported-features": "", + "ue-ip-addr": "", + "service-kpis": "" + } + }, + "provider":{ + "provider_folder": "/Users/IDB0128/Documents/OpenCapif/test_provider_certificate_folder", + "cert_generation":{ + "csr_common_name": "provider", + "csr_organizational_unit": "discovery", + "csr_organization": "telefonica", + "csr_locality": "madrid", + "csr_state_or_province_name": "madrid", + "csr_country_name": "ES", + "csr_email_address": "hola@gmail.com" + }, + "apfs": "2", + "aefs": "3", + "publish_req": { + "service_api_id": "", + "publisher_apf_id": "", + "publisher_aefs_ids": [ + "", + "" + ] + }, + "api_description_path": "" + } +} diff --git a/test/network_app_provider_api_spec.json b/test/network_app_provider_api_spec.json new file mode 100755 index 0000000000000000000000000000000000000000..9672934e439809185e2165e97e9bb1dd8404a673 --- /dev/null +++ b/test/network_app_provider_api_spec.json @@ -0,0 +1,155 @@ +{ + "apiName": "Testtrece", + "aefProfiles": [ + { + "aefId": "AEF0a7db19a1968aeb46da269e6e307c5", + "versions": [ + { + "apiVersion": "v1", + "expiry": "2100-11-30T10:32:02.004Z", + "resources": [ + { + "resourceName": "MONITORING_SUBSCRIPTIONS", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/{scsAsId}/subscriptions", + "custOpName": "http_post", + "operations": [ + "GET", + "POST" + ], + "description": "Endpoint to manage monitoring subscriptions" + }, + { + "resourceName": "MONITORING_SUBSCRIPTION_SINGLE", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/{scsAsId}/subscriptions/{subscriptionId}", + "custOpName": "http_get", + "operations": [ + "GET", + "PUT", + "DELETE" + ], + "description": "Endpoint to manage single subscription" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "Custom operation for specific request" + } + ] + } + ], + "protocol": "HTTP_1_1", + "dataFormat": "JSON", + "securityMethods": [ + "Oauth", + "PSK" + ], + "interfaceDescriptions": [ + { + "ipv4Addr": "127.0.0.1", + "port": 8888, + "securityMethods": [ + "Oauth" + ] + } + ] + }, + { + "aefId": "AEFa3c0228d148f38c7171bfde164804e", + "versions": [ + { + "apiVersion": "v1", + "expiry": "2100-11-30T10:32:02.004Z", + "resources": [ + { + "resourceName": "TSN_LIST_PROFILES", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/profile", + "custOpName": "http_get", + "operations": [ + "GET" + ], + "description": "Endpoint for retrieving the list of available TSN profiles" + }, + { + "resourceName": "TSN_DETAIL_PROFILE", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/profile?name={profileName}", + "custOpName": "http_get", + "operations": [ + "GET" + ], + "description": "Endpoint for retrieving information about a single TSN profile" + }, + { + "resourceName": "TSN_APPLY_CONFIGURATION", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/apply", + "custOpName": "http_post", + "operations": [ + "POST" + ], + "description": "Endpoint for configuring TSN connection parameters" + }, + { + "resourceName": "TSN_CLEAR_CONFIGURATION", + "commType": "SUBSCRIBE_NOTIFY", + "uri": "/clear", + "custOpName": "http_post", + "operations": [ + "POST" + ], + "description": "Endpoint for removing a previous TSN connection configuration" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "Custom operation for specific request" + } + ] + } + ], + "protocol": "HTTP_1_1", + "dataFormat": "JSON", + "securityMethods": [ + "Oauth" + ], + "interfaceDescriptions": [ + { + "ipv4Addr": "127.0.0.1", + "port": 8899, + "securityMethods": [ + "Oauth" + ] + } + ] + } + ], + "description": "API of dummy Network-App to test", + "supportedFeatures": "fffff", + "shareableInfo": { + "isShareable": true, + "capifProvDoms": [ + "string" + ] + }, + "serviceAPICategory": "string", + "apiSuppFeats": "fffff", + "pubApiPath": { + "ccfIds": [ + "string" + ] + }, + "ccfId": "string" +} \ No newline at end of file diff --git a/test/network_app_provider_api_spec_2.json b/test/network_app_provider_api_spec_2.json new file mode 100755 index 0000000000000000000000000000000000000000..0d54904cfb24ee79280504484ebfc7529d4f9fd1 --- /dev/null +++ b/test/network_app_provider_api_spec_2.json @@ -0,0 +1,214 @@ +{ + "apiName": "Test-two", + "aefProfiles": [ + { + "aefId": "AEF71fd7e0328beb8863ec9d4eeef5a08", + "versions": [ + { + "apiVersion": "v1", + "expiry": "2100-11-30T10:32:02.004Z", + "resources": [ + { + "resourceName": "MONITORING_SUBSCRIPTIONS", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/{scsAsId}/subscriptions", + "custOpName": "http_post", + "operations": [ + "GET", + "POST" + ], + "description": "Endpoint to manage monitoring subscriptions" + }, + { + "resourceName": "MONITORING_SUBSCRIPTION_SINGLE", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/{scsAsId}/subscriptions/{subscriptionId}", + "custOpName": "http_get", + "operations": [ + "GET", + "PUT", + "DELETE" + ], + "description": "Endpoint to manage single subscription" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "string" + } + ] + } + ], + "protocol": "HTTP_1_1", + "dataFormat": "JSON", + "securityMethods": [ + "Oauth", + "PSK" + ], + "interfaceDescriptions": [ + { + "ipv4Addr": "127.0.0.1", + "port": 8888, + "securityMethods": [ + "Oauth" + ] + } + ] + }, + { + "aefId": "AEF2d5b96486ce2f2c46a353a9438ad38", + "versions": [ + { + "apiVersion": "v1", + "expiry": "2100-11-30T10:32:02.004Z", + "resources": [ + { + "resourceName": "TSN_LIST_PROFILES", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/profile", + "custOpName": "http_get", + "operations": [ + "GET" + ], + "description": "Endpoint for retrieving the list of available TSN profiles" + }, + { + "resourceName": "TSN_DETAIL_PROFILE", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/profile?name={profileName}", + "custOpName": "http_get", + "operations": [ + "GET" + ], + "description": "Endpoint for retrieving information about a single TSN profile" + }, + { + "resourceName": "TSN_APPLY_CONFIGURATION", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/apply", + "custOpName": "http_post", + "operations": [ + "POST" + ], + "description": "Endpoint for configuring TSN connection parameters" + }, + { + "resourceName": "TSN_CLEAR_CONFIGURATION", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/clear", + "custOpName": "http_post", + "operations": [ + "POST" + ], + "description": "Endpoint for removing a previous TSN connection configuration" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "string" + } + ] + } + ], + "protocol": "HTTP_1_1", + "dataFormat": "JSON", + "securityMethods": [ + "Oauth" + ], + "interfaceDescriptions": [ + { + "ipv4Addr": "127.0.0.1", + "port": 8899, + "securityMethods": [ + "Oauth" + ] + } + ] + }, + { + "aefId": "AEFbe2013357187c44a772b832d9d194e", + "versions": [ + { + "apiVersion": "v1", + "expiry": "2100-11-30T10:32:02.004Z", + "resources": [ + { + "resourceName": "MONITORING_SUBSCRIPTIONS", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/{scsAsId}/subscriptions", + "custOpName": "http_post", + "operations": [ + "GET", + "POST" + ], + "description": "Endpoint to manage monitoring subscriptions" + }, + { + "resourceName": "MONITORING_SUBSCRIPTION_SINGLE", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/{scsAsId}/subscriptions/{subscriptionId}", + "custOpName": "http_get", + "operations": [ + "GET", + "PUT", + "DELETE" + ], + "description": "Endpoint to manage single subscription" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "string" + } + ] + } + ], + "protocol": "HTTP_1_1", + "dataFormat": "JSON", + "securityMethods": [ + "Oauth", + "PSK" + ], + "interfaceDescriptions": [ + { + "ipv4Addr": "127.0.0.1", + "port": 8888, + "securityMethods": [ + "Oauth" + ] + } + ] + } + ], + "description": "API of dummy Network-App to test", + "supportedFeatures": "fffff", + "shareableInfo": { + "isShareable": true, + "capifProvDoms": [ + "string" + ] + }, + "serviceAPICategory": "string", + "apiSuppFeats": "fffff", + "pubApiPath": { + "ccfIds": [ + "string" + ] + }, + "ccfId": "string" +} \ No newline at end of file diff --git a/test/network_app_provider_api_spec_3.json b/test/network_app_provider_api_spec_3.json new file mode 100755 index 0000000000000000000000000000000000000000..548b7408262c54241bb0fb81466339410e41d6bc --- /dev/null +++ b/test/network_app_provider_api_spec_3.json @@ -0,0 +1,155 @@ +{ + "apiName": "Test-three", + "aefProfiles": [ + { + "aefId": "AEF67f10e46783f3e68356b4bb78c1cfc", + "versions": [ + { + "apiVersion": "v1", + "expiry": "2100-11-30T10:32:02.004Z", + "resources": [ + { + "resourceName": "MONITORING_SUBSCRIPTIONS", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/{scsAsId}/subscriptions", + "custOpName": "http_post", + "operations": [ + "GET", + "POST" + ], + "description": "Endpoint to manage monitoring subscriptions" + }, + { + "resourceName": "MONITORING_SUBSCRIPTION_SINGLE", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/{scsAsId}/subscriptions/{subscriptionId}", + "custOpName": "http_get", + "operations": [ + "GET", + "PUT", + "DELETE" + ], + "description": "Endpoint to manage single subscription" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "string" + } + ] + } + ], + "protocol": "HTTP_1_1", + "dataFormat": "JSON", + "securityMethods": [ + "Oauth", + "PSK" + ], + "interfaceDescriptions": [ + { + "ipv4Addr": "127.0.0.1", + "port": 8888, + "securityMethods": [ + "Oauth" + ] + } + ] + }, + { + "aefId": "AEF528e9c4c1918f3c10205104e4336b8", + "versions": [ + { + "apiVersion": "v1", + "expiry": "2100-11-30T10:32:02.004Z", + "resources": [ + { + "resourceName": "TSN_LIST_PROFILES", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/profile", + "custOpName": "http_get", + "operations": [ + "GET" + ], + "description": "Endpoint for retrieving the list of available TSN profiles" + }, + { + "resourceName": "TSN_DETAIL_PROFILE", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/profile?name={profileName}", + "custOpName": "http_get", + "operations": [ + "GET" + ], + "description": "Endpoint for retrieving information about a single TSN profile" + }, + { + "resourceName": "TSN_APPLY_CONFIGURATION", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/apply", + "custOpName": "http_post", + "operations": [ + "POST" + ], + "description": "Endpoint for configuring TSN connection parameters" + }, + { + "resourceName": "TSN_CLEAR_CONFIGURATION", + "commType": " SUBSCRIBE_NOTIFY", + "uri": "/clear", + "custOpName": "http_post", + "operations": [ + "POST" + ], + "description": "Endpoint for removing a previous TSN connection configuration" + } + ], + "custOperations": [ + { + "commType": "REQUEST_RESPONSE", + "custOpName": "string", + "operations": [ + "POST" + ], + "description": "string" + } + ] + } + ], + "protocol": "HTTP_1_1", + "dataFormat": "JSON", + "securityMethods": [ + "Oauth" + ], + "interfaceDescriptions": [ + { + "ipv4Addr": "127.0.0.1", + "port": 8899, + "securityMethods": [ + "Oauth" + ] + } + ] + } + ], + "description": "API of dummy Network-App to test", + "supportedFeatures": "fffff", + "shareableInfo": { + "isShareable": true, + "capifProvDoms": [ + "string" + ] + }, + "serviceAPICategory": "string", + "apiSuppFeats": "fffff", + "pubApiPath": { + "ccfIds": [ + "string" + ] + }, + "ccfId": "string" +} \ No newline at end of file diff --git a/test/test.py b/test/test.py new file mode 100644 index 0000000000000000000000000000000000000000..143558a062a5ad51983b22f43a89727f847a7e2c --- /dev/null +++ b/test/test.py @@ -0,0 +1,178 @@ + +import json +# flake8: noqa + +from opencapif_sdk import capif_invoker_connector, capif_provider_connector, service_discoverer + + +capif_sdk_config_path = "./capif_sdk_config_sample_test.json" + +def preparation_for_update(APFs, AEFs, second_network_app_api,capif_provider_connector): + + capif_provider_connector.apfs = APFs + capif_provider_connector.aefs = AEFs + if second_network_app_api: + capif_provider_connector.api_description_path = "./network_app_provider_api_spec_2.json" + else: + capif_provider_connector.api_description_path = "./network_app_provider_api_spec_3.json" + + return capif_provider_connector + +def ensure_update(chosen_apf, chosen_aefs, second_network_app_api,capif_provider_connector): + + if second_network_app_api: + # Get AEFs ids and APFs ids to publish an APi + + APF = capif_provider_connector.provider_capif_ids[chosen_apf] + AEF1 = capif_provider_connector.provider_capif_ids[chosen_aefs[0]] + AEF2 = capif_provider_connector.provider_capif_ids[chosen_aefs[1]] + AEF3 = capif_provider_connector.provider_capif_ids[chosen_aefs[2]] + + if not APF or not AEF1 or not AEF2: + raise ValueError("Not all necessary values were found in 'provider_service_ids.json'") + + # Update configuration file + capif_provider_connector.publish_req['publisher_apf_id'] = APF + capif_provider_connector.publish_req['publisher_aefs_ids'] = [AEF1, AEF2,AEF3] + + + else: + + APF = capif_provider_connector.provider_capif_ids['APF-1'] + AEF1 = capif_provider_connector.provider_capif_ids['AEF-1'] + AEF2 = capif_provider_connector.provider_capif_ids['AEF-2'] + + capif_provider_connector.publish_req['publisher_apf_id'] = APF + capif_provider_connector.publish_req['publisher_aefs_ids'] = [AEF1, AEF2] + + capif_provider_connector.publish_services() + + if second_network_app_api: + service_api_id = capif_provider_connector.provider_service_ids['Test-two'] + else: + service_api_id = capif_provider_connector.provider_service_ids['Test-three'] + + capif_provider_connector.publish_req['service_api_id'] = service_api_id + + capif_provider_connector.update_service() + + print("PROVIDER UPDATE SERVICE COMPLETED") + + capif_provider_connector.get_all_services() + + print("PROVIDER GET ALL SERVICES COMPLETED") + + capif_provider_connector.get_service() + + print("PROVIDER GET SERVICE COMPLETED") + + capif_provider_connector.unpublish_service() + + return capif_provider_connector + + +if __name__ == "__main__": + try: + # Initialization of the connector + capif_provider_connector = capif_provider_connector(config_file=capif_sdk_config_path) + + capif_provider_connector.onboard_provider() + print("PROVIDER ONBOARDING COMPLETED") + + # Get AEFs ids and APFs ids to publish an API + + + APF1 = capif_provider_connector.provider_capif_ids['APF-1'] + APF2 = capif_provider_connector.provider_capif_ids['APF-2'] + AEF1 = capif_provider_connector.provider_capif_ids['AEF-1'] + AEF2 = capif_provider_connector.provider_capif_ids['AEF-2'] + AEF3 = capif_provider_connector.provider_capif_ids['AEF-3'] + + capif_provider_connector.api_description_path="network_app_provider_api_spec.json" + # Update configuration file + capif_provider_connector.publish_req['publisher_apf_id'] = APF1 + capif_provider_connector.publish_req['publisher_aefs_ids'] = [AEF1, AEF2] + + capif_provider_connector.publish_services() + + print("PROVIDER PUBLISH COMPLETED") + + service_api_id = capif_provider_connector.provider_service_ids["Testtrece"] + + capif_provider_connector.publish_req['service_api_id'] = service_api_id + + capif_provider_connector.update_service() + + print("PROVIDER UPDATE COMPLETED") + + capif_provider_connector.get_all_services() + + print("PROVIDER GET ALL SERVICES COMPLETED") + + capif_provider_connector.get_service() + + print("PROVIDER GET SERVICE COMPLETED") + + capif_invoker_connector = capif_invoker_connector(config_file=capif_sdk_config_path) + + capif_invoker_connector.onboard_invoker() + print("INVOKER ONBOARDING COMPLETED") + + discoverer = service_discoverer(config_file=capif_sdk_config_path) + + discoverer.discover() + + print("SERVICE DISCOVER COMPLETED") + + discoverer.get_tokens() + + print("SERVICE GET TOKENS COMPLETED") + + capif_invoker_connector.update_invoker() + + print("INVOKER UPDATE SERVICE COMPLETED") + + capif_invoker_connector.offboard_invoker() + + print("INVOKER OFFBOARD COMPLETED") + + capif_provider_connector.unpublish_service() + + print("PROVIDER UNPUBLISH SERVICE COMPLETED") + + capif_provider_connector = preparation_for_update(2, 4, True,capif_provider_connector) + + capif_provider_connector.update_provider() + + chosen_apf = "APF-2" + + chosen_aefs = ["AEF-1", "AEF-3", "AEF-4"] + + capif_provider_connector = ensure_update(chosen_apf, chosen_aefs, True,capif_provider_connector) + + print("PROVIDER UPDATE ONE COMPLETED") + + capif_provider_connector = preparation_for_update(1, 2, False,capif_provider_connector) + + capif_provider_connector.update_provider() + + chosen_apf = "APF-1" + + chosen_aefs = ["AEF-1", "AEF-2"] + + capif_provider_connector = ensure_update(chosen_apf, chosen_aefs, False,capif_provider_connector) + + print("PROVIDER UPDATE TWO COMPLETED") + + capif_provider_connector.offboard_provider() + + print("PROVIDER OFFBOARDING COMPLETED") + + print("ALL TESTS PASSED CORRECTLY") + + except FileNotFoundError as e: + print(f"Error: {e}") + except json.JSONDecodeError as e: + print(f"Error reading the JSON file: {e}") + except Exception as e: + print(f"Unexpected error: {e}")