diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
index f45851445..533343870 100644
--- a/.github/workflows/testing.yml
+++ b/.github/workflows/testing.yml
@@ -76,7 +76,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
with:
- node-version: 18.13.0
+ node-version: 18.18.0
- name: Install Chromium Browser
run: sudo apt install chromium-browser
- name: Install dependencies
@@ -99,7 +99,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
with:
- node-version: 18.13.0
+ node-version: 18.18.0
- name: Install dependencies
run: npm install && npm ci
working-directory: ./modules/ui
diff --git a/cmd/prepare b/cmd/prepare
index 252d505a0..9e68f734d 100755
--- a/cmd/prepare
+++ b/cmd/prepare
@@ -20,6 +20,6 @@
echo Installing system dependencies
# Install system dependencies
-sudo apt-get update && sudo apt-get install openvswitch-common openvswitch-switch python3 libpangocairo-1.0-0
+sudo apt-get update && sudo apt-get install openvswitch-common openvswitch-switch python3 libpangocairo-1.0-0 ethtool
echo Finished installing system dependencies
diff --git a/framework/python/src/api/api.py b/framework/python/src/api/api.py
index 758da6656..7f72d8810 100644
--- a/framework/python/src/api/api.py
+++ b/framework/python/src/api/api.py
@@ -95,6 +95,9 @@ def __init__(self, test_run):
self._router.add_api_route("/device/edit",
self.edit_device,
methods=["POST"])
+
+ self._router.add_api_route("/system/modules",
+ self.get_test_modules)
# Allow all origins to access the API
origins = ["*"]
@@ -551,6 +554,20 @@ async def get_results(self, response: Response,
response.status_code = 404
return self._generate_msg(False, "Test results could not be found")
+ async def get_test_modules(self):
+
+ LOGGER.debug("Received request to list test modules")
+
+ test_modules = []
+
+ for test_module in self._get_test_run().get_test_orc().get_test_modules():
+
+ # Only add module if it is an actual, enabled test module
+ if (test_module.enabled and test_module.enable_container):
+ test_modules.append(test_module.display_name)
+
+ return test_modules
+
def _validate_device_json(self, json_obj):
# Check all required properties are present
@@ -575,3 +592,6 @@ def _validate_device_json(self, json_obj):
return False
return True
+
+ def _get_test_run(self):
+ return self._test_run
diff --git a/framework/python/src/common/testreport.py b/framework/python/src/common/testreport.py
index 45687b866..e2a187cce 100644
--- a/framework/python/src/common/testreport.py
+++ b/framework/python/src/common/testreport.py
@@ -58,7 +58,10 @@ def __init__(self,
self._report_url = ''
self._cur_page = 0
# Placeholder until available in json report
- self._version = 'v1.2.2'
+ self._version = 'v1.3-alpha'
+
+ def get_mac_addr(self):
+ return self._mac_addr
def get_mac_addr(self):
return self._mac_addr
@@ -94,6 +97,12 @@ def get_report_url(self):
def set_mac_addr(self, mac_addr):
self._mac_addr = mac_addr
+ def set_mac_addr(self, mac_addr):
+ self._mac_addr = mac_addr
+
+ def set_mac_addr(self, mac_addr):
+ self._mac_addr = mac_addr
+
def to_json(self):
report_json = {}
diff --git a/framework/python/src/core/testrun.py b/framework/python/src/core/testrun.py
index 71070e2cc..22607a520 100644
--- a/framework/python/src/core/testrun.py
+++ b/framework/python/src/core/testrun.py
@@ -100,14 +100,18 @@ def __init__(self,
if validate:
self._session.add_runtime_param('validate')
- self.load_all_devices()
-
self._net_orc = net_orc.NetworkOrchestrator(
session=self._session)
self._test_orc = test_orc.TestOrchestrator(
self._session,
self._net_orc)
+ # Load device repository
+ self.load_all_devices()
+
+ # Load test modules
+ self._test_orc.start()
+
if self._no_ui:
# Check Testrun is able to start
@@ -324,8 +328,6 @@ def start(self):
if self._net_only:
LOGGER.info('Network only option configured, no tests will be run')
else:
- self._test_orc.start()
-
self.get_net_orc().get_listener().register_callback(
self._device_stable,
[NetworkEvent.DEVICE_STABLE]
@@ -382,6 +384,9 @@ def get_config_file(self):
def get_net_orc(self):
return self._net_orc
+ def get_test_orc(self):
+ return self._test_orc
+
def _start_network(self):
# Start the network orchestrator
if not self.get_net_orc().start():
diff --git a/framework/python/src/net_orc/ip_control.py b/framework/python/src/net_orc/ip_control.py
index 1eeafc1a9..506b23a95 100644
--- a/framework/python/src/net_orc/ip_control.py
+++ b/framework/python/src/net_orc/ip_control.py
@@ -42,7 +42,7 @@ def add_namespace(self, namespace):
return success
def check_interface_status(self, interface_name):
- output = util.run_command(cmd=f'ip link show {interface_name}',output=True)
+ output = util.run_command(cmd=f'ip link show {interface_name}', output=True)
if 'state DOWN ' in output[0]:
return False
else:
@@ -81,6 +81,22 @@ def get_links(self):
netns_links.append(interface_name.strip())
return netns_links
+ def get_iface_connection_stats(self, iface):
+ """Extract information about the physical connection"""
+ response = util.run_command(f'ethtool {iface}')
+ if len(response[1]) == 0:
+ return response[0]
+ else:
+ return None
+
+ def get_iface_port_stats(self, iface):
+ """Extract information about packets connection"""
+ response = util.run_command(f'ethtool -S {iface}')
+ if len(response[1]) == 0:
+ return response[0]
+ else:
+ return None
+
def get_namespaces(self):
result = util.run_command('ip netns list')
#Strip ID's from the namespace results
diff --git a/framework/python/src/net_orc/network_orchestrator.py b/framework/python/src/net_orc/network_orchestrator.py
index aa23f1918..11c5918a9 100644
--- a/framework/python/src/net_orc/network_orchestrator.py
+++ b/framework/python/src/net_orc/network_orchestrator.py
@@ -173,6 +173,7 @@ def _device_discovered(self, mac_addr):
# Ignore discovered device
return
+ self._get_port_stats(pre_monitor=True)
self._monitor_in_progress = True
LOGGER.debug(
@@ -203,6 +204,8 @@ def _device_discovered(self, mac_addr):
with open(runtime_device_conf, 'w', encoding='utf-8') as f:
json.dump(self._session.get_target_device().to_config_json(), f, indent=2)
+ self._get_conn_stats()
+
if device.ip_addr is None:
LOGGER.info(
f'Timed out whilst waiting for {mac_addr} to obtain an IP address')
@@ -216,6 +219,31 @@ def _device_discovered(self, mac_addr):
self._start_device_monitor(device)
+ def _get_conn_stats(self):
+ """ Extract information about the physical connection
+ and store it to a file for the conn test module to access"""
+ dev_int = self._session.get_device_interface()
+ conn_stats = self._ip_ctrl.get_iface_connection_stats(dev_int)
+ if conn_stats is not None:
+ eth_out_file = os.path.join(NET_DIR, 'ethtool_conn_stats.txt')
+ with open(eth_out_file, 'w', encoding='utf-8') as f:
+ f.write(conn_stats)
+ else:
+ LOGGER.error('Failed to generate connection stats')
+
+ def _get_port_stats(self, pre_monitor=True):
+ """ Extract information about the port statistics
+ and store it to a file for the conn test module to access"""
+ dev_int = self._session.get_device_interface()
+ port_stats = self._ip_ctrl.get_iface_port_stats(dev_int)
+ if port_stats is not None:
+ suffix = 'pre_monitor' if pre_monitor else 'post_monitor'
+ eth_out_file = os.path.join(NET_DIR, f'ethtool_port_stats_{suffix}.txt')
+ with open(eth_out_file, 'w', encoding='utf-8') as f:
+ f.write(port_stats)
+ else:
+ LOGGER.error('Failed to generate port stats')
+
def monitor_in_progress(self):
return self._monitor_in_progress
@@ -261,6 +289,7 @@ def _start_device_monitor(self, device):
wrpcap(os.path.join(device_runtime_dir, 'monitor.pcap'),
self._monitor_packets)
self._monitor_in_progress = False
+ self._get_port_stats(pre_monitor=False)
self.get_listener().call_callback(NetworkEvent.DEVICE_STABLE,
device.mac_addr)
@@ -498,23 +527,22 @@ def _start_network_service(self, net_module):
try:
client = docker.from_env()
net_module.container = client.containers.run(
- net_module.image_name,
- auto_remove=True,
- cap_add=['NET_ADMIN'],
- name=net_module.container_name,
- hostname=net_module.container_name,
- # Undetermined version of docker seems to have broken
- # DNS configuration (/etc/resolv.conf) Re-add when/if
- # this network is utilized and DNS issue is resolved
- #network=PRIVATE_DOCKER_NET,
- privileged=True,
- detach=True,
- mounts=net_module.mounts,
- environment={
- 'TZ': self.get_session().get_timezone(),
- 'HOST_USER': util.get_host_user()
- }
- )
+ net_module.image_name,
+ auto_remove=True,
+ cap_add=['NET_ADMIN'],
+ name=net_module.container_name,
+ hostname=net_module.container_name,
+ # Undetermined version of docker seems to have broken
+ # DNS configuration (/etc/resolv.conf) Re-add when/if
+ # this network is utilized and DNS issue is resolved
+ #network=PRIVATE_DOCKER_NET,
+ privileged=True,
+ detach=True,
+ mounts=net_module.mounts,
+ environment={
+ 'TZ': self.get_session().get_timezone(),
+ 'HOST_USER': util.get_host_user()
+ })
except docker.errors.ContainerError as error:
LOGGER.error('Container run error')
LOGGER.error(error)
diff --git a/modules/test/conn/README.md b/modules/test/conn/README.md
index cf79c3efb..c2f6377c6 100644
--- a/modules/test/conn/README.md
+++ b/modules/test/conn/README.md
@@ -14,6 +14,9 @@ Within the ```python/src``` directory, the below tests are executed. A few dhcp
| ID | Description | Expected Behavior | Required Result |
|------------------------------|----------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------|
+| connection.port_link | The network switch port connected to the device has an active link without errors | When the etherent cable is connected to the port, the device triggers the port to its enabled \"Link UP\" (LEDs illuminate on device and switch ports if present) state, and the switch shows no errors with the LEDs and when interrogated with a \"show interface\" command on most network switches. | Required |
+| connection.port_speed | The network switch port connected to the device has auto-negotiated a speed that is 10 Mbps or higher | When the ethernet cable is connected to the port, the device autonegotiates a speed that can be checked with the \"show interface\" command on most network switches. The output of this command must also show that the \"configured speed\" is set to \"auto\". | Required |
+| connection.port_duplex | The network switch port connected to the device has auto-negotiated full-duplex. | When the ethernet cable is connected to the port, the device autonegotiates a full-duplex connection. | Required |
| connection.dhcp_address | The device under test has received an IP address from the DHCP server and responds to an ICMP echo (ping) request | The device is not set up with a static IP address. The device accepts an IP address from a DHCP server (RFC 2131) and responds successfully to an ICMP echo (ping) request. | Required |
| connection.mac_address | Check and note device physical address. | N/A | Required |
| connection.mac_oui | The device under test has a MAC address prefix that is registered against a known manufacturer. | The MAC address prefix is registered in the IEEE Organizationally Unique Identifier database. | Required |
diff --git a/modules/test/conn/conf/module_config.json b/modules/test/conn/conf/module_config.json
index 888a06c48..5289e7eb0 100644
--- a/modules/test/conn/conf/module_config.json
+++ b/modules/test/conn/conf/module_config.json
@@ -13,6 +13,24 @@
"timeout": 1800
},
"tests": [
+ {
+ "name": "connection.port_link",
+ "test_description": "The network switch port connected to the device has an active link without errors",
+ "expected_behavior": "When the etherent cable is connected to the port, the device triggers the port to its enabled \"Link UP\" (LEDs illuminate on device and switch ports if present) state, and the switch shows no errors with the LEDs and when interrogated with a \"show interface\" command on most network switches.",
+ "required_result": "Required"
+ },
+ {
+ "name": "connection.port_speed",
+ "test_description": "The network switch port connected to the device has auto-negotiated a speed that is 10 Mbps or higher",
+ "expected_behavior": "When the ethernet cable is connected to the port, the device autonegotiates a speed that can be checked with the \"show interface\" command on most network switches. The output of this command must also show that the \"configured speed\" is set to \"auto\".",
+ "required_result": "Required"
+ },
+ {
+ "name": "connection.port_duplex",
+ "test_description": "The network switch port connected to the device has auto-negotiated full-duplex",
+ "expected_behavior": "When the ethernet cable is connected to the port, the device autonegotiates a full-duplex connection.",
+ "required_result": "Required"
+ },
{
"name": "connection.switch.arp_inspection",
"test_description": "The device implements ARP correctly as per RFC826",
diff --git a/modules/test/conn/python/src/connection_module.py b/modules/test/conn/python/src/connection_module.py
index 34e129103..7df9aa3d9 100644
--- a/modules/test/conn/python/src/connection_module.py
+++ b/modules/test/conn/python/src/connection_module.py
@@ -20,15 +20,16 @@
from dhcp1.client import Client as DHCPClient1
from dhcp2.client import Client as DHCPClient2
from dhcp_util import DHCPUtil
+from port_stats_util import PortStatsUtil
LOG_NAME = 'test_connection'
-LOGGER = None
OUI_FILE = '/usr/local/etc/oui.txt'
STARTUP_CAPTURE_FILE = '/runtime/device/startup.pcap'
MONITOR_CAPTURE_FILE = '/runtime/device/monitor.pcap'
DHCP_CAPTURE_FILE = '/runtime/network/dhcp-1.pcap'
SLAAC_PREFIX = 'fd10:77be:4186'
TR_CONTAINER_MAC_PREFIX = '9a:02:57:1e:8f:'
+LOGGER = None
# Should be at least twice as much as the max lease time
# set in the DHCP server
@@ -38,10 +39,15 @@
class ConnectionModule(TestModule):
"""Connection Test module"""
- def __init__(self, module):
- super().__init__(module_name=module, log_name=LOG_NAME)
+ def __init__(self, module, log_dir=None, conf_file=None, results_dir=None):
+ super().__init__(module_name=module,
+ log_name=LOG_NAME,
+ log_dir=log_dir,
+ conf_file=conf_file,
+ results_dir=results_dir)
global LOGGER
LOGGER = self._get_logger()
+ self._port_stats = PortStatsUtil(logger=LOGGER)
self.dhcp1_client = DHCPClient1()
self.dhcp2_client = DHCPClient2()
self._dhcp_util = DHCPUtil(self.dhcp1_client, self.dhcp2_client, LOGGER)
@@ -74,6 +80,18 @@ def __init__(self, module):
# response = self.dhcp1_client.set_dhcp_range('10.10.10.20','10.10.10.30')
# print("Set Range: " + str(response))
+ def _connection_port_link(self):
+ LOGGER.info('Running connection.port_link')
+ return self._port_stats.connection_port_link_test()
+
+ def _connection_port_speed(self):
+ LOGGER.info('Running connection.port_speed')
+ return self._port_stats.connection_port_speed_test()
+
+ def _connection_port_duplex(self):
+ LOGGER.info('Running connection.port_duplex')
+ return self._port_stats.connection_port_duplex_test()
+
def _connection_switch_arp_inspection(self):
LOGGER.info('Running connection.switch.arp_inspection')
diff --git a/modules/test/conn/python/src/port_stats_util.py b/modules/test/conn/python/src/port_stats_util.py
new file mode 100644
index 000000000..d923501eb
--- /dev/null
+++ b/modules/test/conn/python/src/port_stats_util.py
@@ -0,0 +1,141 @@
+# Copyright 2023 Google LLC
+#
+# 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
+#
+# https://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.
+"""Module that contains various methods for validating the Port statistics """
+
+import os
+
+ETHTOOL_CONN_STATS_FILE = 'runtime/network/ethtool_conn_stats.txt'
+ETHTOOL_PORT_STATS_PRE_FILE = (
+ 'runtime/network/ethtool_port_stats_pre_monitor.txt')
+ETHTOOL_PORT_STATS_POST_FILE = (
+ 'runtime/network/ethtool_port_stats_post_monitor.txt')
+
+LOG_NAME = 'port_stats_util'
+LOGGER = None
+
+
+class PortStatsUtil():
+ """Helper class for various tests concerning Port behavior"""
+
+ def __init__(self,
+ logger,
+ ethtool_conn_stats_file=ETHTOOL_CONN_STATS_FILE,
+ ethtool_port_stats_pre_file=ETHTOOL_PORT_STATS_PRE_FILE,
+ ethtool_port_stats_post_file=ETHTOOL_PORT_STATS_POST_FILE):
+ self.ethtool_conn_stats_file = ethtool_conn_stats_file
+ self.ethtool_port_stats_pre_file = ethtool_port_stats_pre_file
+ self.ethtool_port_stats_post_file = ethtool_port_stats_post_file
+ global LOGGER
+ LOGGER = logger
+ self.conn_stats = self._read_stats_file(self.ethtool_conn_stats_file)
+
+ def is_autonegotiate(self):
+ auto_negotiation = False
+ auto_negotiation_status = self._get_stat_option(stats=self.conn_stats,
+ option='Auto-negotiation:')
+ if auto_negotiation_status is not None:
+ auto_negotiation = 'on' in auto_negotiation_status
+ return auto_negotiation
+
+ def connection_port_link_test(self):
+ stats_pre = self._read_stats_file(self.ethtool_port_stats_pre_file)
+ stats_post = self._read_stats_file(self.ethtool_port_stats_post_file)
+ result = None
+ description = ''
+ details = ''
+ if stats_pre is None or stats_pre is None:
+ result = 'Error'
+ description = 'Port stats not available'
+ else:
+ tx_errors_pre = self._get_stat_option(stats=stats_pre,
+ option='tx_errors:')
+ tx_errors_post = self._get_stat_option(stats=stats_post,
+ option='tx_errors:')
+ rx_errors_pre = self._get_stat_option(stats=stats_pre,
+ option='rx_errors:')
+ rx_errors_post = self._get_stat_option(stats=stats_post,
+ option='rx_errors:')
+ tx_errors = int(tx_errors_post) - int(tx_errors_pre)
+ rx_errors = int(rx_errors_post) - int(rx_errors_pre)
+ if tx_errors > 0 or rx_errors > 0:
+ result = False
+ description = 'Port errors detected'
+ details = f'TX errors: {tx_errors}, RX errors: {rx_errors}'
+ else:
+ result = True
+ description = 'No port errors detected'
+ return result, description, details
+
+ def connection_port_duplex_test(self):
+ auto_negotiation = self.is_autonegotiate()
+ # Calculate final results
+ result = None
+ description = ''
+ details = ''
+ if not auto_negotiation:
+ result = False
+ description = 'Interface not configured for auto-negotiation'
+ else:
+ duplex = self._get_stat_option(stats=self.conn_stats, option='Duplex:')
+ if 'Full' in duplex:
+ result = True
+ description = 'Succesfully auto-negotiated full duplex'
+ details = f'Duplex negotiated: {duplex}'
+ else:
+ result = False
+ description = 'Failed to auto-negotate full duplex'
+ details = f'Duplex negotiated: {duplex}'
+ return result, description, details
+
+ def connection_port_speed_test(self):
+ auto_negotiation = self.is_autonegotiate()
+ # Calculate final results
+ result = None
+ description = ''
+ details = ''
+ if not auto_negotiation:
+ result = False
+ description = 'Interface not configured for auto-negotiation'
+ else:
+ speed = self._get_stat_option(stats=self.conn_stats, option='Speed:')
+ if speed in ('100Mb/s', '1000Mb/s'):
+ result = True
+ description = 'Succesfully auto-negotiated speeds above 10 Mbps'
+ details = f'Speed negotiated: {speed}'
+ else:
+ result = False
+ description = 'Failed to auto-negotate speeds above 10 Mbps'
+ details = f'Speed negotiated: {speed}'
+ return result, description, details
+
+ def _get_stat_option(self, stats, option):
+ """Extract the requested parameter from the ethtool result"""
+ value = None
+ for line in stats.split('\n'):
+ #LOGGER.info(f'Checking option: {line}')
+ if line.startswith(f'{option}'):
+ value = line.split(':')[1].strip()
+ break
+ return value
+
+ def _read_stats_file(self, file):
+ if os.path.isfile(file):
+ with open(file, encoding='utf-8') as f:
+ content = f.read()
+ # Cleanup the results for easier processing
+ lines = content.split('\n')
+ cleaned_lines = [line.strip() for line in lines if line.strip()]
+ recombined_text = '\n'.join(cleaned_lines)
+ return recombined_text
+ return None
diff --git a/modules/ui/src/app/app-routing.module.ts b/modules/ui/src/app/app-routing.module.ts
index d503116d6..454782cc9 100644
--- a/modules/ui/src/app/app-routing.module.ts
+++ b/modules/ui/src/app/app-routing.module.ts
@@ -37,6 +37,14 @@ const routes: Routes = [
import('./pages/reports/history.module').then(m => m.HistoryModule),
title: 'Testrun - Reports',
},
+ {
+ path: 'risk-assessment',
+ loadChildren: () =>
+ import('./pages/risk-assessment/risk-assessment.module').then(
+ m => m.RiskAssessmentModule
+ ),
+ title: 'Testrun - Risk Assessment',
+ },
{
path: '',
redirectTo: 'devices',
diff --git a/modules/ui/src/app/app.component.html b/modules/ui/src/app/app.component.html
index 8b3a8f7dc..bd93aeb12 100644
--- a/modules/ui/src/app/app.component.html
+++ b/modules/ui/src/app/app.component.html
@@ -14,96 +14,134 @@
limitations under the License.
-->
-
-
-
+
+
+
+ No ports are detected. Please define a valid ones using
+
+
+ Selected port is missing! Please define a valid one using
+
+ System settings
+ panel.
+
+
+
+ Step 1: To perform a device test, please, select ports in
Testrun
>
panel.
-
-
- Step 1: To perform a device test, please, select ports in
- System settings
- panel.
-
-
- Step 2: To perform a device test please
- Create a Device
- first.
-
-
- Step 3: Once device is created, you are able to
- start testing.
-
-
-
-
+
+ Step 2: To perform a device test please
+ Create a Device
+ first.
+
+
+ Step 3: Once device is created, you are able to
+ start testing.
+
+
+