diff --git a/src/webui/service/device/forms.py b/src/webui/service/device/forms.py index d1880d321c4cb2cc825baed5353b892aae90aae9..3d728ade13205edb8a0febd649f74992e3477ff2 100644 --- a/src/webui/service/device/forms.py +++ b/src/webui/service/device/forms.py @@ -14,7 +14,7 @@ # 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 webui.utils.form_validators import key_value_validator @@ -22,33 +22,23 @@ 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}.') + raise ValidationError('The operational status value selected is incorrect!') \ No newline at end of file diff --git a/src/webui/service/device/routes.py b/src/webui/service/device/routes.py index b57c5735d4b26c541d60a885512fe37a2fd626bc..220a3a33ce0c34c0cde6c5923b6e3990816f458e 100644 --- a/src/webui/service/device/routes.py +++ b/src/webui/service/device/routes.py @@ -16,12 +16,14 @@ 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 device = Blueprint('device', __name__, url_prefix='/device') context_client = ContextClient() @@ -57,60 +59,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() - 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) + # 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 = '_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() - - for driver in data.split(','): - driver = driver.strip() - if len(driver) == 0: - continue - device.device_drivers.append(int(driver)) + # 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) + 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): diff --git a/src/webui/service/templates/device/add.html b/src/webui/service/templates/device/add.html index fe1ba31f26579fcf681ccf4da64a7906cae9cff5..1acbc7e48dd342f818cb420c251452be1339a301 100644 --- a/src/webui/service/templates/device/add.html +++ b/src/webui/service/templates/device/add.html @@ -17,100 +17,145 @@ {% extends 'base.html' %} {% block content %} - <h1>Add New Device</h1> - - <form id="add_device" method="POST"> - {{ form.hidden_tag() }} - <fieldset> - <div class="row mb-3"> - {{ form.device_id.label(class="col-sm-2 col-form-label") }} - <div class="col-sm-10"> - {% if form.device_id.errors %} - {{ form.device_id(class="form-control is-invalid") }} - <div class="invalid-feedback"> - {% for error in form.device_id.errors %} - <span>{{ error }}</span> - {% endfor %} - </div> - {% else %} - {{ form.device_id(class="form-control") }} - {% endif %} +<h1>Add New Device</h1> +<br /> +<form id="add_device" method="POST"> + {{ form.hidden_tag() }} + <fieldset> + <div class="row mb-3"> + {{ form.device_id.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {% if form.device_id.errors %} + {{ form.device_id(class="form-control is-invalid") }} + <div class="invalid-feedback"> + {% for error in form.device_id.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ form.device_id(class="form-control") }} + {% 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"> + {% if form.device_type.errors %} + {{ form.device_type(class="form-control is-invalid") }} + <div class="invalid-feedback"> + {% for error in form.device_type.errors %} + <span>{{ error }}</span> + {% endfor %} </div> + {% else %} + {{ form.device_type(class="form-select")}} + {% endif %} </div> - <div class="row mb-3"> - {{ form.device_type.label(class="col-sm-2 col-form-label") }} - <div class="col-sm-10"> - {% if form.device_type.errors %} - {{ form.device_type(class="form-control is-invalid") }} - <div class="invalid-feedback"> - {% for error in form.device_type.errors %} - <span>{{ error }}</span> - {% endfor %} - </div> - {% else %} - {{ form.device_type(class="form-control") }} - {% endif %} + </div> + <br /> + <div class="row mb-3"> + {{ form.operational_status.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {% if form.operational_status.errors %} + {{ form.operational_status(class="form-control is-invalid") }} + <div class="invalid-feedback"> + {% for error in form.operational_status.errors %} + <span>{{ error }}</span> + {% endfor %} </div> + {% else %} + {{ form.operational_status(class="form-select") }} + {% endif %} </div> - <div class="row mb-3"> - {{ form.operational_status.label(class="col-sm-2 col-form-label") }} - <div class="col-sm-10"> - {% if form.operational_status.errors %} - {{ form.operational_status(class="form-control is-invalid") }} - <div class="invalid-feedback"> - {% for error in form.operational_status.errors %} - <span>{{ error }}</span> - {% endfor %} - </div> - {% else %} - {{ form.operational_status(class="form-control") }} - {% endif %} + </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 class="row mb-3"> - {{ form.device_config.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) }} - <div class="invalid-feedback"> - {% for error in form.device_config.errors %} - <span>{{ error }}</span> - {% endfor %} - </div> - {% else %} - {{ form.device_config(class="form-control", rows=5) }} - {% endif %} + </div> + <br /> + Configuration Rules <br /> + <div class="row mb-3"> + {{ form.device_config_address.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {% 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_address.errors %} + <span>{{ error }}</span> + {% endfor %} </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> + {% else %} + {{ form.device_config_address(class="form-control", rows=5) }} + {% endif %} </div> - <div class="row mb-3"> - {{ form.device_drivers.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) }} - <div class="invalid-feedback"> - {% for error in form.device_drivers.errors %} - <span>{{ error }}</span> - {% endfor %} - </div> - {% else %} - {{ form.device_drivers(class="form-control", rows=5) }} - {% endif %} + </div> + <div class="row mb-3"> + {{ form.device_config_port.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {% 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_config_port.errors %} + <span>{{ error }}</span> + {% endfor %} </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 }}. + {% else %} + {{ form.device_config_port(class="form-control", rows=5) }} + {% endif %} + </div> + </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 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> + </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 diff --git a/src/webui/service/templates/device/detail.html b/src/webui/service/templates/device/detail.html index 69ca93727310db7f89034f56510ceb5df504083f..f07b9c985c1e6dfdd46a6f13a0852c80280543d9 100644 --- a/src/webui/service/templates/device/detail.html +++ b/src/webui/service/templates/device/detail.html @@ -13,118 +13,120 @@ See the License for the specific language governing permissions and limitations under the License. --> - - {% extends 'base.html' %} - - {% block content %} - <h1>Device {{ device.device_id.device_uuid.uuid }}</h1> - - <div class="row mb-3"> - <div class="col-sm-3"> - <button type="button" class="btn btn-success" onclick="window.location.href='{{ url_for('device.home') }}'"> - <i class="bi bi-box-arrow-in-left"></i> - Back to device list - </button> - </div> - <div class="col-sm-3"> - <a id="update" class="btn btn-secondary" href="#"> - <i class="bi bi-pencil-square"></i> - Update - </a> - </div> - <div class="col-sm-3"> - <!-- <button type="button" class="btn btn-danger"><i class="bi bi-x-square"></i>Delete device</button> --> - <button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#deleteModal"> - <i class="bi bi-x-square"></i>Delete device - </button> - </div> - </div> - <br> - <div class="row mb-3"> - <div class="col-sm-4"> - <b>UUID: </b>{{ device.device_id.device_uuid.uuid }}<br><br> - <b>Type: </b>{{ device.device_type }}<br><br> - <b>Status: </b> {{ dose.Name(device.device_operational_status).replace('DEVICEOPERATIONALSTATUS_', '') }}<br> - <b>Drivers: </b> - <ul> - {% for driver in device.device_drivers %} - <li>{{ dde.Name(driver).replace('DEVICEDRIVER_', '').replace('UNDEFINED', 'EMULATED') }}</li> - {% endfor %} - </ul> - </div> - <div class="col-sm-8"> - <table class="table table-striped table-hover"> - <thead> - <tr> - <th scope="col">Endpoints</th> - <th scope="col">Type</th> - </tr> - </thead> - <tbody> - {% for endpoint in device.device_endpoints %} - <tr> - <td> - {{ endpoint.endpoint_id.endpoint_uuid.uuid }} - </td> - <td> - {{ endpoint.endpoint_type }} - </td> - </tr> - {% endfor %} - </tbody> - </table> - </div> - </div> - </div> +{% extends 'base.html' %} + +{% block content %} +<h1>Device {{ device.device_id.device_uuid.uuid }}</h1> + +<div class="row mb-3"> + <div class="col-sm-3"> + <button type="button" class="btn btn-success" onclick="window.location.href='{{ url_for('device.home') }}'"> + <i class="bi bi-box-arrow-in-left"></i> + Back to device list + </button> + </div> + <div class="col-sm-3"> + <a id="update" class="btn btn-secondary" href="#"> + <i class="bi bi-pencil-square"></i> + Update + </a> + </div> + <div class="col-sm-3"> + <!-- <button type="button" class="btn btn-danger"><i class="bi bi-x-square"></i>Delete device</button> --> + <button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#deleteModal"> + <i class="bi bi-x-square"></i>Delete device + </button> + </div> +</div> - <b>Configurations:</b> +<br> +<div class="row mb-3"> + <div class="col-sm-4"> + <b>UUID: </b>{{ device.device_id.device_uuid.uuid }}<br><br> + <b>Type: </b>{{ device.device_type }}<br><br> + <b>Status: </b> {{ dose.Name(device.device_operational_status).replace('DEVICEOPERATIONALSTATUS_', '') }}<br> + <b>Drivers: </b> + <ul> + {% for driver in device.device_drivers %} + <li>{{ dde.Name(driver).replace('DEVICEDRIVER_', '').replace('UNDEFINED', 'EMULATED') }}</li> + {% endfor %} + </ul> + </div> + <div class="col-sm-8"> <table class="table table-striped table-hover"> <thead> <tr> - <th scope="col">Key</th> - <th scope="col">Value</th> + <th scope="col">Endpoints</th> + <th scope="col">Type</th> </tr> </thead> <tbody> - {% for config in device.device_config.config_rules %} - {% if config.WhichOneof('config_rule') == 'custom' %} + {% for endpoint in device.device_endpoints %} <tr> <td> - {{ config.custom.resource_key }} + {{ endpoint.endpoint_id.endpoint_uuid.uuid }} </td> <td> - <ul> - {% for key, value in (config.custom.resource_value | from_json).items() %} - <li><b>{{ key }}:</b> {{ value }}</li> - {% endfor %} - </ul> + {{ endpoint.endpoint_type }} </td> </tr> - {% endif %} {% endfor %} </tbody> </table> + </div> +</div> +</div> +<b>Configurations:</b> +<table class="table table-striped table-hover"> + <thead> + <tr> + <th scope="col">Key</th> + <th scope="col">Value</th> + </tr> + </thead> + <tbody> + {% for config in device.device_config.config_rules %} + {% if config.WhichOneof('config_rule') == 'custom' %} + <tr> + <td> + {{ config.custom.resource_key }} + </td> + <td> + <ul> + {% for key, value in (config.custom.resource_value | from_json).items() %} + <li><b>{{ key }}:</b> {{ value }}</li> + {% endfor %} + </ul> + </td> + </tr> + {% endif %} + {% endfor %} + </tbody> +</table> - <!-- Modal --> - <div class="modal fade" id="deleteModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true"> - <div class="modal-dialog"> - <div class="modal-content"> - <div class="modal-header"> - <h5 class="modal-title" id="staticBackdropLabel">Delete device?</h5> - <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> - </div> - <div class="modal-body"> - Are you sure you want to delete the device "{{ device.device_id.device_uuid.uuid }}"? - </div> - <div class="modal-footer"> - <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">No</button> - <a type="button" class="btn btn-danger" href="{{ url_for('device.delete', device_uuid=device.device_id.device_uuid.uuid) }}"><i class="bi bi-exclamation-diamond"></i>Yes</a> - </div> - </div> - </div> - </div> - - {% endblock %} - \ No newline at end of file + +<!-- Modal --> +<div class="modal fade" id="deleteModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" + aria-labelledby="staticBackdropLabel" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="staticBackdropLabel">Delete device?</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + Are you sure you want to delete the device "{{ device.device_id.device_uuid.uuid }}"? + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">No</button> + <a type="button" class="btn btn-danger" + href="{{ url_for('device.delete', device_uuid=device.device_id.device_uuid.uuid) }}"><i + class="bi bi-exclamation-diamond"></i>Yes</a> + </div> + </div> + </div> +</div> + +{% endblock %}