Commit 6ece8d0c authored by Lluis Gifre Renom's avatar Lluis Gifre Renom
Browse files

Merge branch 'feat/webui' into 'develop'

WebUI:

See merge request !40
parents 693b55bc d3aa64a2
Loading
Loading
Loading
Loading
+9 −5
Original line number Diff line number Diff line
@@ -14,19 +14,23 @@

# for development purposes only

K8S_NAMESPACE=${K8S_NAMESPACE:-'tf-dev'}
# K8S_NAMESPACE=${K8S_NAMESPACE:-'tf-dev'}

export CONTEXTSERVICE_SERVICE_HOST=`kubectl get service/contextservice -n ${K8S_NAMESPACE} -o jsonpath='{.spec.clusterIP}'`
# export CONTEXTSERVICE_SERVICE_HOST=`kubectl get service/contextservice -n ${K8S_NAMESPACE} -o jsonpath='{.spec.clusterIP}'`

echo Context IP: $CONTEXTSERVICE_SERVICE_HOST
# echo Context IP: $CONTEXTSERVICE_SERVICE_HOST

export DEVICESERVICE_SERVICE_HOST=`kubectl get service/deviceservice -n ${K8S_NAMESPACE} -o jsonpath='{.spec.clusterIP}'`
# export DEVICESERVICE_SERVICE_HOST=`kubectl get service/deviceservice -n ${K8S_NAMESPACE} -o jsonpath='{.spec.clusterIP}'`

echo Device IP: $DEVICESERVICE_SERVICE_HOST
# echo Device IP: $DEVICESERVICE_SERVICE_HOST

source tfs_runtime_env_vars.sh

export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION='python'
export HOST="127.0.0.1"
export HOSTNAME="test"
export FLASK_ENV="development"
export LOG_LEVEL="DEBUG"

# python3 -m webbrowser http://${HOST}:8004

+26 −22
Original line number Diff line number Diff line
@@ -14,41 +14,45 @@

# external imports
from flask_wtf import FlaskForm
from wtforms import StringField, SelectField, TextAreaField, SubmitField
from wtforms import StringField, SelectField, TextAreaField, SubmitField, BooleanField, Form
from wtforms.validators import DataRequired, Length, NumberRange, Regexp, ValidationError
from common.proto.context_pb2 import DeviceDriverEnum, DeviceOperationalStatusEnum
from common.proto.context_pb2 import DeviceOperationalStatusEnum
from webui.utils.form_validators import key_value_validator

class AddDeviceForm(FlaskForm):
    device_id = StringField('ID', 
                           validators=[DataRequired(), Length(min=5)])
    device_type = StringField('Type', 
                           validators=[DataRequired(), Length(min=5)])
    device_config = TextAreaField('Configurations', validators=[key_value_validator()])
    device_type = SelectField('Type', choices = [])                                                     
    operational_status = SelectField('Operational Status',
                        #    choices=[(-1, 'Select...'), (0, 'Undefined'), (1, 'Disabled'), (2, 'Enabled')],
                           coerce=int,
                           validators=[NumberRange(min=0)])
    device_drivers = TextAreaField('Drivers', validators=[DataRequired(), Regexp(r'^\d+(,\d+)*$')])
    device_drivers_undefined = BooleanField('UNDEFINED')
    device_drivers_openconfig = BooleanField('OPENCONFIG')
    device_drivers_transport_api = BooleanField('TRANSPORT_API')
    device_drivers_p4 = BooleanField('P4')
    device_drivers_ietf_network_topology = BooleanField('IETF_NETWORK_TOPOLOGY')
    device_drivers_onf_tr_352 = BooleanField('ONF_TR_352')
    device_drivers_xr = BooleanField('XR')
    device_config_address = StringField('connect/address',default='127.0.0.1',validators=[DataRequired(), Length(min=5)])
    device_config_port = StringField('connect/port',default='0',validators=[DataRequired(), Length(min=1)])
    device_config_settings = TextAreaField('connect/settings',default='{}',validators=[DataRequired(), Length(min=2)])
    submit = SubmitField('Add')

    def validate_operational_status(form, field):
        if field.data not in DeviceOperationalStatusEnum.DESCRIPTOR.values_by_number:
            raise ValidationError('The operational status value selected is incorrect!')

    def validate_device_drivers(form, field):
        if ',' not in field.data:
            data = str(field.data) + ','
        else:
            data = field.data
        for value in data.split(','):
            value = value.strip()
            if len(value) == 0:
                continue
            try:
                value_int = int(value)
            except:
                raise ValidationError(f'The value "{value}" is not a valid driver identified.')
            if value_int not in DeviceDriverEnum.DESCRIPTOR.values_by_number:
                values = ', '.join([str(x) for x in DeviceDriverEnum.DESCRIPTOR.values_by_number])
                raise ValidationError(f'The device driver {value_int} is not correct. Allowed values are: {values}.')
