Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/prepare
Original file line number Diff line number Diff line change
Expand Up @@ -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
18 changes: 17 additions & 1 deletion framework/python/src/net_orc/ip_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
62 changes: 45 additions & 17 deletions framework/python/src/net_orc/network_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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')
Expand All @@ -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

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions modules/test/conn/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
18 changes: 18 additions & 0 deletions modules/test/conn/conf/module_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
24 changes: 21 additions & 3 deletions modules/test/conn/python/src/connection_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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')

Expand Down
141 changes: 141 additions & 0 deletions modules/test/conn/python/src/port_stats_util.py
Original file line number Diff line number Diff line change
@@ -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
Loading