Skip to content
Snippets Groups Projects
Commit f82c518d authored by Lluis Gifre Renom's avatar Lluis Gifre Renom
Browse files

Merge branch 'feat/xr_service_creation_web_ui' into 'develop'

WebUI for creating XR services

See merge request !68
parents ce84889d 1437fb90
No related branches found
No related tags found
2 merge requests!142Release TeraFlowSDN 2.1,!68WebUI for creating XR services
...@@ -96,6 +96,18 @@ ...@@ -96,6 +96,18 @@
"device_operational_status": 1, "device_operational_status": 1,
"device_drivers": [6], "device_drivers": [6],
"device_endpoints": [] "device_endpoints": []
},
{
"device_id": {"device_uuid": {"uuid": "X2-XR-CONSTELLATION"}},
"device_type": "xr-constellation",
"device_config": {"config_rules": [
{"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "172.19.219.44"}},
{"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "443"}},
{"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": "{\"username\": \"xr-user-1\", \"password\": \"xr-user-1\", \"hub_module_name\": \"XR HUB 2\", \"consistency-mode\": \"lifecycle\"}"}}
]},
"device_operational_status": 1,
"device_drivers": [6],
"device_endpoints": []
} }
], ],
"links": [ "links": [
......
...@@ -12,27 +12,56 @@ ...@@ -12,27 +12,56 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from contextlib import contextmanager
import json
import grpc import grpc
from flask import current_app, redirect, render_template, Blueprint, flash, session, url_for from collections import defaultdict
from flask import current_app, redirect, render_template, Blueprint, flash, session, url_for, request
from common.proto.context_pb2 import ( from common.proto.context_pb2 import (
IsolationLevelEnum, Service, ServiceId, ServiceTypeEnum, ServiceStatusEnum, Connection) IsolationLevelEnum, Service, ServiceId, ServiceTypeEnum, ServiceStatusEnum, Connection, Empty, DeviceDriverEnum, ConfigActionEnum, Device, DeviceList)
from common.tools.context_queries.Context import get_context from common.tools.context_queries.Context import get_context
from common.tools.context_queries.Topology import get_topology
from common.tools.context_queries.EndPoint import get_endpoint_names from common.tools.context_queries.EndPoint import get_endpoint_names
from common.tools.context_queries.Service import get_service from common.tools.context_queries.Service import get_service
from context.client.ContextClient import ContextClient from context.client.ContextClient import ContextClient
from service.client.ServiceClient import ServiceClient from service.client.ServiceClient import ServiceClient
from typing import Optional, Set
from common.tools.object_factory.Topology import json_topology_id
from common.tools.object_factory.ConfigRule import json_config_rule_set
from common.tools.object_factory.Context import json_context_id
service = Blueprint('service', __name__, url_prefix='/service') service = Blueprint('service', __name__, url_prefix='/service')
context_client = ContextClient() context_client = ContextClient()
service_client = ServiceClient() service_client = ServiceClient()
@contextmanager
def connected_client(c):
try:
c.connect()
yield c
finally:
c.close()
# Context client must be in connected state when calling this function
def get_device_drivers_in_use(topology_uuid: str, context_uuid: str) -> Set[str]:
active_drivers = set()
grpc_topology = get_topology(context_client, topology_uuid, context_uuid=context_uuid, rw_copy=False)
topo_device_uuids = {device_id.device_uuid.uuid for device_id in grpc_topology.device_ids}
grpc_devices: DeviceList = context_client.ListDevices(Empty())
for device in grpc_devices.devices:
if device.device_id.device_uuid.uuid in topo_device_uuids:
for driver in device.device_drivers:
active_drivers.add(DeviceDriverEnum.Name(driver))
return active_drivers
@service.get('/') @service.get('/')
def home(): def home():
if 'context_uuid' not in session or 'topology_uuid' not in session: if 'context_uuid' not in session or 'topology_uuid' not in session:
flash("Please select a context!", "warning") flash("Please select a context!", "warning")
return redirect(url_for("main.home")) return redirect(url_for("main.home"))
context_uuid = session['context_uuid'] context_uuid = session['context_uuid']
topology_uuid = session['topology_uuid']
context_client.connect() context_client.connect()
...@@ -44,10 +73,12 @@ def home(): ...@@ -44,10 +73,12 @@ def home():
try: try:
services = context_client.ListServices(context_obj.context_id) services = context_client.ListServices(context_obj.context_id)
services = services.services services = services.services
active_drivers = get_device_drivers_in_use(topology_uuid, context_uuid)
except grpc.RpcError as e: except grpc.RpcError as e:
if e.code() != grpc.StatusCode.NOT_FOUND: raise if e.code() != grpc.StatusCode.NOT_FOUND: raise
if e.details() != 'Context({:s}) not found'.format(context_uuid): raise if e.details() != 'Context({:s}) not found'.format(context_uuid): raise
services, device_names, endpoints_data = list(), dict(), dict() services, device_names, endpoints_data = list(), dict(), dict()
active_drivers = set()
else: else:
endpoint_ids = list() endpoint_ids = list()
for service_ in services: for service_ in services:
...@@ -57,7 +88,7 @@ def home(): ...@@ -57,7 +88,7 @@ def home():
context_client.close() context_client.close()
return render_template( return render_template(
'service/home.html', services=services, device_names=device_names, endpoints_data=endpoints_data, 'service/home.html', services=services, device_names=device_names, endpoints_data=endpoints_data,
ste=ServiceTypeEnum, sse=ServiceStatusEnum) ste=ServiceTypeEnum, sse=ServiceStatusEnum, active_drivers=active_drivers)
@service.route('add', methods=['GET', 'POST']) @service.route('add', methods=['GET', 'POST'])
...@@ -67,6 +98,151 @@ def add(): ...@@ -67,6 +98,151 @@ def add():
#return render_template('service/home.html') #return render_template('service/home.html')
def get_hub_module_name(dev: Device) -> Optional[str]:
for cr in dev.device_config.config_rules:
if cr.action == ConfigActionEnum.CONFIGACTION_SET and cr.custom and cr.custom.resource_key == "_connect/settings":
try:
cr_dict = json.loads(cr.custom.resource_value)
if "hub_module_name" in cr_dict:
return cr_dict["hub_module_name"]
except json.JSONDecodeError:
pass
return None
@service.route('add-xr', methods=['GET', 'POST'])
def add_xr():
### FIXME: copypaste
if 'context_uuid' not in session or 'topology_uuid' not in session:
flash("Please select a context!", "warning")
return redirect(url_for("main.home"))
context_uuid = session['context_uuid']
topology_uuid = session['topology_uuid']
context_client.connect()
grpc_topology = get_topology(context_client, topology_uuid, context_uuid=context_uuid, rw_copy=False)
if grpc_topology is None:
flash('Context({:s})/Topology({:s}) not found'.format(str(context_uuid), str(topology_uuid)), 'danger')
return redirect(url_for("main.home"))
else:
topo_device_uuids = {device_id.device_uuid.uuid for device_id in grpc_topology.device_ids}
grpc_devices= context_client.ListDevices(Empty())
devices = [
device for device in grpc_devices.devices
if device.device_id.device_uuid.uuid in topo_device_uuids and DeviceDriverEnum.DEVICEDRIVER_XR in device.device_drivers
]
devices.sort(key=lambda dev: dev.name)
hub_interfaces_by_device = defaultdict(list)
leaf_interfaces_by_device = defaultdict(list)
constellation_name_to_uuid = {}
dev_ep_to_uuid = {}
ep_uuid_to_name = {}
for d in devices:
constellation_name_to_uuid[d.name] = d.device_id.device_uuid.uuid
hm_name = get_hub_module_name(d)
if hm_name is not None:
hm_if_prefix= hm_name + "|"
for ep in d.device_endpoints:
dev_ep_to_uuid[(d.name, ep.name)] = ep.endpoint_id.endpoint_uuid.uuid
if ep.name.startswith(hm_if_prefix):
hub_interfaces_by_device[d.name].append(ep.name)
else:
leaf_interfaces_by_device[d.name].append(ep.name)
ep_uuid_to_name[ep.endpoint_id.endpoint_uuid.uuid] = (d.name, ep.name)
hub_interfaces_by_device[d.name].sort()
leaf_interfaces_by_device[d.name].sort()
# Find out what endpoints are already used so that they can be disabled
# in the create screen
context_obj = get_context(context_client, context_uuid, rw_copy=False)
if context_obj is None:
flash('Context({:s}) not found'.format(str(context_uuid)), 'danger')
return redirect(request.url)
services = context_client.ListServices(context_obj.context_id)
ep_used_by={}
for service in services.services:
if service.service_type == ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE:
for ep in service.service_endpoint_ids:
ep_uuid = ep.endpoint_uuid.uuid
if ep_uuid in ep_uuid_to_name:
dev_name, ep_name = ep_uuid_to_name[ep_uuid]
ep_used_by[f"{ep_name}@{dev_name}"] = service.name
context_client.close()
if request.method != 'POST':
return render_template('service/add-xr.html', devices=devices, hub_if=hub_interfaces_by_device, leaf_if=leaf_interfaces_by_device, ep_used_by=ep_used_by)
else:
service_name = request.form["service_name"]
if service_name == "":
flash(f"Service name must be specified", 'danger')
constellation = request.form["constellation"]
constellation_uuid = constellation_name_to_uuid.get(constellation, None)
if constellation_uuid is None:
flash(f"Invalid constellation \"{constellation}\"", 'danger')
hub_if = request.form["hubif"]
hub_if_uuid = dev_ep_to_uuid.get((constellation, hub_if), None)
if hub_if_uuid is None:
flash(f"Invalid hub interface \"{hub_if}\"", 'danger')
leaf_if = request.form["leafif"]
leaf_if_uuid = dev_ep_to_uuid.get((constellation, leaf_if), None)
if leaf_if_uuid is None:
flash(f"Invalid leaf interface \"{leaf_if}\"", 'danger')
if service_name == "" or constellation_uuid is None or hub_if_uuid is None or leaf_if_uuid is None:
return redirect(request.url)
json_context_uuid=json_context_id(context_uuid)
sr = {
"name": service_name,
"service_id": {
"context_id": {"context_uuid": {"uuid": context_uuid}},
"service_uuid": {"uuid": service_name}
},
'service_type' : ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE,
"service_endpoint_ids": [
{'device_id': {'device_uuid': {'uuid': constellation_uuid}}, 'endpoint_uuid': {'uuid': hub_if_uuid}, 'topology_id': json_topology_id("admin", context_id=json_context_uuid)},
{'device_id': {'device_uuid': {'uuid': constellation_uuid}}, 'endpoint_uuid': {'uuid': leaf_if_uuid}, 'topology_id': json_topology_id("admin", context_id=json_context_uuid)}
],
'service_status' : {'service_status': ServiceStatusEnum.SERVICESTATUS_PLANNED},
'service_constraints' : [],
}
json_tapi_settings = {
'capacity_value' : 50.0,
'capacity_unit' : 'GHz',
'layer_proto_name': 'PHOTONIC_MEDIA',
'layer_proto_qual': 'tapi-photonic-media:PHOTONIC_LAYER_QUALIFIER_NMC',
'direction' : 'UNIDIRECTIONAL',
}
config_rule = json_config_rule_set('/settings', json_tapi_settings)
with connected_client(service_client) as sc:
endpoints, sr['service_endpoint_ids'] = sr['service_endpoint_ids'], []
try:
create_response = sc.CreateService(Service(**sr))
except Exception as e:
flash(f'Failure to update service name {service_name} with endpoints and configuration, exception {str(e)}', 'danger')
return redirect(request.url)
sr['service_endpoint_ids'] = endpoints
sr['service_config'] = {'config_rules': [config_rule]}
try:
update_response = sc.UpdateService(Service(**sr))
flash(f'Created service {update_response.service_uuid.uuid}', 'success')
except Exception as e:
flash(f'Failure to update service {create_response.service_uuid.uuid} with endpoints and configuration, exception {str(e)}', 'danger')
return redirect(request.url)
return redirect(url_for('service.home'))
@service.get('<path:service_uuid>/detail') @service.get('<path:service_uuid>/detail')
def detail(service_uuid: str): def detail(service_uuid: str):
if 'context_uuid' not in session or 'topology_uuid' not in session: if 'context_uuid' not in session or 'topology_uuid' not in session:
......
<!--
Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
{% extends 'base.html' %}
{% block content %}
<script>
var js_hub_if = JSON.parse('{{hub_if | tojson | safe}}');
var js_leaf_if = JSON.parse('{{leaf_if | tojson | safe}}');
var js_ep_used_by = JSON.parse('{{ep_used_by | tojson | safe}}');
function clear_select_except_first(s) {
while (s.options.length > 1) {
s.remove(1);
}
}
function add_ep_to_select(sel, dev_name, ep_name) {
used_by = js_ep_used_by[ep_name + "@" + dev_name];
var o;
if (used_by === undefined) {
o = new Option(ep_name, ep_name)
} else {
o = new Option(ep_name + " (used by " + used_by + ")", ep_name)
o.disabled=true
}
sel.add(o);
}
function constellationSelected() {
const constellation_select = document.getElementById('constellation');
const hubif_select = document.getElementById('hubif');
const leafif_select = document.getElementById('leafif');
clear_select_except_first(hubif_select)
clear_select_except_first(leafif_select)
if (constellation_select.value) {
const hub_ifs=js_hub_if[constellation_select.value]
for (const hi of hub_ifs) {
add_ep_to_select(hubif_select, constellation_select.value, hi);
}
const leaf_ifs=js_leaf_if[constellation_select.value]
for (const li of leaf_ifs) {
add_ep_to_select(leafif_select, constellation_select.value, li);
}
}
}
</script>
<h1>Add XR Service</h1>
<form action="#" method="post">
<fieldset class="form-group row mb-3">
<label for="service_name" class="col-sm-3 col-form-label">Service name:</label>
<div class="col-sm-9">
<input type="text" id="service_name" name="service_name" class="form-control">
</div>
</fieldset>
<fieldset class="form-group row mb-3">
<label for="constellation" class="col-sm-3 col-form-label">Constellation:</label>
<div class="col-sm-9">
<select name="constellation" id="constellation" onchange="constellationSelected()" class="form-select">
<option value="">(choose constellation)</option>
{% for dev in devices %}
<option value="{{dev.name}}">{{dev.name}}</option>
{% endfor %}
</select>
</div>
</fieldset>
<fieldset class="form-group row mb-3">
<label for="hubif" class="col-sm-3 col-form-label">Hub Endpoint:</label>
<div class="col-sm-9">
<select name="hubif" id="hubif" class="col-sm-8 form-select">
<option value="">(choose hub endpoint)</option>
</select>
</div>
</fieldset>
<fieldset class="form-group row mb-3">
<label for="leafif" class="col-sm-3 col-form-label">Leaf Endpoint:</label>
<div class="col-sm-9">
<select name="leafif" id="leafif" class="col-sm-8 form-select">
<option value="">(choose leaf endpoint)</option>
</select>
</div>
</fieldset>
<input type="submit" class="btn btn-primary" value="Create">
</form>
{% endblock %}
...@@ -26,6 +26,18 @@ ...@@ -26,6 +26,18 @@
Add New Service Add New Service
</a> </a>
</div> --> </div> -->
<!-- Only display XR service addition button if there are XR constellations. Otherwise it might confuse
user, as other service types do not have GUI to add service yet. -->
{% if "DEVICEDRIVER_XR" in active_drivers %}
<div class="col">
<a href="{{ url_for('service.add_xr') }}" class="btn btn-primary" style="margin-bottom: 10px;">
<i class="bi bi-plus"></i>
Add New XR Service
</a>
</div>
{% endif %}
<div class="col"> <div class="col">
{{ services | length }} services found in context <i>{{ session['context_uuid'] }}</i> {{ services | length }} services found in context <i>{{ session['context_uuid'] }}</i>
</div> </div>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment