diff --git a/src/webui/service/__init__.py b/src/webui/service/__init__.py index 75e1036420d0bc88a790fb7b65f4f4900abaaadd..d60cca6597ced52db8e320f3ba1beb2b032be65b 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 f1423e92ed63fa778448978167c1c8e646414885..b57c5735d4b26c541d60a885512fe37a2fd626bc 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 51e903d9ec28c5aaac20cd49e2f97dd7044e12bf..5b8831b7732443830a6f9b1ef8f7da92b4c41cc0 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 abef11e06d6222c6bbab527f3a41ccdc5918480f..b138592fccd3f65831673912d04aba79f2dd3c72 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 9b1b088579c5b01218316bf1c96b5208ff854609..1ca5329e396fc1744a2ae26207d37d84b1bca27d 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 ddf7a8d0de921219274135fe71fed683b078a4e7..932103acce76f051a39ec9e34a50e61003e09d7f 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 Binary files a/src/webui/service/static/topology_icons/network.png and b/src/webui/service/static/topology_icons/network.png differ diff --git a/src/webui/service/templates/base.html b/src/webui/service/templates/base.html index 5d7801d11880e89869120985307c6b43416f5a05..bee98ee82da3482caf1fad930d03d30572ba287d 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 db390939ff926b5bbfbfc6507b0f4e79695f3693..43b066cc0227801672fc25780f27e3a699338632 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">