Skip to content

Commit

Permalink
Merge pull request #4 from EVOLVED-5G/logging
Browse files Browse the repository at this point in the history
Logging
  • Loading branch information
NaniteBased authored May 15, 2023
2 parents a683320 + 6cf9cda commit 6ae8b28
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 124 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
**15/05/2023** [Version 1.2.0]
- Implement access logs
- EVOLVED-5G SDK v1.0.4 integration

**31/03/2023** [Version 1.1.0]
- Implement Authorization

Expand Down
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ not supported"`. To enable a backend, specify the URL where the TSN backend is l

### CAPIF integration

The TSN FrontEnd can optionally work with CAPIF, both for publishing the API and for securing access to the endpoints. This
behavior is controlled by the `Enabled` and `SecurityEnabled` entries in the `CAPIF` section of `config.json`. These
two settings are set to `true` by default.
The TSN FrontEnd can optionally work with CAPIF for publishing the API, as well as for securing and logging access to the endpoints.
This behavior is controlled by the `Enabled`, `SecurityEnabled` and `LoggingEnabled` entries in the `CAPIF` section of `config.json`.
These settings are set to `true` by default.

> By default, this configuration points to the `capifcore` domain, which is also the default for CAPIF deployments.
> Unless the CAPIF domain can be resolved by external DNS, this value should be configured on the `hosts` file of the
Expand All @@ -60,7 +60,7 @@ two settings are set to `true` by default.
Upon a successful publishing process, the following file will be created `capif_data/publisherDetails.txt`. This
file contains the username and password used during the registration (randomly generated) and the host and port
configured for the FrontEnd. In the case of a Docker deployment, this file can be checked using the following command
in the host machine:
in the host machine (or using the `check.sh` script):

```
docker exec TSN_FrontEnd bash -c "cat ./capif_data/publisherDetails.txt"
Expand All @@ -74,12 +74,16 @@ header, with the Bearer provided by CAPIF.
> If this setting is enabled, but for some reason the TSN FrontEnd was not able to register the API through CAPIF, or to
> retrieve the CAPIF public key, the server will immediately abort the execution.
Additionally, if the `LoggingEnabled` parameter is set to `true` a record of every access attempt to the endpoints, including
input parameters as well as the returned response, will be recorded in CAPIF. In all cases this information is recorded
in the `access.log` file, in the root folder of the TSN FrontEnd.

## Deployment

### Docker container

This repository contains 3 files (`Dockerfile`, `build.sh`, `run.sh`) prepared for supporting the deployment of the
TSF FrontEnd as a Docker container. The deployment procedure is as follows:
This repository contains 4 files (`Dockerfile`, `build.sh`, `run.sh`, `check.sh`) prepared for supporting the deployment
of the TSF FrontEnd as a Docker container. The deployment procedure is as follows:

1. Clone this repository. The environment must already have an installation of Docker (tested on version 20.10.7).
2. Include any necessary profile information in the `Profiles` sub-folder. `Profiles/sample` can be used as a guide.
Expand All @@ -90,6 +94,7 @@ Profiles must have the `yml` extension to be read by the TSN FrontEnd.
- CAPIF is configured in the `capifcore` domain, with ports 8080 (HTTP) and 443 (HTTPS)
4. Execute `build.sh`. This file will prepare a Docker image (tagged `tsn_frontend`).
5. Execute `run.sh`. This will create a new container (named `TSN_FrontEnd`) based on the previously generated image.
6. Optionally, execute `check.sh`, in order to confirm if some errors have been recorded and the CAPIF publisher details.

> Note that the build process will create a copy of the files in the `Profiles` sub-folder and `config.json`. If these
> files are edited after the creation of the image, this process (starting from step 4) must be executed again.
Expand All @@ -112,7 +117,7 @@ that the values for `<HOST>` and `<PORT>` match those configured in `config.json
> Changes made to the `Profiles` folder and `config.json` will be reflected in the TSN FrontEnd after restarting the server.
>
> In order to force a new CAPIF publishing process, manually delete the `capif_data/publisherDetails.txt` file
> previously generated.
> previously generated. Also consider removing `access.log` and `error.log`, to avoid misunderstandings.
## Endpoints

Expand Down
5 changes: 3 additions & 2 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from flask import Flask, render_template
from flask_misaka import Misaka
from back_end import ConfigurationHandler
from capif import maybePublishApi
from capif import CapifHandler
import json

app = Flask(__name__)
Expand All @@ -17,7 +17,8 @@
readme = file.read()

