From 3c8de187cc10220ab56e47be519ce7f8ffefd228 Mon Sep 17 00:00:00 2001 From: Bruno Garcia Date: Mon, 22 Mar 2021 11:01:29 +0100 Subject: [PATCH 1/6] Add Crypt class --- Helper/crypt.py | 21 +++++++++++++++++++++ README.md | 4 ++-- REST/dispatcherApi.py | 3 +-- 3 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 Helper/crypt.py diff --git a/Helper/crypt.py b/Helper/crypt.py new file mode 100644 index 0000000..7291bf4 --- /dev/null +++ b/Helper/crypt.py @@ -0,0 +1,21 @@ +import jwt +from typing import List, Tuple + + +class Crypt: + def __init__(self, secret: str): + self.secret = secret + + def Encode(self, target: int, executions: List[int]) -> str: + """'target' is the landing experiment execution, 'executions' is + the list of all executions belonging to the user""" + payload = {"t": target, "l": executions} + token = jwt.encode(payload, self.secret, algorithm="HS256") + if isinstance(token, bytes): # Older versions of jwt return bytes + token = token.decode(encoding="UTF-8") + return token + + def Decode(self, token: str) -> Tuple[int, List[int]]: + """Returns a tuple (, )""" + payload = jwt.decode(token, self.secret, algorithm="HS256") + return payload["t"], payload["l"] diff --git a/README.md b/README.md index cfcade6..668afef 100644 --- a/README.md +++ b/README.md @@ -67,8 +67,8 @@ In order to test that the connections with the Dispatcher and ELCM are working p is working properly. > If you do not see any messages, check the `Logging` section of the configuration file (`config.yml`). Ensure that > the levels are set to `DEBUG` or `INFO` -1. Open the Portal using a web browser. -2. Register a new user (top right, `Register` tab). If no errors are reported after pressing the `Register` button at +2. Open the Portal using a web browser. +3. Register a new user (top right, `Register` tab). If no errors are reported after pressing the `Register` button at the bottom then the connection with the Dispatcher is working properly. > Note that newly registered users are not "active", and cannot log in to the Portal until their registration has > been validated by the platform administrator(s). For information about the user activation procedure refer to the diff --git a/REST/dispatcherApi.py b/REST/dispatcherApi.py index a8a1011..fccb2da 100644 --- a/REST/dispatcherApi.py +++ b/REST/dispatcherApi.py @@ -9,7 +9,6 @@ from os.path import split - class VimInfo: def __init__(self, data): self.Name = data['name'] @@ -146,7 +145,7 @@ def GetAvailableVnfds(self, user: User) -> Tuple[List[str], Optional[str]]: return data if error is None else [], error def GetAvailableNsds(self, user: User) -> Tuple[List[str], Optional[str]]: - data, error = self.basicGet(user, '/mano/nsd', f"list of VNFDs") # type: Dict, Optional[str] + data, error = self.basicGet(user, '/mano/nsd', f"list of NSDs") # type: Dict, Optional[str] return data if error is None else [], error def handleErrorcodes(self, code: int, data: Dict, overrides: Dict[int, str] = None) -> str: From df31800b7d0851176c27c14e70830694ee905930 Mon Sep 17 00:00:00 2001 From: Bruno Garcia Date: Mon, 22 Mar 2021 12:36:18 +0100 Subject: [PATCH 2/6] Add Analytics configuration --- Helper/__init__.py | 1 + Helper/config.py | 38 ++++++++++++++++++++++++++++++-------- Helper/crypt.py | 2 +- Helper/defaultConfig | 6 +++++- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/Helper/__init__.py b/Helper/__init__.py index 6877394..e9f1e48 100644 --- a/Helper/__init__.py +++ b/Helper/__init__.py @@ -3,3 +3,4 @@ from .child import Child from .action_handler import ActionHandler, Action from .facility import Facility +from .crypt import Crypt diff --git a/Helper/config.py b/Helper/config.py index 081f71b..8a67bbf 100644 --- a/Helper/config.py +++ b/Helper/config.py @@ -7,7 +7,7 @@ class hostPort: def __init__(self, data: Dict, key: str): - self.data = data[key] + self.data = data.get(key, {}) @property def Host(self): @@ -22,6 +22,15 @@ def Url(self): return f"{self.Host}:{self.Port}/" +class possiblyEnabled: + def __init__(self, data: Dict, key: str): + self.data = data[key] + + @property + def Enabled(self) -> bool: + return self.data.get("Enabled", False) + + class Dispatcher(hostPort): def __init__(self, data: Dict): super().__init__(data, 'Dispatcher') @@ -58,13 +67,9 @@ def LogLevel(self): return self.toLogLevel(self.data['LogLevel']) -class EastWest: +class EastWest(possiblyEnabled): def __init__(self, data: Dict): - self.data = data.get('EastWest', {}) - - @property - def Enabled(self) -> bool: - return self.data.get('Enabled', False) + super().__init__(data, 'EastWest') @property def Remotes(self) -> Dict[str, Dict[str, Union[int, str]]]: @@ -89,6 +94,19 @@ def RemoteApi(self, name: str): return None +class Analytics(possiblyEnabled): + def __init__(self, data: Dict): + super().__init__(data, 'Analytics') + + @property + def Url(self) -> Optional[str]: + return self.data.get("URL", None) + + @property + def Secret(self) -> Optional[str]: + return self.data.get("Secret", None) + + class Config: FILENAME = 'config.yml' FILENAME_NOTICES = 'notices.yml' @@ -154,4 +172,8 @@ def Logging(self) -> Logging: @property def EastWest(self) -> EastWest: - return EastWest(self.data) \ No newline at end of file + return EastWest(self.data) + + @property + def Analytics(self) -> Analytics: + return Analytics(self.data) diff --git a/Helper/crypt.py b/Helper/crypt.py index 7291bf4..e67c1a0 100644 --- a/Helper/crypt.py +++ b/Helper/crypt.py @@ -17,5 +17,5 @@ def Encode(self, target: int, executions: List[int]) -> str: def Decode(self, token: str) -> Tuple[int, List[int]]: """Returns a tuple (, )""" - payload = jwt.decode(token, self.secret, algorithm="HS256") + payload = jwt.decode(token, self.secret, algorithms=["HS256"]) return payload["t"], payload["l"] diff --git a/Helper/defaultConfig b/Helper/defaultConfig index b6ffdfd..6bdfed4 100644 --- a/Helper/defaultConfig +++ b/Helper/defaultConfig @@ -15,4 +15,8 @@ PlatformDescriptionPage: platform.html Description: 5th Generation End-to-end Network, Experimentation, System Integration, and Showcasing EastWest: Enabled: False - Remotes: {} # One key for each remote Portal, each key containing 'Host' and 'Port' values \ No newline at end of file + Remotes: {} # One key for each remote Portal, each key containing 'Host' and 'Port' values +Analytics: + Enabled: False + URL: # External (Internet) URL of the Analytics + Secret: # Secret key shared with the Analytics Portal, in order to secure the created URLs \ No newline at end of file From a481aff4b41d2f8788fe47fea544f7e3c5ffd5ec Mon Sep 17 00:00:00 2001 From: Bruno Garcia Date: Mon, 22 Mar 2021 12:36:48 +0100 Subject: [PATCH 3/6] Add Analytics URL creation --- REST/__init__.py | 1 + REST/analyticsApi.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 REST/analyticsApi.py diff --git a/REST/__init__.py b/REST/__init__.py index d276dfb..e2f9e07 100644 --- a/REST/__init__.py +++ b/REST/__init__.py @@ -2,3 +2,4 @@ from .elcmApi import ElcmApi from .dispatcherApi import DispatcherApi, VimInfo from .remoteApi import RemoteApi +from .analyticsApi import AnalyticsApi diff --git a/REST/analyticsApi.py b/REST/analyticsApi.py new file mode 100644 index 0000000..ea567d1 --- /dev/null +++ b/REST/analyticsApi.py @@ -0,0 +1,31 @@ +from Helper import Crypt, Config +from app.models import User, Experiment +from typing import List + + +class AnalyticsApi: + crypt = None + url = None + + def __init__(self): + if AnalyticsApi.crypt is None: + config = Config().Analytics + if config.Enabled: + AnalyticsApi.url = config.Url + AnalyticsApi.crypt = Crypt(config.Secret) if config.Secret is not None else None + + def GetToken(self, target: int, user: User) -> str: + if self.crypt is not None: + experiments: List[Experiment] = list(user.Experiments) + executions = [] + for experiment in experiments: + executions.extend(experiment.experimentExecutions()) + + # TODO: Consider filtering the list with `execution.status == "Finished"` + executions = sorted([execution.id for execution in executions]) + return self.crypt.Encode(target, executions) + else: + return "" + + def GetUrl(self, target: int, user: User) -> str: + return f"{self.url}/endpoint?token={self.GetToken(target, user)}" From 4f479e4605c5d030384bf3306876c54f0b23fd2d Mon Sep 17 00:00:00 2001 From: Bruno Garcia Date: Mon, 22 Mar 2021 12:50:36 +0100 Subject: [PATCH 4/6] Add link to Analytics module to executions --- app/execution/routes.py | 5 +++-- app/experiment/routes.py | 9 +++++++-- app/templates/execution/_execution.html | 3 +++ app/templates/execution/execution.html | 4 ++++ 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/app/execution/routes.py b/app/execution/routes.py index 9197f9c..f6c9168 100644 --- a/app/execution/routes.py +++ b/app/execution/routes.py @@ -5,7 +5,7 @@ from app.execution import bp from app.models import Experiment, Execution from Helper import Config, LogInfo, Log -from REST import DispatcherApi, ElcmApi +from REST import DispatcherApi, ElcmApi, AnalyticsApi @bp.route('/', methods=['GET']) @@ -32,6 +32,7 @@ def _responseToLogList(response): if status == 'Success': localLogs = _responseToLogList(localResponse) remoteLogs = None + analyticsUrl = AnalyticsApi().GetUrl(experiment.id, current_user) if experiment.remoteDescriptor is not None: success = False @@ -52,7 +53,7 @@ def _responseToLogList(response): execution=execution, localLogs=localLogs, remoteLogs=remoteLogs, experiment=experiment, grafanaUrl=config.GrafanaUrl, executionId=getLastExecution() + 1, - dispatcherUrl=config.ELCM.Url, # TODO: Use dispatcher + dispatcherUrl=config.ELCM.Url, analyticsUrl=analyticsUrl, ewEnabled=Config().EastWest.Enabled) else: if status == 'Not Found': diff --git a/app/experiment/routes.py b/app/experiment/routes.py index 966e2f2..4d795b1 100644 --- a/app/experiment/routes.py +++ b/app/experiment/routes.py @@ -3,7 +3,7 @@ from flask import render_template, flash, redirect, url_for, request, jsonify, abort from flask.json import loads as jsonParse from flask_login import current_user, login_required -from REST import ElcmApi, DispatcherApi +from REST import ElcmApi, DispatcherApi, AnalyticsApi from app import db from app.experiment import bp from app.models import Experiment, Execution, Action, NetworkService @@ -284,10 +284,15 @@ def experiment(experimentId: int): flash(f'The experiment {exp.name} doesn\'t have any executions yet', 'info') return redirect(url_for('main.index')) else: + analyticsApi = AnalyticsApi() + analyticsUrls = {} + for execution in executions: + analyticsUrls[execution.id] = analyticsApi.GetToken(execution.id, current_user) + return render_template('experiment/experiment.html', title=f'Experiment: {exp.name}', experiment=exp, executions=executions, formRun=formRun, grafanaUrl=config.GrafanaUrl, executionId=getLastExecution() + 1, - dispatcherUrl=config.ELCM.Url, # TODO: Use dispatcher + dispatcherUrl=config.ELCM.Url, analyticsUrls=analyticsUrls, ewEnabled=Config().EastWest.Enabled) else: Log.I(f'Forbidden - User {current_user.username} don\'t have permission to access experiment {experimentId}') diff --git a/app/templates/execution/_execution.html b/app/templates/execution/_execution.html index fa060d0..a6b2b42 100644 --- a/app/templates/execution/_execution.html +++ b/app/templates/execution/_execution.html @@ -38,6 +38,9 @@ 📊 + 📈 + 💾 diff --git a/app/templates/execution/execution.html b/app/templates/execution/execution.html index d85f25d..3e31b31 100644 --- a/app/templates/execution/execution.html +++ b/app/templates/execution/execution.html @@ -58,6 +58,7 @@ {% endmacro %} {% block app_content %} +

