Commit 1437fb90 authored by Ville Hallivuori's avatar Ville Hallivuori
Browse files

WebUI for creating XR services

parent dda9a413
Loading
Loading
Loading
Loading
+12 −0
Original line number Original line Diff line number Diff line
@@ -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": [
+179 −3
Original line number Original line Diff line number Diff line
@@ -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():
        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():
    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():
    #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:
+105 −0
Original line number Original line Diff line number Diff line
<!--
 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 %}
+12 −0
Original line number Original line Diff line number Diff line
@@ -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>