Skip to content

Commit

Permalink
Merge pull request #6 from 5genesis/analytics
Browse files Browse the repository at this point in the history
Analytics
  • Loading branch information
NaniteBased authored Apr 22, 2021
2 parents 17b5536 + 0b9a92e commit f0a1e1a
Show file tree
Hide file tree
Showing 13 changed files with 119 additions and 18 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
**22/04/2021** [Version 2.4.2]

- Analytics Dashboard integration

**12/01/2021** [Version 2.4.1]

- Updated documentation
Expand Down
1 change: 1 addition & 0 deletions Helper/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
from .child import Child
from .action_handler import ActionHandler, Action
from .facility import Facility
from .crypt import Crypt
38 changes: 30 additions & 8 deletions Helper/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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')
Expand Down Expand Up @@ -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]]]:
Expand All @@ -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'
Expand Down Expand Up @@ -154,4 +172,8 @@ def Logging(self) -> Logging:

@property
def EastWest(self) -> EastWest:
return EastWest(self.data)
return EastWest(self.data)

@property
def Analytics(self) -> Analytics:
return Analytics(self.data)
21 changes: 21 additions & 0 deletions Helper/crypt.py
Original file line number Diff line number Diff line change
@@ -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 (<landing execution>, <list of executions>)"""
payload = jwt.decode(token, self.secret, algorithms=["HS256"])
return payload["t"], payload["l"]
6 changes: 5 additions & 1 deletion Helper/defaultConfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Remotes: {} # One key for each remote Portal, each key containing 'Host' and 'Port' values
Analytics:
Enabled: False
URL: <Internet address>/dash # External URL of the Analytics Dashboard
Secret: # Secret key shared with the Analytics Dashboard, used in order to create secure URLs
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -67,8 +68,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
Expand Down Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions REST/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
from .elcmApi import ElcmApi
from .dispatcherApi import DispatcherApi, VimInfo
from .remoteApi import RemoteApi
from .analyticsApi import AnalyticsApi
31 changes: 31 additions & 0 deletions REST/analyticsApi.py
Original file line number Diff line number Diff line change
@@ -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 "<Analytics disabled or no Secret>"

def GetUrl(self, target: int, user: User) -> str:
return f"{self.url}/endpoint?token={self.GetToken(target, user)}"
3 changes: 1 addition & 2 deletions REST/dispatcherApi.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from os.path import split



class VimInfo:
def __init__(self, data):
self.Name = data['name']
Expand Down Expand Up @@ -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:
Expand Down
7 changes: 4 additions & 3 deletions app/execution/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
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('/<executionId>', methods=['GET'])
@bp.route('/<int:executionId>', methods=['GET'])
@login_required
def execution(executionId: int):
def _responseToLogList(response):
Expand All @@ -32,6 +32,7 @@ def _responseToLogList(response):
if status == 'Success':
localLogs = _responseToLogList(localResponse)
remoteLogs = None
analyticsUrl = AnalyticsApi().GetUrl(executionId, current_user)

if experiment.remoteDescriptor is not None:
success = False
Expand All @@ -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':
Expand Down
9 changes: 7 additions & 2 deletions app/experiment/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.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,
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}')
Expand Down
3 changes: 3 additions & 0 deletions app/templates/execution/_execution.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
<a class="btn btn-blue btn-sm {{ "" if hasDashboard else "disabled"}}"
href="{{ grafanaUrl }}{{ execution.dashboard_url }}" role="button" target="_blank">📊</a>

<a class="btn btn-blue btn-sm {{ "" if finished else "disabled"}}"
href="{{ analyticsUrls[execution.id] }}" role="button" target="_blank">📈</a>

<a class="btn btn-blue btn-sm {{ "" if finished else "disabled"}}"
href="http://{{ dispatcherUrl }}execution/{{ execution.id }}/results" role="button">💾</a>
</td>
Expand Down
4 changes: 4 additions & 0 deletions app/templates/execution/execution.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
{% endmacro %}

{% block app_content %}

<div style="text-align: center">
<h2>Execution {{ execution.id }}</h2>
</div>
Expand Down Expand Up @@ -111,6 +112,9 @@ <h2>Execution {{ execution.id }}</h2>
<a class="btn btn-blue btn-sm {{ "" if hasDashboard else "disabled"}}"
href="{{ grafanaUrl }}{{ execution.dashboard_url }}" role="button" target="_blank">📊</a>

<a class="btn btn-blue btn-sm {{ "" if finished else "disabled"}}"
href="{{ analyticsUrl }}" role="button" target="_blank">📈</a>

<a class="btn btn-blue btn-sm {{ "" if finished else "disabled"}}"
href="http://{{ dispatcherUrl }}execution/{{ execution.id }}/results" role="button">💾</a>
</td>
Expand Down

0 comments on commit f0a1e1a

Please sign in to comment.