class ConfigForm(FlaskForm):
    device_key_config = StringField('Key configuration')
    device_value_config = StringField('Value configuration')    
    submit = SubmitField('Add')


class UpdateDeviceForm(FlaskForm):
    update_operational_status = SelectField('Operational Status',
                           choices=[(-1, 'Select...'), (0, 'Undefined'), (1, 'Disabled'), (2, 'Enabled')],
                           coerce=int,
                           validators=[NumberRange(min=0)])
                        
    submit = SubmitField('Update')
+117 −34
Original line number Diff line number Diff line
@@ -16,12 +16,16 @@ from flask import current_app, render_template, Blueprint, flash, session, redir
from common.proto.context_pb2 import (
    ConfigActionEnum, ConfigRule,
    Device, DeviceDriverEnum, DeviceId, DeviceList, DeviceOperationalStatusEnum,
    Empty, TopologyId)
    Empty, TopologyId, ContextId)
from common.tools.object_factory.Context import json_context_id
from common.tools.object_factory.Topology import json_topology_id
from common.tools.context_queries.Device import add_device_to_topology
from context.client.ContextClient import ContextClient
from device.client.DeviceClient import DeviceClient
from webui.service.device.forms import AddDeviceForm
from common.DeviceTypes import DeviceTypeEnum
from webui.service.device.forms import ConfigForm
from webui.service.device.forms import UpdateDeviceForm

