diff --git a/src/webui/service/device/forms.py b/src/webui/service/device/forms.py index e4c71d92170dc9fe46996a1c93978647800aa300..38f56bc0081f2ca7ceb8067b8a3108ba5d5b51ed 100644 --- a/src/webui/service/device/forms.py +++ b/src/webui/service/device/forms.py @@ -33,6 +33,7 @@ class AddDeviceForm(FlaskForm): device_drivers_gnmi_openconfig = BooleanField('GNMI OPENCONFIG') device_drivers_optical_tfs = BooleanField('OPTICAL TFS') device_drivers_ietf_actn = BooleanField('IETF ACTN') + device_drivers_qkd = BooleanField('QKD') 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)]) @@ -56,4 +57,4 @@ class UpdateDeviceForm(FlaskForm): coerce=int, validators=[NumberRange(min=0)]) - submit = SubmitField('Update') + submit = SubmitField('Update') \ No newline at end of file diff --git a/src/webui/service/device/routes.py b/src/webui/service/device/routes.py index fb930024d9fc3ff0b70b77345041e33dcb0d28c2..8856c6020366d3dffb73471b24b4f04bbc6c6813 100644 --- a/src/webui/service/device/routes.py +++ b/src/webui/service/device/routes.py @@ -129,6 +129,8 @@ def add(): device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_OPTICAL_TFS) if form.device_drivers_ietf_actn.data: device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_IETF_ACTN) + if form.device_drivers_qkd.data: + device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_QKD) device_obj.device_drivers.extend(device_drivers) # pylint: disable=no-member try: @@ -250,4 +252,4 @@ def update(device_uuid): return redirect(url_for('device.home')) except Exception as e: # pylint: disable=broad-except flash(f'Problem updating the device. {e.details()}', 'danger') - return render_template('device/update.html', device=response, form=form, submit_text='Update Device') + return render_template('device/update.html', device=response, form=form, submit_text='Update Device') \ No newline at end of file diff --git a/src/webui/service/service/forms.py b/src/webui/service/service/forms.py index f07acf54365b79245583e7f9567b8bc4a5cfd89d..2eb9794a943a036d3d305057c9bce6b784cc60d6 100644 --- a/src/webui/service/service/forms.py +++ b/src/webui/service/service/forms.py @@ -17,6 +17,11 @@ from flask_wtf import FlaskForm from wtforms import StringField, SelectField, IntegerField, DecimalField from wtforms.validators import InputRequired, Optional, NumberRange, ValidationError, StopValidation +# Custom uuid validator +def validate_uuid_address(form, field): + if not re.match(r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', field.data): + raise ValidationError('Invalid uuid format') + # Custom IPv4 address validator def validate_ipv4_address(form, field): try: @@ -60,7 +65,25 @@ class CustomInputRequired(): raise StopValidation(self.message) class AddServiceForm_1(FlaskForm): - service_type = SelectField('Type of service', choices=[('', 'Select a type of service to add'), ('ACL_L2', 'ACL_L2'), ('ACL_IPV4', 'ACL_IPV4'), ('ACL_IPV6', 'ACL_IPV6'), ('L2VPN', 'L2VPN'), ('L3VPN', 'L3VPN')], validators=[InputRequired()]) + service_type = SelectField('Type of service', choices=[('', 'Select a type of service to add'), ('ACL_L2', 'ACL_L2'), ('ACL_IPV4', 'ACL_IPV4'), ('ACL_IPV6', 'ACL_IPV6'), ('L2VPN', 'L2VPN'), ('L3VPN', 'L3VPN'), ('QKD', 'QKD')], validators=[InputRequired()]) + +class AddServiceForm_QKD(FlaskForm): + #GENERIC SERVICE PARAMETERS (COMMON & MANDATORY) + service_name = StringField('Service Name', validators=[CustomInputRequired()]) + service_type = SelectField('Service Type', choices=[(6, '6 (QKD)')], validators=[CustomInputRequired()]) + service_device_1 = SelectField('Device_1', choices=[('', 'Select a device (Mandatory)')], validators=[CustomInputRequired()]) + service_device_2 = SelectField('Device_2', choices=[('', 'Select a device (Mandatory)')], validators=[CustomInputRequired()]) + service_endpoint_1 = StringField('Device_1 Endpoint', validators=[CustomInputRequired()]) + service_endpoint_2 = StringField('Device_2 Endpoint', validators=[CustomInputRequired()]) + + #GENERIC SERVICE CONSTRAINT PARAMETERS (ALL OPTIONAL) + service_capacity = DecimalField('Service Capacity', places=2, default=10.00, validators=[Optional(), NumberRange(min=0)]) + service_latency = DecimalField('Service Latency', places=2, default=15.20, validators=[Optional(), NumberRange(min=0)]) + service_availability= DecimalField('Service Availability', places=2, validators=[Optional(), NumberRange(min=0)]) + service_isolation = SelectField('Service Isolation', choices=[('', 'Select (Optional)'), ('NO_ISOLATION', 'NO_ISOLATION'), ('PHYSICAL_ISOLATION', 'PHYSICAL_ISOLATION'), + ('LOGICAL_ISOLATION', 'LOGICAL_ISOLATION'), ('PROCESS_ISOLATION', 'PROCESS_ISOLATION'), ('PHYSICAL_MEMORY_ISOLATION', 'PHYSICAL_MEMORY_ISOLATION'), + ('PHYSICAL_NETWORK_ISOLATION', 'PHYSICAL_NETWORK_ISOLATION'), ('VIRTUAL_RESOURCE_ISOLATION', 'VIRTUAL_RESOURCE_ISOLATION'), + ('NETWORK_FUNCTIONS_ISOLATION', 'NETWORK_FUNCTIONS_ISOLATION'), ('SERVICE_ISOLATION', 'SERVICE_ISOLATION')], validators=[Optional()]) class AddServiceForm_ACL_L2(FlaskForm): #GENERIC SERVICE PARAMETERS (COMMON & MANDATORY) @@ -258,4 +281,4 @@ class AddServiceForm_L3VPN(FlaskForm): Device_2_IF_mtu = IntegerField('Device_2 Interface MTU', validators=[Optional(), NumberRange(min=0, message="MTU value can't be negative")]) Device_2_IF_address_ip = StringField('Device_2 IP Address', validators=[CustomInputRequired(), validate_ipv4_address]) Device_2_IF_address_prefix = IntegerField('Device_2 IP Prefix length', validators=[CustomInputRequired(), validate_uint32]) - Device_2_IF_description = StringField ('Device_2 SubIF Description', validators=[Optional()]) + Device_2_IF_description = StringField ('Device_2 SubIF Description', validators=[Optional()]) \ No newline at end of file diff --git a/src/webui/service/service/routes.py b/src/webui/service/service/routes.py index 92025b2bec4f7c70b446a8c422e2cdb166c95466..d8a3747a1c572cede9020ffb65d63e1546b9b0a7 100644 --- a/src/webui/service/service/routes.py +++ b/src/webui/service/service/routes.py @@ -35,14 +35,14 @@ from common.tools.object_factory.Constraint import ( from common.tools.object_factory.Context import json_context_id from common.tools.object_factory.Device import json_device_id from common.tools.object_factory.EndPoint import json_endpoint_id -from common.tools.object_factory.Service import json_service_l2nm_planned, json_service_l3nm_planned +from common.tools.object_factory.Service import json_service_l2nm_planned, json_service_l3nm_planned, json_service_qkd_planned from common.tools.object_factory.Topology import json_topology_id from context.client.ContextClient import ContextClient from device.client.DeviceClient import DeviceClient from service.client.ServiceClient import ServiceClient from webui.service.service.forms import ( AddServiceForm_1, AddServiceForm_ACL_L2, AddServiceForm_ACL_IPV4, AddServiceForm_ACL_IPV6, - AddServiceForm_L2VPN, AddServiceForm_L3VPN + AddServiceForm_L2VPN, AddServiceForm_L3VPN, AddServiceForm_QKD ) LOGGER = logging.getLogger(__name__) @@ -329,10 +329,83 @@ def add_configure(): form_1 = AddServiceForm_1() if form_1.validate_on_submit(): service_type = str(form_1.service_type.data) - if service_type in {'ACL_L2', 'ACL_IPV4', 'ACL_IPV6', 'L2VPN', 'L3VPN'}: + if service_type in {'ACL_L2', 'ACL_IPV4', 'ACL_IPV6', 'L2VPN', 'L3VPN', 'QKD'}: return redirect(url_for('service.add_configure_{:s}'.format(service_type))) return render_template('service/add.html', form_1=form_1, submit_text='Continue to configuraton') +@service.route('add/configure/QKD', methods=['GET', 'POST']) +def add_configure_QKD(): + form_qkd = AddServiceForm_QKD() + service_obj = Service() + + context_uuid, topology_uuid = get_context_and_topology_uuids() + if context_uuid and topology_uuid: + context_client.connect() + grpc_topology = get_topology(context_client, topology_uuid, context_uuid=context_uuid, rw_copy=False) + if grpc_topology: + topo_device_uuids = {device_id.device_uuid.uuid for device_id in grpc_topology.device_ids} + devices = get_filtered_devices(context_client, topo_device_uuids) + 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_QKD in device.device_drivers + ] + choices = get_device_choices(devices) + add_device_choices_to_form(choices, form_qkd.service_device_1) + add_device_choices_to_form(choices, form_qkd.service_device_2) + else: + flash('Context({:s})/Topology({:s}) not found'.format(str(context_uuid), str(topology_uuid)), 'danger') + else: + flash('Missing context or topology UUID', 'danger') + + if form_qkd.validate_on_submit(): + try: + [selected_device_1, selected_device_2, selected_endpoint_1, selected_endpoint_2] = validate_selected_devices_and_endpoints(form_qkd, devices) + except Exception as e: + flash('{:s}'.format(str(e.args[0])), 'danger') + current_app.logger.exception(e) + return render_template('service/configure_QKD.html', form_qkd=form_qkd, submit_text='Add New Service') + + service_uuid, service_type, endpoint_ids = set_service_parameters(service_obj, form_qkd, selected_device_1, selected_device_2, selected_endpoint_1, selected_endpoint_2) + constraints = add_constraints(form_qkd) + params_device_1_with_data = get_device_params(form_qkd, 1, service_type) + params_device_2_with_data = get_device_params(form_qkd, 2, service_type) + print(params_device_1_with_data) + print(params_device_2_with_data) + params_settings = {} + config_rules = [ + json_config_rule_set( + '/settings', params_settings + ), + json_config_rule_set( + '/device[{:s}]/endpoint[{:s}]/settings'.format(str(selected_device_1.name), str(selected_endpoint_1)), params_device_1_with_data + ), + json_config_rule_set( + '/device[{:s}]/endpoint[{:s}]/settings'.format(str(selected_device_2.name), str(selected_endpoint_2)), params_device_2_with_data + ) + ] + + service_client.connect() + context_client.connect() + device_client.connect() + descriptor_json = json_service_qkd_planned(service_uuid = service_uuid, endpoint_ids = endpoint_ids, constraints = constraints, config_rules = config_rules, context_uuid= context_uuid) + descriptor_json = {"services": [descriptor_json]} + try: + process_descriptors(descriptor_json) + flash('Service "{:s}" added successfully!'.format(service_obj.service_id.service_uuid.uuid), 'success') + return redirect(url_for('service.home', service_uuid=service_obj.service_id.service_uuid.uuid)) + except Exception as e: + flash('Problem adding service: {:s}'.format((str(e.args[0]))), 'danger') + current_app.logger.exception(e) + finally: + context_client.close() + device_client.close() + service_client.close() + + + return render_template('service/configure_QKD.html', form_qkd=form_qkd, submit_text='Add New Service') + + @service.route('add/configure/ACL_L2', methods=['GET', 'POST']) def add_configure_ACL_L2(): form_acl = AddServiceForm_ACL_L2() @@ -666,8 +739,11 @@ def get_device_params(form, device_num, form_type): 'ni_description': str(getattr(form, 'NI_description').data), 'subif_description': str(getattr(form, f'Device_{device_num}_IF_description').data), } + elif form_type == 6: + device_params = { + } else: raise ValueError(f'Unsupported form type: {form_type}') params_with_data = {k: v for k, v in device_params.items() if v is not None and str(v) != 'None' and v != ''} - return params_with_data + return params_with_data \ No newline at end of file diff --git a/src/webui/service/static/topology_icons/emu-qkd-node.png b/src/webui/service/static/topology_icons/emu-qkd-node.png new file mode 100644 index 0000000000000000000000000000000000000000..d4dc1abaf42a56ff07d1f4a2c5d250b56486584d Binary files /dev/null and b/src/webui/service/static/topology_icons/emu-qkd-node.png differ diff --git a/src/webui/service/static/topology_icons/qkd-node.png b/src/webui/service/static/topology_icons/qkd-node.png new file mode 100644 index 0000000000000000000000000000000000000000..79f40d2a600bd7f9e55d0360a132800c09a8ac85 Binary files /dev/null and b/src/webui/service/static/topology_icons/qkd-node.png differ diff --git a/src/webui/service/templates/app/home.html b/src/webui/service/templates/app/home.html new file mode 100644 index 0000000000000000000000000000000000000000..ed37a21cbb93919ed3abd1212c1ed5c8fbea8ff8 --- /dev/null +++ b/src/webui/service/templates/app/home.html @@ -0,0 +1,78 @@ +{% extends 'base.html' %} + +{% block content %} + <h1>Apps</h1> + + <div class="row"> + <div class="col"> + {{ apps | length }} apps found in context <i>{{ session['context_uuid'] }}</i> + </div> + </div> + + <table class="table table-striped table-hover"> + <thead> + <tr> + <th scope="col">UUID</th> + <th scope="col">Status</th> + <th scope="col">Type</th> + <th scope="col">Device 1</th> + <th scope="col">Device 2</th> + <th scope="col"></th> + </tr> + </thead> + <tbody> + {% if apps %} + {% for app in apps %} + <tr> + <td> + {{ app.app_id.app_uuid.uuid }} + </td> + <td> + {{ ase.Name(app.app_status).replace('QKDAPPSTATUS_', '') }} + </td> + <td> + {{ ate.Name(app.app_type).replace('QKDAPPTYPES_', '').replace('CLIENT', 'EXTERNAL') }} + </td> + <td> + <li> + <a href="{{ url_for('device.detail', device_uuid=app.local_device_id.device_uuid.uuid) }}"> + {{ device_names.get(app.local_device_id.device_uuid.uuid, app.local_device_id.device_uuid.uuid) }} + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16"> + <path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/> + <path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/> + </svg> + </a> + </li> + </td> + <td> + {% if app.remote_device_id.device_uuid.uuid %} + <li> + <a href="{{ url_for('device.detail', device_uuid=app.remote_device_id.device_uuid.uuid) }}"> + {{ device_names.get(app.remote_device_id.device_uuid.uuid, app.remote_device_id.device_uuid.uuid) }} + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16"> + <path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/> + <path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/> + </svg> + </a> + </li> + {% endif %} + </td> + <td> + <a href="{{ url_for('app.detail', app_uuid=app.app_id.app_uuid.uuid) }}"> + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16"> + <path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/> + <path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/> + </svg> + </a> + </td> + </tr> + {% endfor %} + {% else %} + <tr> + <td colspan="7">No apps found</td> + </tr> + {% endif %} + </tbody> + </table> + +{% endblock %} \ No newline at end of file diff --git a/src/webui/service/templates/base.html b/src/webui/service/templates/base.html index 66e188465994a47f173dcca93237b46cd86adb16..8c1dddbc166d4340e2771ea8479f466fbcc636f7 100644 --- a/src/webui/service/templates/base.html +++ b/src/webui/service/templates/base.html @@ -103,6 +103,13 @@ <a class="nav-link" href="{{ url_for('load_gen.home') }}">Load Generator</a> {% endif %} </li> + <li class="nav-item"> + {% if '/app/' in request.path %} + <a class="nav-link active" aria-current="page" href="{{ url_for('app.home') }}">App</a> + {% else %} + <a class="nav-link" href="{{ url_for('app.home') }}">App</a> + {% endif %} + </li> <li class="nav-item"> {% if '/bgpls/' in request.path %} <a class="nav-link active" aria-current="page" href="{{ url_for('bgpls.home') }}">BGPLS</a> diff --git a/src/webui/service/templates/device/add.html b/src/webui/service/templates/device/add.html index 3bea6ae719a75c91835ceb35f50b5bbeba2c7940..e11c37688c09b96849c63a5d51cd7e546468d558 100644 --- a/src/webui/service/templates/device/add.html +++ b/src/webui/service/templates/device/add.html @@ -95,6 +95,7 @@ <br /> {{ form.device_drivers_optical_tfs }} {{ form.device_drivers_optical_tfs.label(class="col-sm-3 col-form-label") }} {{ form.device_drivers_ietf_actn }} {{ form.device_drivers_ietf_actn.label(class="col-sm-3 col-form-label") }} + {{ form.device_drivers_qkd }} {{ form.device_drivers_qkd.label(class="col-sm-3 col-form-label") }} {% endif %} </div> </div> diff --git a/src/webui/service/templates/service/configure_QKD.html b/src/webui/service/templates/service/configure_QKD.html new file mode 100644 index 0000000000000000000000000000000000000000..3864a5c9f271a183630a1fb32e503cfb76ba9bd1 --- /dev/null +++ b/src/webui/service/templates/service/configure_QKD.html @@ -0,0 +1,187 @@ +<!-- + 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 %} +<h1>Add New Service [QKD]</h1> +<form method="POST" action="{{ url_for('service.add_configure_QKD') }}"> + <fieldset> + <div class="row mb-3"> + {{ form_qkd.hidden_tag() }} + </div> + <h3>Generic Service Parameters</h3> + {% if form_qkd.qkd_params is not none %} + <div class="row mb-3"> + {{ form_qkd.service_name.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {% if form_qkd.service_name.errors %} + {{ form_qkd.service_name(class="form-control is-invalid", placeholder="Mandatory") }} + <div class="invalid-feedback"> + {% for error in form_qkd.service_name.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ form_qkd.service_name(class="form-control", placeholder="Mandatory") }} + {% endif %} + </div> + </div> + <div class="row mb-3"> + {{ form_qkd.service_type.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {% if form_qkd.service_type.errors %} + {{ form_qkd.service_type(class="form-control is-invalid", placeholder="Mandatory") }} + <div class="invalid-feedback"> + {% for error in form_qkd.service_type.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ form_qkd.service_type(class="form-control", placeholder="Mandatory") }} + {% endif %} + </div> + </div> + <div class="row mb-3"> + {{ form_qkd.service_device_1.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-4"> + {% if form_qkd.service_device_1.errors %} + {{ form_qkd.service_device_1(class="form-control is-invalid", placeholder="Mandatory") }} + <div class="invalid-feedback"> + {% for error in form_qkd.service_device_1.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ form_qkd.service_device_1(class="form-control", placeholder="Mandatory") }} + {% endif %} + </div> + {{ form_qkd.service_device_2.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-4"> + {% if form_qkd.service_device_2.errors %} + {{ form_qkd.service_device_2(class="form-control is-invalid", placeholder="Mandatory") }} + <div class="invalid-feedback"> + {% for error in form_qkd.service_device_2.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ form_qkd.service_device_2(class="form-control", placeholder="Mandatory") }} + {% endif %} + </div> + </div> + <div class="row mb-3"> + {{ form_qkd.service_endpoint_1.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-4"> + {% if form_qkd.service_endpoint_1.errors %} + {{ form_qkd.service_endpoint_1(class="form-control is-invalid", placeholder="Mandatory") }} + <div class="invalid-feedback"> + {% for error in form_qkd.service_endpoint_1.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ form_qkd.service_endpoint_1(class="form-control", placeholder="Mandatory") }} + {% endif %} + </div> + {{ form_qkd.service_endpoint_2.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-4"> + {% if form_qkd.service_endpoint_2.errors %} + {{ form_qkd.service_endpoint_2(class="form-control is-invalid", placeholder="Mandatory") }} + <div class="invalid-feedback"> + {% for error in form_qkd.service_endpoint_2.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ form_qkd.service_endpoint_2(class="form-control", placeholder="Mandatory") }} + {% endif %} + </div> + </div> + </br> + <h3>Generic Service Constraints</h3> + <div class="row mb-3"> + {{ form_qkd.service_capacity.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {% if form_qkd.service_capacity.errors %} + {{ form_qkd.service_capacity(class="form-control is-invalid") }} + <div class="invalid-feedback"> + {% for error in form_qkd.service_capacity.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ form_qkd.service_capacity(class="form-control") }} + {% endif %} + </div> + </div> + <div class="row mb-3"> + {{ form_qkd.service_latency.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {% if form_qkd.service_latency.errors %} + {{ form_qkd.service_latency(class="form-control is-invalid") }} + <div class="invalid-feedback"> + {% for error in form_qkd.service_latency.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ form_qkd.service_latency(class="form-control") }} + {% endif %} + </div> + </div> + <div class="row mb-3"> + {{ form_qkd.service_availability.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {% if form_qkd.service_availability.errors %} + {{ form_qkd.service_availability(class="form-control is-invalid") }} + <div class="invalid-feedback"> + {% for error in form_qkd.service_availability.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ form_qkd.service_availability(class="form-control") }} + {% endif %} + </div> + </div> + <div class="row mb-3"> + {{ form_qkd.service_isolation.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {% if form_qkd.service_isolation.errors %} + {{ form_qkd.service_isolation(class="form-control is-invalid") }} + <div class="invalid-feedback"> + {% for error in form_qkd.service_isolation.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ form_qkd.service_isolation(class="form-control") }} + {% endif %} + </div> + </div> + {% endif %} + <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> + </fieldset> + </form> + {% endblock %} \ No newline at end of file