diff --git a/configure_dashboards.sh b/configure_dashboards.sh index edfdfa370538cfb7129e365be5062f16291a67d7..4a32d76deb79bb514b6d547c0e3e2b87ec269e77 100755 --- a/configure_dashboards.sh +++ b/configure_dashboards.sh @@ -25,8 +25,8 @@ INFLUXDB_DATABASE=$(kubectl --namespace $K8S_NAMESPACE get secrets influxdb-secr # GRAFANA_HOSTNAME=$(kubectl get node $K8S_HOSTNAME -o 'jsonpath={.status.addresses[?(@.type=="InternalIP")].address}') # GRAFANA_HOSTNAME=`kubectl get service/webuiservice-public -n ${K8S_NAMESPACE} -o jsonpath='{.spec.clusterIP}'` GRAFANA_HOSTNAME=`kubectl get nodes --selector=node-role.kubernetes.io/master -o jsonpath='{$.items[*].status.addresses[?(@.type=="InternalIP")].address}'` -# GRAFANA_PORT=$(kubectl get service webuiservice-public --namespace $K8S_NAMESPACE -o 'jsonpath={.spec.ports[?(@.port==3000)].nodePort}') -GRAFANA_PORT=`kubectl get service/webuiservice-public -n ${K8S_NAMESPACE} -o jsonpath='{.spec.ports[1].nodePort}'` +GRAFANA_PORT=$(kubectl get service webuiservice-public --namespace $K8S_NAMESPACE -o 'jsonpath={.spec.ports[?(@.port==3000)].nodePort}') +#GRAFANA_PORT=`kubectl get service/webuiservice-public -n ${K8S_NAMESPACE} -o jsonpath='{.spec.ports[1].nodePort}'` GRAFANA_USERNAME="admin" GRAFANA_PASSWORD=${GRAFANA_PASSWORD:-"admin123+"} GRAFANA_URL="http://${GRAFANA_USERNAME}:${GRAFANA_PASSWORD}@${GRAFANA_HOSTNAME}:${GRAFANA_PORT}" diff --git a/manifests/expose_services.yaml b/manifests/expose_services.yaml deleted file mode 100644 index 7e5d2236ab3be6928cd9247a8f5a6e8220a4d1ab..0000000000000000000000000000000000000000 --- a/manifests/expose_services.yaml +++ /dev/null @@ -1,90 +0,0 @@ ---- -apiVersion: v1 -kind: Service -metadata: - name: contextservice-public - labels: - app: contextservice -spec: - type: NodePort - selector: - app: contextservice - ports: - - name: grpc - protocol: TCP - port: 1010 - targetPort: 1010 - - name: http - protocol: TCP - port: 8080 - targetPort: 8080 - nodePort: 30808 ---- -apiVersion: v1 -kind: Service -metadata: - name: redis-tests - labels: - app: contextservice -spec: - type: NodePort - selector: - app: contextservice - ports: - - name: redis - protocol: TCP - port: 6379 - targetPort: 6379 ---- -apiVersion: v1 -kind: Service -metadata: - name: influx-tests - labels: - app: monitoringservice -spec: - type: NodePort - selector: - app: monitoringservice - ports: - - name: influx - protocol: TCP - port: 8086 - targetPort: 8086 ---- -apiVersion: v1 -kind: Service -metadata: - name: deviceservice-public - labels: - app: deviceservice -spec: - type: NodePort - selector: - app: deviceservice - ports: - - name: grpc - protocol: TCP - port: 2020 - targetPort: 2020 ---- -apiVersion: v1 -kind: Service -metadata: - name: computeservice-public - labels: - app: computeservice -spec: - type: NodePort - selector: - app: computeservice - ports: - - name: http - protocol: TCP - port: 8080 - targetPort: 8080 - - name: grpc - protocol: TCP - port: 9090 - targetPort: 9090 ---- \ No newline at end of file diff --git a/open_dashboard.sh b/open_dashboard.sh index 6f87b207c24bfd7e61a8d37740ffe104c2dc85a5..8291a22c75cd2c2b83bedcab2ac0167c56c966a6 100755 --- a/open_dashboard.sh +++ b/open_dashboard.sh @@ -18,8 +18,8 @@ K8S_NAMESPACE=${K8S_NAMESPACE:-'tf-dev'} -GRAFANA_IP=`kubectl get service/webuiservice -n ${K8S_NAMESPACE} -o jsonpath='{.spec.clusterIP}'` -GRAFANA_PORT=`kubectl get service/webuiservice -n ${K8S_NAMESPACE} -o jsonpath='{.spec.ports[1].port}'` +GRAFANA_IP=$(kubectl get service/webuiservice -n ${K8S_NAMESPACE} -o jsonpath='{.spec.clusterIP}') +GRAFANA_PORT=$(kubectl get service webuiservice-public --namespace $K8S_NAMESPACE -o 'jsonpath={.spec.ports[?(@.port==3000)].nodePort}') URL=http://${GRAFANA_IP}:${GRAFANA_PORT} echo Opening Dashboard on URL ${URL} diff --git a/open_webui.sh b/open_webui.sh index 23cb704ad7a2ee5e9134b2727a02f10b6591582b..f33f7036527c74756564f350924951c19ac4f817 100755 --- a/open_webui.sh +++ b/open_webui.sh @@ -16,23 +16,20 @@ K8S_NAMESPACE=${K8S_NAMESPACE:-'tf-dev'} -WEBUI_SERVICE_NAME="service/webuiservice-public" -WEBUI_PROTO=`kubectl get ${WEBUI_SERVICE_NAME} -n ${K8S_NAMESPACE} -o jsonpath='{.spec.ports[0].name}'` -WEBUI_IP=`kubectl get ${WEBUI_SERVICE_NAME} -n ${K8S_NAMESPACE} -o jsonpath='{.spec.clusterIP}'` -WEBUI_PORT=`kubectl get ${WEBUI_SERVICE_NAME} -n ${K8S_NAMESPACE} -o jsonpath='{.spec.ports[0].port}'` -GRAFANA_PORT=`kubectl get ${WEBUI_SERVICE_NAME} -n ${K8S_NAMESPACE} -o jsonpath='{.spec.ports[1].port}'` -URL=${WEBUI_PROTO}://${WEBUI_IP}:${WEBUI_PORT} +WEBUI_SERVICE_NAME="webuiservice-public" +WEBUI_PROTO=`kubectl get service ${WEBUI_SERVICE_NAME} -n ${K8S_NAMESPACE} -o jsonpath='{.spec.ports[0].name}'` +WEBUI_IP=`kubectl get service ${WEBUI_SERVICE_NAME} -n ${K8S_NAMESPACE} -o jsonpath='{.spec.clusterIP}'` +WEBUI_PORT=$(kubectl get service ${WEBUI_SERVICE_NAME} --namespace $K8S_NAMESPACE -o 'jsonpath={.spec.ports[?(@.port==8004)].nodePort}') +GRAFANA_PORT=$(kubectl get service ${WEBUI_SERVICE_NAME} --namespace $K8S_NAMESPACE -o 'jsonpath={.spec.ports[?(@.port==3000)].nodePort}') +# Open WebUI +URL=${WEBUI_PROTO}://${WEBUI_IP}:${WEBUI_PORT} echo Opening web UI on URL ${URL} - # curl -kL ${URL} - python3 -m webbrowser ${URL} +# Open Dashboard URL=${WEBUI_PROTO}://${WEBUI_IP}:${GRAFANA_PORT} - -echo Opening web UI on URL ${URL} - +echo Opening Dashboard on URL ${URL} # curl -kL ${URL} - python3 -m webbrowser ${URL} diff --git a/src/tests/ofc22/.gitignore b/src/tests/ofc22/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..0a3f4400d5c88b1af32c7667d69d2fdc12d5424e --- /dev/null +++ b/src/tests/ofc22/.gitignore @@ -0,0 +1,2 @@ +# Add here your files containing confidential testbed details such as IP addresses, ports, usernames, passwords, etc. +descriptors_real.json diff --git a/src/tests/ofc22/descriptors_emulated.json b/src/tests/ofc22/descriptors_emulated.json new file mode 100644 index 0000000000000000000000000000000000000000..3905fbc59a538185c1baa7d899e48a838864790d --- /dev/null +++ b/src/tests/ofc22/descriptors_emulated.json @@ -0,0 +1,108 @@ +{ + "contexts": [ + { + "context_id": {"context_uuid": {"uuid": "admin"}}, + "topology_ids": [], + "service_ids": [] + } + ], + "topologies": [ + { + "topology_id": {"topology_uuid": {"uuid": "admin"}, "context_id": {"context_uuid": {"uuid": "admin"}}}, + "device_ids": [], + "link_ids": [] + } + ], + "devices": [ + { + "device_id": {"device_uuid": {"uuid": "R1-INF"}}, + "device_type": "emu-packet-router", + "device_config": {"config_rules": [ + {"action": 1, "resource_key": "_connect/address", "resource_value": "127.0.0.1"}, + {"action": 1, "resource_key": "_connect/port", "resource_value": "0"}, + {"action": 1, "resource_key": "_connect/settings", "resource_value": "{\"endpoints\": [{\"sample_types\": [], \"type\": \"optical\", \"uuid\": \"13/0/0\"}, {\"sample_types\": [101, 102, 201, 202], \"type\": \"copper\", \"uuid\": \"13/1/2\"}]}"} + ]}, + "device_operational_status": 1, + "device_drivers": [0], + "device_endpoints": [] + }, + { + "device_id": {"device_uuid": {"uuid": "R2-EMU"}}, + "device_type": "emu-packet-router", + "device_config": {"config_rules": [ + {"action": 1, "resource_key": "_connect/address", "resource_value": "127.0.0.1"}, + {"action": 1, "resource_key": "_connect/port", "resource_value": "0"}, + {"action": 1, "resource_key": "_connect/settings", "resource_value": "{\"endpoints\": [{\"sample_types\": [], \"type\": \"optical\", \"uuid\": \"13/0/0\"}, {\"sample_types\": [101, 102, 201, 202], \"type\": \"copper\", \"uuid\": \"13/1/2\"}]}"} + ]}, + "device_operational_status": 1, + "device_drivers": [0], + "device_endpoints": [] + }, + { + "device_id": {"device_uuid": {"uuid": "R3-INF"}}, + "device_type": "emu-packet-router", + "device_config": {"config_rules": [ + {"action": 1, "resource_key": "_connect/address", "resource_value": "127.0.0.1"}, + {"action": 1, "resource_key": "_connect/port", "resource_value": "0"}, + {"action": 1, "resource_key": "_connect/settings", "resource_value": "{\"endpoints\": [{\"sample_types\": [], \"type\": \"optical\", \"uuid\": \"13/0/0\"}, {\"sample_types\": [101, 102, 201, 202], \"type\": \"copper\", \"uuid\": \"13/1/2\"}]}"} + ]}, + "device_operational_status": 1, + "device_drivers": [0], + "device_endpoints": [] + }, + { + "device_id": {"device_uuid": {"uuid": "R4-EMU"}}, + "device_type": "emu-packet-router", + "device_config": {"config_rules": [ + {"action": 1, "resource_key": "_connect/address", "resource_value": "127.0.0.1"}, + {"action": 1, "resource_key": "_connect/port", "resource_value": "0"}, + {"action": 1, "resource_key": "_connect/settings", "resource_value": "{\"endpoints\": [{\"sample_types\": [], \"type\": \"optical\", \"uuid\": \"13/0/0\"}, {\"sample_types\": [101, 102, 201, 202], \"type\": \"copper\", \"uuid\": \"13/1/2\"}]}"} + ]}, + "device_operational_status": 1, + "device_drivers": [0], + "device_endpoints": [] + }, + { + "device_id": {"device_uuid": {"uuid": "O1-OLS"}}, + "device_type": "emu-optical-line-system", + "device_config": {"config_rules": [ + {"action": 1, "resource_key": "_connect/address", "resource_value": "127.0.0.1"}, + {"action": 1, "resource_key": "_connect/port", "resource_value": "0"}, + {"action": 1, "resource_key": "_connect/settings", "resource_value": "{\"endpoints\": [{\"sample_types\": [], \"type\": \"optical\", \"uuid\": \"aade6001-f00b-5e2f-a357-6a0a9d3de870\"}, {\"sample_types\": [], \"type\": \"optical\", \"uuid\": \"eb287d83-f05e-53ec-ab5a-adf6bd2b5418\"}, {\"sample_types\": [], \"type\": \"optical\", \"uuid\": \"0ef74f99-1acc-57bd-ab9d-4b958b06c513\"}, {\"sample_types\": [], \"type\": \"optical\", \"uuid\": \"50296d99-58cc-5ce7-82f5-fc8ee4eec2ec\"}]}"} + ]}, + "device_operational_status": 1, + "device_drivers": [0], + "device_endpoints": [] + } + ], + "links": [ + { + "link_id": {"link_uuid": {"uuid": "R1-INF/13/0/0==O1-OLS/aade6001-f00b-5e2f-a357-6a0a9d3de870"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "R1-INF"}}, "endpoint_uuid": {"uuid": "13/0/0"}}, + {"device_id": {"device_uuid": {"uuid": "O1-OLS"}}, "endpoint_uuid": {"uuid": "aade6001-f00b-5e2f-a357-6a0a9d3de870"}} + ] + }, + { + "link_id": {"link_uuid": {"uuid": "R2-EMU/13/0/0==O1-OLS/eb287d83-f05e-53ec-ab5a-adf6bd2b5418"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "R2-EMU"}}, "endpoint_uuid": {"uuid": "13/0/0"}}, + {"device_id": {"device_uuid": {"uuid": "O1-OLS"}}, "endpoint_uuid": {"uuid": "eb287d83-f05e-53ec-ab5a-adf6bd2b5418"}} + ] + }, + { + "link_id": {"link_uuid": {"uuid": "R3-INF/13/0/0==O1-OLS/0ef74f99-1acc-57bd-ab9d-4b958b06c513"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "R3-INF"}}, "endpoint_uuid": {"uuid": "13/0/0"}}, + {"device_id": {"device_uuid": {"uuid": "O1-OLS"}}, "endpoint_uuid": {"uuid": "0ef74f99-1acc-57bd-ab9d-4b958b06c513"}} + ] + }, + { + "link_id": {"link_uuid": {"uuid": "R4-EMU/13/0/0==O1-OLS/50296d99-58cc-5ce7-82f5-fc8ee4eec2ec"}}, + "link_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": "R4-EMU"}}, "endpoint_uuid": {"uuid": "13/0/0"}}, + {"device_id": {"device_uuid": {"uuid": "O1-OLS"}}, "endpoint_uuid": {"uuid": "50296d99-58cc-5ce7-82f5-fc8ee4eec2ec"}} + ] + } + ] +} diff --git a/src/tests/ofc22/redeploy_webui.sh b/src/tests/ofc22/redeploy_webui.sh new file mode 100755 index 0000000000000000000000000000000000000000..975f84a9d3b75e00a809acd336d844973cb26897 --- /dev/null +++ b/src/tests/ofc22/redeploy_webui.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# 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. + +export COMPONENT="webui" +export IMAGE_TAG="ofc22" +export K8S_NAMESPACE="ofc22" +export K8S_HOSTNAME="kubernetes-master" +export GRAFANA_PASSWORD="admin123+" + +# Constants +TMP_FOLDER="./tmp" + +# Create a tmp folder for files modified during the deployment +TMP_MANIFESTS_FOLDER="$TMP_FOLDER/manifests" +mkdir -p $TMP_MANIFESTS_FOLDER +TMP_LOGS_FOLDER="$TMP_FOLDER/logs" +mkdir -p $TMP_LOGS_FOLDER + +echo "Processing '$COMPONENT' component..." +IMAGE_NAME="$COMPONENT:$IMAGE_TAG" + +echo " Building Docker image..." +BUILD_LOG="$TMP_LOGS_FOLDER/build_${COMPONENT}.log" +docker build -t "$IMAGE_NAME" -f ./src/"$COMPONENT"/Dockerfile ./src/ > "$BUILD_LOG" + +sleep 1 + +echo " Deploying '$COMPONENT' component to Kubernetes..." +kubectl --namespace $K8S_NAMESPACE scale deployment --replicas=0 ${COMPONENT}service +kubectl --namespace $K8S_NAMESPACE scale deployment --replicas=1 ${COMPONENT}service +printf "\n" + +sleep 1 + +echo "Waiting for '$COMPONENT' component..." +kubectl wait --namespace $K8S_NAMESPACE --for='condition=available' --timeout=300s deployment/${COMPONENT}service +printf "\n" + +echo "Configuring DataStores and Dashboards..." +./configure_dashboards.sh +printf "\n\n" + +echo "Reporting Deployment..." +kubectl --namespace $K8S_NAMESPACE get all +printf "\n" + +echo "Done!" diff --git a/src/tests/ofc22/tests/BuildDescriptors.py b/src/tests/ofc22/tests/BuildDescriptors.py new file mode 100644 index 0000000000000000000000000000000000000000..5c5419190487eb5089e4a30f523dca43fa3870f2 --- /dev/null +++ b/src/tests/ofc22/tests/BuildDescriptors.py @@ -0,0 +1,35 @@ +# 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. + +import copy, json, sys +from .Objects import CONTEXTS, DEVICES, LINKS, TOPOLOGIES + +def main(): + with open('tests/ofc22/descriptors_emulated.json', 'w', encoding='UTF-8') as f: + devices = [] + for device,connect_rules in DEVICES: + device = copy.deepcopy(device) + device['device_config']['config_rules'].extend(connect_rules) + devices.append(device) + + f.write(json.dumps({ + 'contexts': CONTEXTS, + 'topologies': TOPOLOGIES, + 'devices': devices, + 'links': LINKS + })) + return 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/src/tests/ofc22/tests/LoadDescriptors.py b/src/tests/ofc22/tests/LoadDescriptors.py new file mode 100644 index 0000000000000000000000000000000000000000..c84c2549096f7483699f79de4aaf9b52fb181072 --- /dev/null +++ b/src/tests/ofc22/tests/LoadDescriptors.py @@ -0,0 +1,40 @@ +# 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. + +import json, logging, sys +from common.Settings import get_setting +from context.client.ContextClient import ContextClient +from context.proto.context_pb2 import Context, ContextId, Device, Empty, Link, Topology +from device.client.DeviceClient import DeviceClient + +LOGGER = logging.getLogger(__name__) +LOGGER.setLevel(logging.DEBUG) + +def main(): + context_client = ContextClient( + get_setting('CONTEXTSERVICE_SERVICE_HOST'), get_setting('CONTEXTSERVICE_SERVICE_PORT_GRPC')) + device_client = DeviceClient( + get_setting('DEVICESERVICE_SERVICE_HOST'), get_setting('DEVICESERVICE_SERVICE_PORT_GRPC')) + + with open('tests/ofc22/descriptors.json', 'r', encoding='UTF-8') as f: + descriptors = json.loads(f.read()) + + for context in descriptors['contexts' ]: context_client.SetContext (Context (**context )) + for topology in descriptors['topologies']: context_client.SetTopology(Topology(**topology)) + for device in descriptors['devices' ]: device_client .AddDevice (Device (**device )) + for link in descriptors['links' ]: context_client.SetLink (Link (**link )) + return 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/src/tests/ofc22/tests/Objects.py b/src/tests/ofc22/tests/Objects.py index a47ba7c16d841ca51c9889f83a0796d8540bb621..6205b5e4ada67749f8230e99badca048a2372c2b 100644 --- a/src/tests/ofc22/tests/Objects.py +++ b/src/tests/ofc22/tests/Objects.py @@ -61,7 +61,7 @@ except ImportError: DEVICE_O1_ADDRESS = '0.0.0.0' DEVICE_O1_PORT = 4900 -# USE_REAL_DEVICES = False # Uncomment to force to use emulated devices +#USE_REAL_DEVICES = False # Uncomment to force to use emulated devices def json_endpoint_ids(device_id : Dict, endpoint_descriptors : List[Tuple[str, str, List[int]]]): return [ diff --git a/src/webui/Config.py b/src/webui/Config.py index f6fc9125f90e7a78a606129af3b7f55964174ab3..e7720a405e2874c3679f9f507df2fdffc610f84a 100644 --- a/src/webui/Config.py +++ b/src/webui/Config.py @@ -25,6 +25,7 @@ WEBUI_SERVICE_PORT = 8004 METRICS_PORT = 9192 SECRET_KEY = '>s&}24@{]]#k3&^5$f3#?6?h3{W@[}/7z}2pa]>{3&5%RP<)[(' +MAX_CONTENT_LENGTH = 1024*1024 HOST = '0.0.0.0' # accepts connections coming from any ADDRESS diff --git a/src/webui/service/__main__.py b/src/webui/service/__main__.py index 38bc0f790d7030ef1ed4ede0085a6161db1f8cec..5dd20aab74751390a11b32e9ae2c63aadb9e364e 100644 --- a/src/webui/service/__main__.py +++ b/src/webui/service/__main__.py @@ -16,7 +16,7 @@ import os, sys, logging from prometheus_client import start_http_server from common.Settings import wait_for_environment_variables from webui.service import create_app -from webui.Config import WEBUI_SERVICE_PORT, LOG_LEVEL, METRICS_PORT, HOST, SECRET_KEY, DEBUG +from webui.Config import MAX_CONTENT_LENGTH, WEBUI_SERVICE_PORT, LOG_LEVEL, METRICS_PORT, HOST, SECRET_KEY, DEBUG def main(): service_port = os.environ.get('WEBUISERVICE_SERVICE_PORT', WEBUI_SERVICE_PORT) @@ -37,7 +37,10 @@ def main(): start_http_server(metrics_port) - app = create_app(use_config={'SECRET_KEY': SECRET_KEY}) + app = create_app(use_config={ + 'SECRET_KEY': SECRET_KEY, + 'MAX_CONTENT_LENGTH': MAX_CONTENT_LENGTH, + }) app.run(host=host, port=service_port, debug=debug) logger.info('Bye') diff --git a/src/webui/service/device/routes.py b/src/webui/service/device/routes.py index f4e58d3b0f92ef4e002c086ccc6092c9d5d397ef..2221240e5f974dab60e116ee09c81cc9dd41f039 100644 --- a/src/webui/service/device/routes.py +++ b/src/webui/service/device/routes.py @@ -17,7 +17,7 @@ from device.client.DeviceClient import DeviceClient from context.client.ContextClient import ContextClient from webui.Config import (CONTEXT_SERVICE_ADDRESS, CONTEXT_SERVICE_PORT, DEVICE_SERVICE_ADDRESS, DEVICE_SERVICE_PORT) -from webui.proto.context_pb2 import (ContextId, DeviceList, DeviceId, +from webui.proto.context_pb2 import (ContextId, DeviceList, DeviceId, Empty, Device, DeviceDriverEnum, DeviceOperationalStatusEnum, ConfigActionEnum, ConfigRule, TopologyIdList, TopologyList) from webui.service.device.forms import AddDeviceForm @@ -32,10 +32,8 @@ def home(): if context_uuid == "-": flash("Please select a context!", "warning") return redirect(url_for("main.home")) - request: ContextId = ContextId() - request.context_uuid.uuid = session.get('context_uuid', '-') context_client.connect() - response: DeviceList = context_client.ListDevices(request) + response: DeviceList = context_client.ListDevices(Empty()) context_client.close() return render_template('device/home.html', devices=response.devices, dde=DeviceDriverEnum, @@ -45,12 +43,6 @@ def home(): def add(): form = AddDeviceForm() - request: ContextId = ContextId() - request.context_uuid.uuid = session.get('context_uuid', '-') - context_client.connect() - response: TopologyIdList = context_client.ListTopologyIds(request) - context_client.close() - # listing enum values form.operational_status.choices = [(-1, 'Select...')] for key, value in DeviceOperationalStatusEnum.DESCRIPTOR.values_by_name.items(): diff --git a/src/webui/service/main/forms.py b/src/webui/service/main/forms.py index 0dc80c7a20628921b03fc8fd8d0189a75f1053a4..892905e48e7b22bf9c1b27f27cf16f68f8b5aa94 100644 --- a/src/webui/service/main/forms.py +++ b/src/webui/service/main/forms.py @@ -14,12 +14,20 @@ # external imports from flask_wtf import FlaskForm -from wtforms import SelectField, SubmitField -from wtforms.validators import DataRequired, Length +from flask_wtf.file import FileAllowed +from wtforms import SelectField, FileField, SubmitField +#from wtforms.validators import DataRequired, Length class ContextForm(FlaskForm): - context = SelectField('Context', - choices=[], - validators=[DataRequired(), Length(min=1)]) - submit = SubmitField('Select') + context = SelectField( 'Context', + choices=[], + validators=[ + #DataRequired(), + #Length(min=1) + ]) + 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 6a74b6f53fdcdd1c46128c695e78a4dab81cf491..81b8fe8c08c85f9ef63ac5df62c7cb9fc042e92c 100644 --- a/src/webui/service/main/routes.py +++ b/src/webui/service/main/routes.py @@ -12,38 +12,89 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json import logging import sys -from flask import redirect, render_template, Blueprint, flash, session, url_for -from webui.Config import CONTEXT_SERVICE_ADDRESS, CONTEXT_SERVICE_PORT +from flask import redirect, render_template, Blueprint, flash, session, url_for, request +from webui.Config import (CONTEXT_SERVICE_ADDRESS, CONTEXT_SERVICE_PORT, + DEVICE_SERVICE_ADDRESS, DEVICE_SERVICE_PORT) from context.client.ContextClient import ContextClient -from webui.proto.context_pb2 import Empty +from device.client.DeviceClient import DeviceClient +from webui.proto.context_pb2 import Context, Device, Empty, Link, Topology from webui.service.main.forms import ContextForm main = Blueprint('main', __name__) context_client: ContextClient = ContextClient(CONTEXT_SERVICE_ADDRESS, CONTEXT_SERVICE_PORT) +device_client: DeviceClient = DeviceClient(DEVICE_SERVICE_ADDRESS, DEVICE_SERVICE_PORT) logger = logging.getLogger(__name__) +def process_descriptor(item_name_singluar, item_name_plural, grpc_method, grpc_class, items): + num_ok, num_err = 0, 0 + for item in items: + try: + grpc_method(grpc_class(**item)) + num_ok += 1 + except Exception as e: # pylint: disable=broad-except + flash(f'Unable to add {item_name_singluar} {str(item)}: {str(e)}', 'error') + num_err += 1 + if num_ok : flash(f'{str(num_ok)} {item_name_plural} added', 'success') + if num_err: flash(f'{str(num_err)} {item_name_plural} failed', 'danger') + +def process_descriptors(descriptors): + logger.warning(str(descriptors.data)) + logger.warning(str(descriptors.name)) + try: + logger.warning(str(request.files)) + descriptors_file = request.files[descriptors.name] + logger.warning(str(descriptors_file)) + descriptors_data = descriptors_file.read() + logger.warning(str(descriptors_data)) + descriptors = json.loads(descriptors_data) + logger.warning(str(descriptors)) + except Exception as e: # pylint: disable=broad-except + flash(f'Unable to load descriptor file: {str(e)}', 'danger') + return + + process_descriptor('Context', 'Contexts', context_client.SetContext, Context, descriptors['contexts' ]) + process_descriptor('Topology', 'Topologies', context_client.SetTopology, Topology, descriptors['topologies']) + process_descriptor('Device', 'Devices', device_client .AddDevice, Device, descriptors['devices' ]) + process_descriptor('Link', 'Links', context_client.SetLink, Link, descriptors['links' ]) + @main.route('/', methods=['GET', 'POST']) def home(): - # flash('This is an info message', 'info') - # flash('This is a danger message', 'danger') context_client.connect() - response = context_client.ListContextIds(Empty()) - context_client.close() - context_form: ContextForm = ContextForm() - context_form.context.choices.append(('', 'Select...')) - for context in response.context_ids: - context_form.context.choices.append((context.context_uuid.uuid, context.context_uuid)) - 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') - if 'context_uuid' in session: - context_form.context.data = session['context_uuid'] - - return render_template('main/home.html', context_form=context_form) + device_client.connect() + try: + logger.warning('home: {:s}'.format(str(request))) + # flash('This is an info message', 'info') + # flash('This is a danger message', 'danger') + response = context_client.ListContextIds(Empty()) + context_form: ContextForm = ContextForm() + context_form.context.choices.append(('', 'Select...')) + for context in response.context_ids: + context_form.context.choices.append((context.context_uuid.uuid, context.context_uuid)) + logger.warning('context_form.data = {:s}'.format(str(context_form.data))) + logger.warning('before validate_on_submit') + if context_form.validate_on_submit(): + logger.warning('inside validate_on_submit') + logger.warning('context_form.context.data = {:s}'.format(str(context_form.context.data))) + logger.warning('context_form.descriptors.data = {:s}'.format(str(context_form.descriptors.data))) + if context_form.context.data: + session['context_uuid'] = context_form.context.data + flash(f'The context was successfully set to `{context_form.context.data}`.', 'success') + if context_form.descriptors.data: + process_descriptors(context_form.descriptors) + logger.warning('context_form.errors = {:s}'.format(str(context_form.errors))) + if 'context_uuid' in session: + context_form.context.data = session['context_uuid'] + return render_template('main/home.html', context_form=context_form) + except: + logger.exception('Something failed') + finally: + context_client.close() + device_client.close() @main.get('/about') diff --git a/src/webui/service/templates/main/home.html b/src/webui/service/templates/main/home.html index e7633bbdbfaf69b5982890045d8e16fe2e795dc8..2a75e19608e6d0ee2c82a945befe37a9dc8abbf7 100644 --- a/src/webui/service/templates/main/home.html +++ b/src/webui/service/templates/main/home.html @@ -28,23 +28,43 @@ {% endfor %} - <h2>Select the working context</h2> - <form id="select_context" method="POST"> + <form id="select_context" method="POST" enctype="multipart/form-data"> {{ context_form.hidden_tag() }} <fieldset class="form-group"> - <div class="input-group mb-3"> - - {% if context_form.context.errors %} - {{ context_form.context(class="form-select is-invalid") }} - <div class="invalid-feedback"> - {% for error in context_form.context.errors %} - <span>{{ error }}</span> - {% endfor %} + <legend>Select the working context, or upload a JSON descriptors file</legend> + <div class="row mb-3"> + <div class="row mb-3"> + {{ context_form.context.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {% if context_form.context.errors %} + {{ context_form.context(class="form-select is-invalid") }} + <div class="invalid-feedback"> + {% for error in context_form.context.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ context_form.context(class="form-select") }} + {% endif %} </div> - {% else %} - {{ context_form.context(class="form-select") }} - {% endif %} + </div> + {{ context_form.descriptors.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {% if context_form.descriptors.errors %} + {{ context_form.descriptors(class="form-control is-invalid") }} + <div class="invalid-feedback"> + {% for error in context_form.descriptors.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ context_form.descriptors(class="form-control") }} + {% endif %} + </div> + </div> + + <div class="d-grid gap-2 d-md-flex justify-content-md-start"> {{ context_form.submit(class='btn btn-primary') }} </div> </fieldset>