device = Blueprint('device', __name__, url_prefix='/device')
context_client = ContextClient()
@@ -57,60 +61,75 @@ def add():
    form = AddDeviceForm()

    # listing enum values
    form.operational_status.choices = [(-1, 'Select...')]
    form.operational_status.choices = []
    for key, value in DeviceOperationalStatusEnum.DESCRIPTOR.values_by_name.items():
        form.operational_status.choices.append(
            (DeviceOperationalStatusEnum.Value(key), key.replace('DEVICEOPERATIONALSTATUS_', '')))

    # device driver ids
    device_driver_ids = []
    for key in DeviceDriverEnum.DESCRIPTOR.values_by_name:
        device_driver_ids.append(f"{DeviceDriverEnum.Value(key)}={key.replace('DEVICEDRIVER_', '')}")
    device_driver_ids = ', '.join(device_driver_ids)
    # items for Device Type field
    for device_type in DeviceTypeEnum:
        form.device_type.choices.append((device_type.value,device_type.value))    

    if form.validate_on_submit():
        device = Device()
        # Device UUID: 
        device.device_id.device_uuid.uuid = form.device_id.data
        device.device_type = form.device_type.data
        if '\n' not in form.device_config.data:
            data = form.device_config.data.strip() + '\n'
        else:
            data = form.device_config.data.strip()
        
        for config in data.split('\n'):
            if len(config.strip()) > 0:
                parts = config.strip().split('=')
                config_rule: ConfigRule = ConfigRule()

        # Device type: 
        device.device_type = str(form.device_type.data)

        # Device configurations: 
        config_rule = device.device_config.config_rules.add()
        config_rule.action = ConfigActionEnum.CONFIGACTION_SET
        config_rule.custom.resource_key = '_connect/address'
        config_rule.custom.resource_value = form.device_config_address.data

        config_rule = device.device_config.config_rules.add()
        config_rule.action = ConfigActionEnum.CONFIGACTION_SET
                config_rule.custom.resource_key = parts[0].strip()
                config_rule.custom.resource_value = parts[1].strip()
                device.device_config.config_rules.append(config_rule)
        config_rule.custom.resource_key = '_connect/port'
        config_rule.custom.resource_value = form.device_config_port.data

        config_rule = device.device_config.config_rules.add()
        config_rule.action = ConfigActionEnum.CONFIGACTION_SET
        config_rule.custom.resource_key = '_connect/settings'
        config_rule.custom.resource_value = form.device_config_settings.data

        # Device status: 
        device.device_operational_status = form.operational_status.data

        if ',' not in form.device_drivers.data:
            data = form.device_drivers.data.strip() + ','
        else:
            data = form.device_drivers.data.strip()
        # Device drivers: 
        if form.device_drivers_undefined.data:
            device.device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_UNDEFINED)
        if form.device_drivers_openconfig.data:
            device.device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_OPENCONFIG)
        if form.device_drivers_transport_api.data:
            device.device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_TRANSPORT_API)
        if form.device_drivers_p4.data:
            device.device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_P4)
        if form.device_drivers_ietf_network_topology.data:
            device.device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_IETF_NETWORK_TOPOLOGY)
        if form.device_drivers_onf_tr_352.data:
            device.device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_ONF_TR_352)
        if form.device_drivers_xr.data:
            device.device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_XR)

        for driver in data.split(','):
            driver = driver.strip()
            if len(driver) == 0:
                continue
            device.device_drivers.append(int(driver))
        try:
            device_client.connect()
            response: DeviceId = device_client.AddDevice(device)
            device_client.close()

            context_uuid = session['context_uuid']
            topology_uuid = session['topology_uuid']
            context_client.connect()
            context_id = ContextId(**json_context_id(context_uuid))
            add_device_to_topology(context_client, context_id, topology_uuid, device.device_id.device_uuid.uuid)
            context_client.close()
            flash(f'New device was created with ID "{response.device_uuid.uuid}".', 'success')
            return redirect(url_for('device.home'))
        except Exception as e:
            flash(f'Problem adding the device. {e.details()}', 'danger')
        
    return render_template('device/add.html', form=form,
                    submit_text='Add New Device',
                    device_driver_ids=device_driver_ids)
                        submit_text='Add New Device')

@device.route('detail/<path:device_uuid>', methods=['GET', 'POST'])
def detail(device_uuid: str):
@@ -144,3 +163,67 @@ def delete(device_uuid):
        flash(f'Problem deleting device "{device_uuid}": {e.details()}', 'danger')
        current_app.logger.exception(e)
    return redirect(url_for('device.home'))

@device.route('<path:device_uuid>/addconfig', methods=['GET', 'POST'])
def addconfig(device_uuid):
    form = ConfigForm()
    request = DeviceId()
    request.device_uuid.uuid = device_uuid
    context_client.connect()
    response = context_client.GetDevice(request)
    context_client.close()

    if form.validate_on_submit():
        device = Device()
        device.CopyFrom(response)
        config_rule = device.device_config.config_rules.add()
        config_rule.action = ConfigActionEnum.CONFIGACTION_SET
        config_rule.custom.resource_key = form.device_key_config.data
        config_rule.custom.resource_value = form.device_value_config.data
        try:
            device_client.connect()
            response: DeviceId = device_client.ConfigureDevice(device)
            device_client.close()
            flash(f'New configuration was created with ID "{response.device_uuid.uuid}".', 'success')
            return redirect(url_for('device.home'))
        except Exception as e:
             flash(f'Problem adding the device. {e.details()}', 'danger')

    return render_template('device/addconfig.html', form=form,  submit_text='Add New Configuration')

@device.route('updateconfig', methods=['GET', 'POST'])
def updateconfig():

        
    return render_template('device/updateconfig.html')