Execution {{ execution.id }}

@@ -111,6 +112,9 @@

Execution {{ execution.id }}

📊 + 📈 + 💾 From 08d901ec5818f771ca5bb68f603a404574c22de5 Mon Sep 17 00:00:00 2001 From: Bruno Garcia Date: Thu, 22 Apr 2021 08:42:26 +0200 Subject: [PATCH 5/6] Fix button activation, urls --- app/execution/routes.py | 4 ++-- app/experiment/routes.py | 2 +- app/templates/execution/_execution.html | 2 +- app/templates/execution/execution.html | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/execution/routes.py b/app/execution/routes.py index f6c9168..44a1209 100644 --- a/app/execution/routes.py +++ b/app/execution/routes.py @@ -8,7 +8,7 @@ from REST import DispatcherApi, ElcmApi, AnalyticsApi -@bp.route('/', methods=['GET']) +@bp.route('/', methods=['GET']) @login_required def execution(executionId: int): def _responseToLogList(response): @@ -32,7 +32,7 @@ def _responseToLogList(response): if status == 'Success': localLogs = _responseToLogList(localResponse) remoteLogs = None - analyticsUrl = AnalyticsApi().GetUrl(experiment.id, current_user) + analyticsUrl = AnalyticsApi().GetUrl(executionId, current_user) if experiment.remoteDescriptor is not None: success = False diff --git a/app/experiment/routes.py b/app/experiment/routes.py index 4d795b1..aa63a7a 100644 --- a/app/experiment/routes.py +++ b/app/experiment/routes.py @@ -287,7 +287,7 @@ def experiment(experimentId: int): analyticsApi = AnalyticsApi() analyticsUrls = {} for execution in executions: - analyticsUrls[execution.id] = analyticsApi.GetToken(execution.id, current_user) + analyticsUrls[execution.id] = analyticsApi.GetUrl(execution.id, current_user) return render_template('experiment/experiment.html', title=f'Experiment: {exp.name}', experiment=exp, executions=executions, formRun=formRun, grafanaUrl=config.GrafanaUrl, diff --git a/app/templates/execution/_execution.html b/app/templates/execution/_execution.html index a6b2b42..86486ba 100644 --- a/app/templates/execution/_execution.html +++ b/app/templates/execution/_execution.html @@ -38,7 +38,7 @@ 📊 - 📈 Execution {{ execution.id }} 📊 - 📈 Date: Thu, 22 Apr 2021 08:55:17 +0200 Subject: [PATCH 6/6] Update documentation --- CHANGELOG.md | 4 ++++ Helper/defaultConfig | 4 ++-- README.md | 5 +++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc8d4fb..027dc04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +**22/04/2021** [Version 2.4.2] + + - Analytics Dashboard integration + **12/01/2021** [Version 2.4.1] - Updated documentation diff --git a/Helper/defaultConfig b/Helper/defaultConfig index 6bdfed4..93325ef 100644 --- a/Helper/defaultConfig +++ b/Helper/defaultConfig @@ -18,5 +18,5 @@ EastWest: Remotes: {} # One key for each remote Portal, each key containing 'Host' and 'Port' values Analytics: Enabled: False - URL: # External (Internet) URL of the Analytics - Secret: # Secret key shared with the Analytics Portal, in order to secure the created URLs \ No newline at end of file + URL: /dash # External URL of the Analytics Dashboard + Secret: # Secret key shared with the Analytics Dashboard, used in order to create secure URLs \ No newline at end of file diff --git a/README.md b/README.md index 668afef..5851e8f 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ ### Optional integrations: - [Grafana](https://grafana.com/) (tested with version 5.4) + - [Analytics Dashboard](https://github.com/5genesis/Analytics) (Release B) ## Deployment @@ -103,6 +104,10 @@ The Portal instance can be configured by editing the `config.yml` file. - Remotes: Dictionary containing the connection configuration for each remote platform's Portal, with each key containing 'Host' and 'Port' values in the same format as in the `ELCM` section. Defaults to an empty dictionary (`{}`). +- Analytics: + - Enabled: Boolean value indicating if the Analytics Dashboard is available. Defaults to `False`. + - URL: External URL of the Analytics Dashboard + - Secret: Secret key shared with the Analytics Dashboard, used in order to create secure URLs #### Portal notices