if app.config["CAPIF_ENABLED"]:
maybePublicKey = maybePublishApi(config)
CapifHandler.Initialize(config)
maybePublicKey = CapifHandler.MaybePublishApi()

if app.config["CAPIF_SECURITY_ENABLED"]:
if maybePublicKey is None:
Expand Down
190 changes: 123 additions & 67 deletions capif.py
Original file line number Diff line number Diff line change
@@ -1,77 +1,133 @@
from evolved5g.sdk import CAPIFProviderConnector
from evolved5g.sdk import CAPIFProviderConnector, CAPIFLogger
from os.path import exists, join, dirname, abspath
from random import choice
from string import ascii_letters
from requests.exceptions import RequestException
from typing import Dict


def maybePublishApi(config: Dict) -> [None | str]:
class CapifHandler:
detailsFile = './capif_data/publisherDetails.txt'
capif = config['CAPIF']
baseFolder = abspath(join(dirname(__file__), 'capif_data'))

if exists(detailsFile): # Publish the API through CAPIF only once
print("API already registered.")
else:
user = "tsn_" + ''.join(choice(ascii_letters) for _ in range(6))
password = ''.join(choice(ascii_letters) for _ in range(6))

host = config['FrontEnd']['Host']
port = config['FrontEnd']['Port']

with open(join(baseFolder, 'tsn_af_api.Template'), 'r', encoding='utf-8') as template:
with open(join(baseFolder, 'tsn_af_api.json'), 'w', encoding='utf-8') as output:
for line in template:
output.write(line
.replace('<<HOST>>', f'"{host}"')
.replace('<<PORT>>', str(port)))

capif_connector = CAPIFProviderConnector(certificates_folder=baseFolder,
capif_host=capif['Host'],
capif_http_port=capif['HttpPort'],
capif_https_port=capif['HttpsPort'],
capif_netapp_username=user,
capif_netapp_password=password,
description="TSN AF API",
csr_common_name="tsnAfExposer",
csr_organizational_unit="test_app_ou",
csr_organization="test_app_o",
crs_locality="Madrid",
csr_state_or_province_name="Madrid",
csr_country_name="ES",
csr_email_address="test@example.com"
)
try:
capif_connector.register_and_onboard_provider()

capif_connector.publish_services(
service_api_description_json_full_path=abspath(join(baseFolder, 'tsn_af_api.json')))

with open(detailsFile, 'w', encoding="utf-8") as output:
output.write(f'API registered with the following details:\n')
output.write(f' User: {user}\n')
output.write(f' Password: {password}\n')
output.write(f' Host: {host}\n')
output.write(f' Port: {port}\n')

print("API registered in CAPIF")
except RequestException as e:
print(f'Unable to publish API. Exception: {e}')

if capif["SecurityEnabled"]:
certPath = join(baseFolder, 'capif_cert_server.pem')
if not exists(certPath):
print("Certificate file not found")
return None
initialized = False
frontEndHost = frontEndPort = None
host = httpPort = httpsPort = None
securityEnabled = loggingEnabled = None

capifLogger = apiId = None

@classmethod
def Initialize(cls, config: Dict):
cls.frontEndHost = config['FrontEnd']['Host']
cls.frontEndPort = config['FrontEnd']['Port']

capif = config['CAPIF']
cls.host = capif['Host']
cls.httpPort = capif['HttpPort']
cls.httpsPort = capif['HttpsPort']
cls.securityEnabled = capif['SecurityEnabled']
cls.loggingEnabled = capif['LoggingEnabled']

cls.initialized = True

@classmethod
def MaybePublishApi(cls) -> [None | str]:
if not cls.initialized:
raise RuntimeError("CapifHandler must be initialized before calling this method.")

if exists(cls.detailsFile): # Publish the API through CAPIF only once
print("API already registered.")
else:
user = "tsn_" + ''.join(choice(ascii_letters) for _ in range(6))
password = ''.join(choice(ascii_letters) for _ in range(6))

with open(join(cls.baseFolder, 'tsn_af_api.Template'), 'r', encoding='utf-8') as template:
with open(join(cls.baseFolder, 'tsn_af_api.json'), 'w', encoding='utf-8') as output:
for line in template:
output.write(line
.replace('<<HOST>>', f'"{cls.frontEndHost}"')
.replace('<<PORT>>', str(cls.frontEndPort)))