@device.route('<path:device_uuid>/update', methods=['GET', 'POST'])
def update(device_uuid):
    form = UpdateDeviceForm()
    request = DeviceId()
    request.device_uuid.uuid = device_uuid
    context_client.connect()
    response = context_client.GetDevice(request)
    context_client.close()

    # listing enum values
    form.update_operational_status.choices = []
    for key, value in DeviceOperationalStatusEnum.DESCRIPTOR.values_by_name.items():
        form.update_operational_status.choices.append((DeviceOperationalStatusEnum.Value(key), key.replace('DEVICEOPERATIONALSTATUS_', '')))

    form.update_operational_status.default = response.device_operational_status

    if form.validate_on_submit():
        device = Device()
        device.CopyFrom(response)
        device.device_operational_status = form.update_operational_status.data
        try:
            device_client.connect()
            response: DeviceId = device_client.ConfigureDevice(device)
            device_client.close()
            flash(f'Status of device with ID "{response.device_uuid.uuid}" was updated.', 'success')
            return redirect(url_for('device.home'))
        except Exception as e:
             flash(f'Problem updating the device. {e.details()}', 'danger')  
    return render_template('device/update.html', device=response, form=form, submit_text='Update Device')
+130 −85
Original line number Diff line number Diff line
@@ -18,7 +18,7 @@

{% block content %}
<h1>Add New Device</h1>

<br />
<form id="add_device" method="POST">
    {{ form.hidden_tag() }}
    <fieldset>
@@ -37,6 +37,7 @@
                {% endif %}
            </div>
        </div>
        <br />
        <div class="row mb-3">
            {{ form.device_type.label(class="col-sm-2 col-form-label") }}
            <div class="col-sm-10">
@@ -48,10 +49,11 @@
                    {% endfor %}
                </div>
                {% else %}
                        {{ form.device_type(class="form-control") }}
                {{ form.device_type(class="form-select")}}
                {% endif %}
            </div>
        </div>
        <br />
        <div class="row mb-3">
            {{ form.operational_status.label(class="col-sm-2 col-form-label") }}
            <div class="col-sm-10">
