From 4bc896fad2d66dfba7cb90ce66c068184d2de571 Mon Sep 17 00:00:00 2001 From: gifrerenom <lluis.gifre@cttc.es> Date: Fri, 28 Oct 2022 19:35:50 +0000 Subject: [PATCH] WebUI component: - extneded context selection to context/topology - enabled selection of context/topology to plot - enabled listing of devices/links based on context/topology - updated network icon --- src/webui/service/__init__.py | 9 ++- src/webui/service/device/routes.py | 28 +++++-- src/webui/service/link/routes.py | 28 +++++-- src/webui/service/main/forms.py | 25 +++--- src/webui/service/main/routes.py | 73 +++++++++++++----- .../topology_icons/Acknowledgements.txt | 3 +- .../service/static/topology_icons/network.png | Bin 8988 -> 7520 bytes src/webui/service/templates/base.html | 2 +- src/webui/service/templates/main/home.html | 22 +++--- 9 files changed, 126 insertions(+), 64 deletions(-) diff --git a/src/webui/service/__init__.py b/src/webui/service/__init__.py index 75e103642..d60cca659 100644 --- a/src/webui/service/__init__.py +++ b/src/webui/service/__init__.py @@ -19,10 +19,10 @@ from context.client.ContextClient import ContextClient from device.client.DeviceClient import DeviceClient def get_working_context() -> str: - if 'context_uuid' in session: - return session['context_uuid'] - else: - return 'Not selected' + return session['context_uuid'] if 'context_uuid' in session else '---' + +def get_working_topology() -> str: + return session['topology_uuid'] if 'topology_uuid' in session else '---' def liveness(): pass @@ -85,6 +85,7 @@ def create_app(use_config=None, web_app_root=None): app.jinja_env.filters['from_json'] = from_json app.jinja_env.globals.update(get_working_context=get_working_context) + app.jinja_env.globals.update(get_working_topology=get_working_topology) if web_app_root is not None: app.wsgi_app = SetSubAppMiddleware(app.wsgi_app, web_app_root) diff --git a/src/webui/service/device/routes.py b/src/webui/service/device/routes.py index f1423e92e..b57c5735d 100644 --- a/src/webui/service/device/routes.py +++ b/src/webui/service/device/routes.py @@ -16,7 +16,9 @@ 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) + Empty, TopologyId) +from common.tools.object_factory.Context import json_context_id +from common.tools.object_factory.Topology import json_topology_id from context.client.ContextClient import ContextClient from device.client.DeviceClient import DeviceClient from webui.service.device.forms import AddDeviceForm @@ -27,16 +29,28 @@ device_client = DeviceClient() @device.get('/') def home(): - context_uuid = session.get('context_uuid', '-') - if context_uuid == "-": + if 'context_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() - response: DeviceList = context_client.ListDevices(Empty()) + json_topo_id = json_topology_id(topology_uuid, context_id=json_context_id(context_uuid)) + grpc_topology = context_client.GetTopology(TopologyId(**json_topo_id)) + topo_device_uuids = {device_id.device_uuid.uuid for device_id in grpc_topology.device_ids} + grpc_devices: DeviceList = context_client.ListDevices(Empty()) context_client.close() - return render_template('device/home.html', devices=response.devices, - dde=DeviceDriverEnum, - dose=DeviceOperationalStatusEnum) + + devices = [ + device for device in grpc_devices.devices + if device.device_id.device_uuid.uuid in topo_device_uuids + ] + + return render_template( + 'device/home.html', devices=devices, dde=DeviceDriverEnum, + dose=DeviceOperationalStatusEnum) @device.route('add', methods=['GET', 'POST']) def add(): diff --git a/src/webui/service/link/routes.py b/src/webui/service/link/routes.py index 51e903d9e..5b8831b77 100644 --- a/src/webui/service/link/routes.py +++ b/src/webui/service/link/routes.py @@ -14,7 +14,9 @@ from flask import current_app, render_template, Blueprint, flash, session, redirect, url_for -from common.proto.context_pb2 import Empty, Link, LinkEvent, LinkId, LinkIdList, LinkList, DeviceId +from common.proto.context_pb2 import Empty, Link, LinkEvent, LinkId, LinkIdList, LinkList, DeviceId, TopologyId +from common.tools.object_factory.Context import json_context_id +from common.tools.object_factory.Topology import json_topology_id from context.client.ContextClient import ContextClient @@ -23,18 +25,28 @@ context_client = ContextClient() @link.get('/') def home(): - context_uuid = session.get('context_uuid', '-') - if context_uuid == "-": + if 'context_topology_uuid' not in session: flash("Please select a context!", "warning") return redirect(url_for("main.home")) - request = Empty() + + context_uuid = session['context_uuid'] + topology_uuid = session['topology_uuid'] + context_client.connect() - response = context_client.ListLinks(request) + json_topo_id = json_topology_id(topology_uuid, context_id=json_context_id(context_uuid)) + grpc_topology = context_client.GetTopology(TopologyId(**json_topo_id)) + topo_link_uuids = {link_id.link_uuid.uuid for link_id in grpc_topology.link_ids} + grpc_links: LinkList = context_client.ListLinks(Empty()) context_client.close() + + links = [ + link for link in grpc_links.links + if link.link_id.link_uuid.uuid in topo_link_uuids + ] + return render_template( - "link/home.html", - links=response.links, - ) + 'link/home.html', links=links) + @link.route('detail/<path:link_uuid>', methods=('GET', 'POST')) def detail(link_uuid: str): diff --git a/src/webui/service/main/forms.py b/src/webui/service/main/forms.py index abef11e06..b138592fc 100644 --- a/src/webui/service/main/forms.py +++ b/src/webui/service/main/forms.py @@ -19,20 +19,21 @@ from wtforms import SelectField, FileField, SubmitField from wtforms.validators import DataRequired, Length -class ContextForm(FlaskForm): - context = SelectField( 'Context', - choices=[], - validators=[ - DataRequired(), - Length(min=1) - ]) - +class ContextTopologyForm(FlaskForm): + context_topology = SelectField( + 'Ctx/Topo', + choices=[], + validators=[ + DataRequired(), + Length(min=1) + ]) submit = SubmitField('Submit') class DescriptorForm(FlaskForm): - descriptors = FileField('Descriptors', - validators=[ - FileAllowed(['json'], 'JSON Descriptors only!') - ]) + descriptors = FileField( + 'Descriptors', + validators=[ + FileAllowed(['json'], 'JSON Descriptors only!') + ]) submit = SubmitField('Submit') diff --git a/src/webui/service/main/routes.py b/src/webui/service/main/routes.py index 9b1b08857..1ca5329e3 100644 --- a/src/webui/service/main/routes.py +++ b/src/webui/service/main/routes.py @@ -12,10 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json, logging +import json, logging, re from flask import jsonify, redirect, render_template, Blueprint, flash, session, url_for, request -from common.proto.context_pb2 import Connection, Context, Device, Empty, Link, Service, Slice, Topology, ContextIdList +from common.proto.context_pb2 import Connection, Context, Device, Empty, Link, Service, Slice, Topology, ContextIdList, TopologyId, TopologyIdList from common.tools.grpc.Tools import grpc_message_to_json_string +from common.tools.object_factory.Context import json_context_id +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 @@ -23,7 +25,7 @@ from slice.client.SliceClient import SliceClient from webui.service.main.DescriptorTools import ( format_custom_config_rules, get_descriptors_add_contexts, get_descriptors_add_services, get_descriptors_add_slices, get_descriptors_add_topologies, split_devices_by_rules) -from webui.service.main.forms import ContextForm, DescriptorForm +from webui.service.main.forms import ContextTopologyForm, DescriptorForm main = Blueprint('main', __name__) @@ -154,20 +156,34 @@ def process_descriptors(descriptors): def home(): context_client.connect() device_client.connect() - response: ContextIdList = context_client.ListContextIds(Empty()) - context_form: ContextForm = ContextForm() - context_form.context.choices.append(('', 'Select...')) + context_topology_form: ContextTopologyForm = ContextTopologyForm() + context_topology_form.context_topology.choices.append(('', 'Select...')) - for context in response.context_ids: - context_form.context.choices.append((context.context_uuid.uuid, context.context_uuid)) + ctx_response: ContextIdList = context_client.ListContextIds(Empty()) + for context_id in ctx_response.context_ids: + context_uuid = context_id.context_uuid.uuid + topo_response: TopologyIdList = context_client.ListTopologyIds(context_id) + for topology_id in topo_response.topology_ids: + topology_uuid = topology_id.topology_uuid.uuid + context_topology_uuid = 'ctx[{:s}]/topo[{:s}]'.format(context_uuid, topology_uuid) + context_topology_name = 'Context({:s}):Topology({:s})'.format(context_uuid, topology_uuid) + context_topology_entry = (context_topology_uuid, context_topology_name) + context_topology_form.context_topology.choices.append(context_topology_entry) - if context_form.validate_on_submit(): - session['context_uuid'] = context_form.context.data - flash(f'The context was successfully set to `{context_form.context.data}`.', 'success') - return redirect(url_for("main.home")) + if context_topology_form.validate_on_submit(): + context_topology_uuid = context_topology_form.context_topology.data + if len(context_topology_uuid) > 0: + match = re.match('ctx\[([^\]]+)\]\/topo\[([^\]]+)\]', context_topology_uuid) + if match is not None: + session['context_topology_uuid'] = context_topology_uuid = match.group(0) + session['context_uuid'] = context_uuid = match.group(1) + session['topology_uuid'] = topology_uuid = match.group(2) + MSG = f'Context({context_uuid})/Topology({topology_uuid}) successfully selected.' + flash(MSG, 'success') + return redirect(url_for("main.home")) - if 'context_uuid' in session: - context_form.context.data = session['context_uuid'] + if 'context_topology_uuid' in session: + context_topology_form.context_topology.data = session['context_topology_uuid'] descriptor_form: DescriptorForm = DescriptorForm() try: @@ -181,22 +197,39 @@ def home(): context_client.close() device_client.close() - return render_template('main/home.html', context_form=context_form, descriptor_form=descriptor_form) + return render_template( + 'main/home.html', context_topology_form=context_topology_form, descriptor_form=descriptor_form) @main.route('/topology', methods=['GET']) def topology(): context_client.connect() try: + if 'context_topology_uuid' not in session: + return jsonify({'devices': [], 'links': []}) + + context_uuid = session['context_uuid'] + topology_uuid = session['topology_uuid'] + + json_topo_id = json_topology_id(topology_uuid, context_id=json_context_id(context_uuid)) + grpc_topology = context_client.GetTopology(TopologyId(**json_topo_id)) + + topo_device_uuids = {device_id.device_uuid.uuid for device_id in grpc_topology.device_ids} + topo_link_uuids = {link_id .link_uuid .uuid for link_id in grpc_topology.link_ids } + response = context_client.ListDevices(Empty()) - devices = [{ - 'id': device.device_id.device_uuid.uuid, - 'name': device.device_id.device_uuid.uuid, - 'type': device.device_type, - } for device in response.devices] + devices = [] + for device in response.devices: + if device.device_id.device_uuid.uuid not in topo_device_uuids: continue + devices.append({ + 'id': device.device_id.device_uuid.uuid, + 'name': device.device_id.device_uuid.uuid, + 'type': device.device_type, + }) response = context_client.ListLinks(Empty()) links = [] for link in response.links: + if link.link_id.link_uuid.uuid not in topo_link_uuids: continue if len(link.link_endpoint_ids) != 2: str_link = grpc_message_to_json_string(link) logger.warning('Unexpected link with len(endpoints) != 2: {:s}'.format(str_link)) diff --git a/src/webui/service/static/topology_icons/Acknowledgements.txt b/src/webui/service/static/topology_icons/Acknowledgements.txt index ddf7a8d0d..932103acc 100644 --- a/src/webui/service/static/topology_icons/Acknowledgements.txt +++ b/src/webui/service/static/topology_icons/Acknowledgements.txt @@ -1,6 +1,7 @@ Network Topology Icons taken from https://vecta.io/symbols -https://symbols.getvecta.com/stencil_240/51_cloud.4d0a827676.png => cloud.png +https://symbols.getvecta.com/stencil_240/51_cloud.4d0a827676.png => network.png + #modified to be grey instead of white https://symbols.getvecta.com/stencil_240/15_atm-switch.1bbf9a7cca.png => packet-switch.png https://symbols.getvecta.com/stencil_241/45_atm-switch.6a7362c1df.png => emu-packet-switch.png diff --git a/src/webui/service/static/topology_icons/network.png b/src/webui/service/static/topology_icons/network.png index 0f8e9c9714edd1c11904367ef1e9c60ef7ed3295..1f770f7bb2a31834a191e6c8727f059e1f14bbe1 100644 GIT binary patch literal 7520 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 literal 8988 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 O0V&C;%T`KbasLBlgSHX? diff --git a/src/webui/service/templates/base.html b/src/webui/service/templates/base.html index 5d7801d11..bee98ee82 100644 --- a/src/webui/service/templates/base.html +++ b/src/webui/service/templates/base.html @@ -103,7 +103,7 @@ </li> </ul> <span class="navbar-text" style="color: #fff;"> - Current context: <b>{{ get_working_context() }}</b> + Current Context(<b>{{ get_working_context() }}</b>)/Topology(<b>{{ get_working_topology() }}</b>) </span> </div> </div> diff --git a/src/webui/service/templates/main/home.html b/src/webui/service/templates/main/home.html index db390939f..43b066cc0 100644 --- a/src/webui/service/templates/main/home.html +++ b/src/webui/service/templates/main/home.html @@ -19,7 +19,7 @@ {% block content %} <h2>ETSI TeraFlowSDN Controller</h2> - {% for field, message in context_form.errors.items() %} + {% for field, message in context_topology_form.errors.items() %} <div class="alert alert-dismissible fade show" role="alert"> <b>{{ field }}</b>: {{ message }} <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> @@ -28,32 +28,32 @@ {% endfor %} <form id="select_context" method="POST" enctype="multipart/form-data"> - {{ context_form.hidden_tag() }} + {{ context_topology_form.hidden_tag() }} <fieldset class="form-group"> - <legend>Select the working context, or upload a JSON descriptors file</legend> + <legend>Select the desired Context/Topology</legend> <div class="row mb-3"> - {{ context_form.context.label(class="col-sm-1 col-form-label") }} + {{ context_topology_form.context_topology.label(class="col-sm-1 col-form-label") }} <div class="col-sm-5"> - {% if context_form.context.errors %} - {{ context_form.context(class="form-select is-invalid") }} + {% if context_topology_form.context_topology.errors %} + {{ context_topology_form.context_topology(class="form-select is-invalid") }} <div class="invalid-feedback"> - {% for error in context_form.context.errors %} + {% for error in context_topology_form.context_topology.errors %} <span>{{ error }}</span> {% endfor %} </div> {% else %} - {{ context_form.context(class="form-select") }} + {{ context_topology_form.context_topology(class="form-select") }} {% endif %} </div> <div class="col-sm-2"> - {{ context_form.submit(class='btn btn-primary') }} + {{ context_topology_form.submit(class='btn btn-primary') }} </div> </div> </fieldset> </form> - <form id="select_context" method="POST" enctype="multipart/form-data"> - {{ context_form.hidden_tag() }} + <form id="upload_descriptors" method="POST" enctype="multipart/form-data"> + {{ descriptor_form.hidden_tag() }} <fieldset class="form-group"> <legend>Upload a JSON descriptors file</legend> <div class="row mb-3"> -- GitLab