capif_connector = CAPIFProviderConnector(certificates_folder=cls.baseFolder,
capif_host=cls.host,
capif_http_port=cls.httpPort,
capif_https_port=cls.httpsPort,
capif_netapp_username=user,
capif_netapp_password=password,
description="TSN AF API",
csr_common_name="tsnAfExposer",
csr_organizational_unit="test_app_ou",
csr_organization="test_app_o",
crs_locality="Madrid",
csr_state_or_province_name="Madrid",
csr_country_name="ES",
csr_email_address="test@example.com"
)
try:
capif_connector.register_and_onboard_provider()

capif_connector.publish_services(
service_api_description_json_full_path=abspath(join(cls.baseFolder, 'tsn_af_api.json')))

with open(cls.detailsFile, 'w', encoding="utf-8") as output:
output.write(f'API registered with the following details:\n')
output.write(f' User: {user}\n')
output.write(f' Password: {password}\n')
output.write(f' Host: {cls.frontEndHost}\n')
output.write(f' Port: {cls.frontEndPort}\n')

print("API registered in CAPIF")
except RequestException as e:
with open("error.log", "a") as err:
err.write(f"Unable to publish API: '{e}'\n")

if cls.securityEnabled:
certPath = join(cls.baseFolder, 'capif_cert_server.pem')
if not exists(certPath):
with open("error.log", "a") as err:
err.write(f"Certificate file (capif_cert_server.pem) not found.\n")
return None
else:
with open(certPath, 'rb') as certFile:
certificate = certFile.read()

from OpenSSL import crypto
crypt = crypto.load_certificate(crypto.FILETYPE_PEM, certificate)
publicKey = crypt.get_pubkey()
return crypto.dump_publickey(crypto.FILETYPE_PEM, publicKey)
else:
with open(certPath, 'rb') as certFile:
certificate = certFile.read()

from OpenSSL import crypto
crypt = crypto.load_certificate(crypto.FILETYPE_PEM, certificate)
publicKey = crypt.get_pubkey()
return crypto.dump_publickey(crypto.FILETYPE_PEM, publicKey)
else:
print("Security is disabled.")
return None
print("Security is disabled.")
return None

@classmethod
def MaybeLog(cls, invokerId, resource, uri, method, time, payload, response, code):
if not cls.initialized:
raise RuntimeError("CapifHandler must be initialized before calling this method.")

if cls.loggingEnabled:
if cls.apiId is None:
try:
cls.capifLogger = CAPIFLogger(certificates_folder=cls.baseFolder,
capif_host=cls.host, capif_https_port=str(cls.httpsPort))
serviceDescription = cls.capifLogger.get_capif_service_description(
capif_service_api_description_json_full_path=join(cls.baseFolder, "CAPIF_tsn_af_api.json"))
cls.apiId = serviceDescription["apiId"]
except Exception as e:
with open("error.log", "a") as err:
err.write(f"Unable to retrieve apiId: '{e}'\n")
return

entry = cls.capifLogger.LogEntry(
apiId=cls.apiId, apiVersion='v1', apiName='/tsn/api/',
resourceName=resource, uri=uri, protocol='HTTP_1_1',
invocationTime=time, invocationLatency=10, operation=method,
result=str(code), inputParameters=payload, outputParameters=response
)

try:
cls.capifLogger.save_log(invokerId, [entry])
except Exception as e:
with open("error.log", "a") as err:
err.write(f"Unable to send log to CAPIF: '{e}'\n")
err.write(f"Entry: {entry}\n\n")

4 changes: 2 additions & 2 deletions capif_data/tsn_af_api.Template
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@
],
"protocol": "HTTP_1_1",
"dataFormat": "JSON",
"securityMethods": ["Oauth"],
"securityMethods": ["OAUTH"],
"interfaceDescriptions": [
{
"ipv4Addr": <<HOST>>,
"port": <<PORT>>,
"securityMethods": ["Oauth"]
"securityMethods": ["OAUTH"]
}
]
}
Expand Down
8 changes: 8 additions & 0 deletions check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
echo "Errors:"
echo "--------------------------------------------------------------------------------"
docker exec TSN_FrontEnd bash -c "cat ./error.log"
echo ""
echo "Publisher details:"
echo "--------------------------------------------------------------------------------"
docker exec TSN_FrontEnd bash -c "cat ./capif_data/publisherDetails.txt"
echo "[END]"
1 change: 1 addition & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"CAPIF": {
"Enabled": true,
"SecurityEnabled": true,
"LoggingEnabled": true,
"Host": "capifcore",
"HttpPort": 8080,
"HttpsPort": 443
Expand Down
Loading

0 comments on commit 6ae8b28

Please sign in to comment.