@@ -63,44 +65,87 @@
                    {% endfor %}
                </div>
                {% else %}
                        {{ form.operational_status(class="form-control") }}
                {{ form.operational_status(class="form-select") }}
                {% endif %}
            </div>
        </div>
        <br />
        <div class="row mb-3">
            <div class="col-sm-2 col-form-label">Drivers</div>
            <div class="col-sm-10">
                {% if form.device_drivers_undefined.errors %}
                {{ form.device_drivers_undefined(class="form-control is-invalid") }}
                <div class="invalid-feedback">
                    {% for error in form.device_drivers_undefined.errors %}
                    <span>{{ error }}</span>
                    {% endfor %}
                </div>
                {% else %}
                {{ form.device_drivers_undefined }} {{ form.device_drivers_undefined.label(class="col-sm-3
                col-form-label") }}
                {{ form.device_drivers_openconfig }} {{ form.device_drivers_openconfig.label(class="col-sm-3
                col-form-label") }}
                {{ form.device_drivers_transport_api }} {{ form.device_drivers_transport_api.label(class="col-sm-3
                col-form-label") }}
                <br />{{ form.device_drivers_p4 }} {{ form.device_drivers_p4.label(class="col-sm-3 col-form-label") }}
                {{ form.device_drivers_ietf_network_topology }} {{
                form.device_drivers_ietf_network_topology.label(class="col-sm-3
                col-form-label") }}
                {{ form.device_drivers_onf_tr_352 }} {{ form.device_drivers_onf_tr_352.label(class="col-sm-3
                col-form-label") }}<br />
                {{ form.device_drivers_xr }} {{ form.device_drivers_xr.label(class="col-sm-3
                col-form-label") }}
                {% endif %}
            </div>
        </div>
        <br />
        Configuration Rules <br />
        <div class="row mb-3">
                {{ form.device_config.label(class="col-sm-2 col-form-label") }}
            {{ form.device_config_address.label(class="col-sm-2 col-form-label") }}
            <div class="col-sm-10">
                  {% if form.device_config.errors %}
                        {{ form.device_config(class="form-control is-invalid", rows=5) }}
                {% if form.device_config_address.errors %}
                {{ form.device_config_address(class="form-control is-invalid", rows=5) }}
                <div class="invalid-feedback">
                            {% for error in form.device_config.errors %}
                    {% for error in form.device_config_address.errors %}
                    <span>{{ error }}</span>
                    {% endfor %}
                </div>
                {% else %}
                        {{ form.device_config(class="form-control", rows=5) }}
                {{ form.device_config_address(class="form-control", rows=5) }}
                {% endif %}
            </div>
                <div id="device_config_help" class="form-text">The device configurations should follow a <i>key=value</i> format, one configuration per line.</div>
        </div>
        <div class="row mb-3">
                {{ form.device_drivers.label(class="col-sm-2 col-form-label") }}
            {{ form.device_config_port.label(class="col-sm-2 col-form-label") }}
            <div class="col-sm-10">
                  {% if form.device_drivers.errors %}
                        {{ form.device_drivers(class="form-control is-invalid", rows=5) }}
                {% if form.device_config_port.errors %}
                {{ form.device_config_port(class="form-control is-invalid", rows=5) }}
                <div class="invalid-feedback">
                            {% for error in form.device_drivers.errors %}
                    {% for error in form.device_config_port.errors %}
                    <span>{{ error }}</span>
                    {% endfor %}
                </div>
                {% else %}
                        {{ form.device_drivers(class="form-control", rows=5) }}
                {{ form.device_config_port(class="form-control", rows=5) }}
                {% endif %}
            </div>
                <div id="device_drivers_help" class="form-text">
                    List the device drivers by their numerical ID, separated by commas, without spaces between them. Numerical IDs: {{ device_driver_ids }}.
        </div>
        <div class="row mb-3">
            {{ form.device_config_settings.label(class="col-sm-2 col-form-label") }}
            <div class="col-sm-10">
                {% if form.device_config_settings.errors %}
                {{ form.device_config_settings(class="form-control is-invalid", rows=5) }}
                <div class="invalid-feedback">
                    {% for error in form.device_config_settings.errors %}
                    <span>{{ error }}</span>
                    {% endfor %}
                </div>
                {% else %}
                {{ form.device_config_settings(class="form-control", rows=5) }}
                {% endif %}
            </div>
        </div>
        <br />
        <div class="d-grid gap-2 d-md-flex justify-content-md-start">
            <button type="submit" class="btn btn-primary">
                <i class="bi bi-plus-circle-fill"></i>
+69 −0
Original line number Diff line number Diff line
<!--
 Copyright 2021-2023 H2020 TeraFlow (https://www.teraflow-h2020.eu/)

 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 %}
<h1>Add New Configuration</h1>
<br />
<form id="add_device" method="POST">
    {{ form.hidden_tag() }}
    <fieldset>
        <div class="row mb-3">
            {{ form.device_key_config.label(class="col-sm-2 col-form-label") }}
            <div class="col-sm-10">
                {% if form.device_key_config.errors %}
                {{ form.device_key_config(class="form-control is-invalid") }}
                <div class="invalid-feedback">
                    {% for error in form.device_key_config.errors %}
                    <span>{{ error }}</span>
                    {% endfor %}
                </div>
                {% else %}
                {{ form.device_key_config(class="form-control") }}
                {% endif %}
            </div>
        </div>
        <br />
        <div class="row mb-3">
            {{ form.device_value_config.label(class="col-sm-2 col-form-label") }}
            <div class="col-sm-10">
                {% if form.device_value_config.errors %}
                {{ form.device_value_config(class="form-control is-invalid") }}
                <div class="invalid-feedback">
                    {% for error in form.device_value_config.errors %}
                    <span>{{ error }}</span>
                    {% endfor %}
                </div>
                {% else %}
                {{ form.device_value_config(class="form-control") }}
                {% endif %}
            </div>
        </div>
        <br />
        <div class="d-grid gap-2 d-md-flex justify-content-md-start">
            <button type="submit" class="btn btn-primary">
                <i class="bi bi-plus-circle-fill"></i>
                {{ submit_text }}
            </button>
            <button type="button" class="btn btn-block btn-secondary" onclick="javascript: history.back()">
                <i class="bi bi-box-arrow-in-left"></i>
                Cancel
            </button>
        </div>
    </fieldset>
</form>
{% endblock %}
 No newline at end of file
Loading