diff --git a/modules/network/dhcp-1/bin/isc-dhcp-service b/modules/network/dhcp-1/bin/isc-dhcp-service new file mode 100644 index 000000000..de029515b --- /dev/null +++ b/modules/network/dhcp-1/bin/isc-dhcp-service @@ -0,0 +1,56 @@ +#!/bin/bash + +# 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. + +CONFIG_FILE=/etc/dhcp/dhcpd.conf +DHCP_PID_FILE=/var/run/dhcpd.pid +DHCP_LOG_FILE=/runtime/network/dhcp1-dhcpd.log + +stop_dhcp(){ + # Directly kill by PID file reference + if [ -f "$DHCP_PID_FILE" ]; then + kill -9 $(cat $DHCP_PID_FILE) || true + rm -f $DHCP_PID_FILE + fi +} + +start_dhcp(){ + /usr/sbin/dhcpd -d &> $DHCP_LOG_FILE & +} + +case "$1" in + start) + start_dhcp + ;; + stop) + stop_dhcp + ;; + restart) + stop_dhcp + sleep 1 + start_dhcp + ;; + status) + if [ -f "$DHCP_PID_FILE" ]; then + echo "isc-dhcp service is running." + else + echo "isc-dhcp service is not running." + fi + ;; + *) + echo "Usage: $0 {start|stop|status|restart}" + exit 1 + ;; +esac \ No newline at end of file diff --git a/modules/network/dhcp-1/bin/start_network_service b/modules/network/dhcp-1/bin/start_network_service index 413c48ceb..a9ff445e4 100644 --- a/modules/network/dhcp-1/bin/start_network_service +++ b/modules/network/dhcp-1/bin/start_network_service @@ -43,7 +43,9 @@ cp /testrun/conf/isc-dhcp-server /etc/default/ cp /testrun/conf/dhcpd.conf /etc/dhcp/dhcpd.conf cp /testrun/conf/radvd.conf /etc/radvd.conf -# Move the radvd-sevice file to the correct location + +# Move the service files to the correct location +cp /testrun/bin/isc-dhcp-service /usr/local/bin/ cp /testrun/bin/radvd-service /usr/local/bin/ # Start the DHCP Server diff --git a/modules/network/dhcp-1/conf/dhcpd.conf b/modules/network/dhcp-1/conf/dhcpd.conf index ee171279c..df804acf9 100644 --- a/modules/network/dhcp-1/conf/dhcpd.conf +++ b/modules/network/dhcp-1/conf/dhcpd.conf @@ -1,4 +1,5 @@ -default-lease-time 300; +default-lease-time 30; +max-lease-time 30; failover peer "failover-peer" { primary; @@ -8,7 +9,7 @@ failover peer "failover-peer" { peer port 647; max-response-delay 60; max-unacked-updates 10; - mclt 3600; + mclt 30; split 128; load balance max seconds 3; } diff --git a/modules/network/dhcp-1/conf/isc-dhcp-server b/modules/network/dhcp-1/conf/isc-dhcp-server index 44db95cd9..161f52d80 100644 --- a/modules/network/dhcp-1/conf/isc-dhcp-server +++ b/modules/network/dhcp-1/conf/isc-dhcp-server @@ -1,4 +1,4 @@ # On what interfaces should the DHCP server (dhcpd) serve DHCP requests? # Separate multiple interfaces with spaces, e.g. "eth0 eth1". INTERFACESv4="veth0" -#INTERFACESv6="veth0" +#INTERFACESv6="veth0" \ No newline at end of file diff --git a/modules/network/dhcp-1/conf/radvd.conf b/modules/network/dhcp-1/conf/radvd.conf index 89995785f..2f0c75d9d 100644 --- a/modules/network/dhcp-1/conf/radvd.conf +++ b/modules/network/dhcp-1/conf/radvd.conf @@ -1,13 +1,12 @@ -interface veth0 -{ - AdvSendAdvert on; - AdvManagedFlag off; - MinRtrAdvInterval 30; - MaxRtrAdvInterval 60; - prefix fd10:77be:4186::/64 { - AdvOnLink on; - AdvAutonomous on; - AdvRouterAddr on; - AdvSourceLLAddress off; - }; +interface veth0 +{ + AdvSendAdvert on; + AdvManagedFlag off; + MinRtrAdvInterval 30; + MaxRtrAdvInterval 60; + prefix fd10:77be:4186::/64 { + AdvOnLink on; + AdvAutonomous on; + AdvRouterAddr on; + }; }; \ No newline at end of file diff --git a/modules/network/dhcp-1/python/src/grpc_server/dhcp_config.py b/modules/network/dhcp-1/python/src/grpc_server/dhcp_config.py index 6f003014c..877d49610 100644 --- a/modules/network/dhcp-1/python/src/grpc_server/dhcp_config.py +++ b/modules/network/dhcp-1/python/src/grpc_server/dhcp_config.py @@ -20,13 +20,15 @@ LOGGER = None CONFIG_FILE = '/etc/dhcp/dhcpd.conf' DEFAULT_LEASE_TIME_KEY = 'default-lease-time' +MAX_LEASE_TIME_KEY = 'max-lease-time' class DHCPConfig: """Represents the DHCP Servers configuration and gives access to modify it""" def __init__(self): - self._default_lease_time = 300 + self._default_lease_time = 30 + self._max_lease_time = 30 self._subnets = [] self._peer = None self._reserved_hosts = [] @@ -120,17 +122,50 @@ def set_range(self, start, end, subnet=0, pool=0): octets[-1] = '0' dhcp_subnet = '.'.join(octets) - #Update the subnet and range - self._subnets[subnet].set_subnet(dhcp_subnet) + # Calcualte the netmask from the range + prefix = self.calculate_prefix_length(start, end) + netmask = self.calculate_netmask(prefix) + + #Update the subnet, range and netmask + self._subnets[subnet].set_subnet(dhcp_subnet, netmask) self._subnets[subnet].pools[pool].set_range(start, end) + def calculate_prefix_length(self, start_ip, end_ip): + start_octets = start_ip.split('.') + end_octets = end_ip.split('.') + + start_int = int( + ''.join(format(int(octet), '08b') for octet in start_octets), 2) + end_int = int(''.join(format(int(octet), '08b') for octet in end_octets), 2) + + xor_result = start_int ^ end_int + prefix_length = 32 - xor_result.bit_length() + + return prefix_length + + def calculate_netmask(self, prefix_length): + num_network_bits = prefix_length + num_host_bits = 32 - num_network_bits + + netmask_int = (2**num_network_bits - 1) << num_host_bits + netmask_octets = [(netmask_int >> (i * 8)) & 0xff for i in range(3, -1, -1)] + + return '.'.join(str(octet) for octet in netmask_octets) + def __str__(self): + config = ('{DEFAULT_LEASE_TIME_KEY} {DEFAULT_LEASE_TIME};' + if self._default_lease_time is not None else '') + config += ('\n\r{MAX_LEASE_TIME_KEY} {MAX_LEASE_TIME};' + if self._max_lease_time is not None else '') + # Encode the top level config options - config = """{DEFAULT_LEASE_TIME_KEY} {DEFAULT_LEASE_TIME};""" + #config = """{DEFAULT_LEASE_TIME_KEY} {DEFAULT_LEASE_TIME};""" config = config.format(length='multi-line', DEFAULT_LEASE_TIME_KEY=DEFAULT_LEASE_TIME_KEY, - DEFAULT_LEASE_TIME=self._default_lease_time) + DEFAULT_LEASE_TIME=self._default_lease_time, + MAX_LEASE_TIME_KEY=MAX_LEASE_TIME_KEY, + MAX_LEASE_TIME=self._max_lease_time) # Encode the failover peer config += '\n\n' + str(self._peer) @@ -358,12 +393,24 @@ def set_subnet(self, subnet, netmask=None): self._subnet = subnet self._subnet_mask = netmask - # Calculate the broadcast from the subnet - octets = subnet.split('.') - octets[-1] = '255' - dhcp_broadcast = '.'.join(octets) + # Calculate the broadcast from the subnet and netmask + broadcast = self.calculate_broadcast_address(subnet, netmask) + self._broadcast = broadcast + + def calculate_broadcast_address(self, subnet_address, netmask): + subnet_octets = subnet_address.split('.') + netmask_octets = netmask.split('.') + + subnet_int = int( + ''.join(format(int(octet), '08b') for octet in subnet_octets), 2) + netmask_int = int( + ''.join(format(int(octet), '08b') for octet in netmask_octets), 2) + + broadcast_int = subnet_int | (~netmask_int & 0xffffffff) + broadcast_octets = [(broadcast_int >> (i * 8)) & 0xff + for i in range(3, -1, -1)] - self._broadcast = dhcp_broadcast + return '.'.join(str(octet) for octet in broadcast_octets) def resolve_subnet(self, subnet): subnet_parts = subnet.split('\n') diff --git a/modules/network/dhcp-1/python/src/grpc_server/dhcp_config_test.py b/modules/network/dhcp-1/python/src/grpc_server/dhcp_config_test.py index a34ff4e31..c283f0726 100644 --- a/modules/network/dhcp-1/python/src/grpc_server/dhcp_config_test.py +++ b/modules/network/dhcp-1/python/src/grpc_server/dhcp_config_test.py @@ -19,6 +19,7 @@ CONFIG_FILE = 'conf/dhcpd.conf' DHCP_CONFIG = None + def get_config_file_path(): current_dir = os.path.dirname(os.path.abspath(__file__)) module_dir = os.path.dirname( @@ -91,6 +92,15 @@ def test_resolve_config_with_hosts(self): self.assertIsNotNone(host) print('ResolveConfigWithHosts:\n' + str(config_with_hosts)) + def test_set_subnet_range(self): + range_start = '10.0.0.100' + range_end = '10.0.0.200' + DHCP_CONFIG.set_range(range_start, range_end) + subnets = DHCP_CONFIG.resolve_subnets(str(DHCP_CONFIG)) + pool = subnets[0].pools[0] + self.assertTrue(pool.range_start == range_start + and pool.range_end == range_end) + print('SetSubnetRange:\n' + str(DHCP_CONFIG)) if __name__ == '__main__': suite = unittest.TestSuite() @@ -100,6 +110,6 @@ def test_resolve_config_with_hosts(self): suite.addTest(DHCPConfigTest('test_add_reserved_host')) suite.addTest(DHCPConfigTest('test_delete_reserved_host')) suite.addTest(DHCPConfigTest('test_resolve_config_with_hosts')) - + suite.addTest(DHCPConfigTest('test_set_subnet_range')) runner = unittest.TextTestRunner() runner.run(suite) diff --git a/modules/network/dhcp-1/python/src/grpc_server/dhcp_lease.py b/modules/network/dhcp-1/python/src/grpc_server/dhcp_lease.py index 0d2f43e3b..dd7ba9516 100644 --- a/modules/network/dhcp-1/python/src/grpc_server/dhcp_lease.py +++ b/modules/network/dhcp-1/python/src/grpc_server/dhcp_lease.py @@ -39,7 +39,7 @@ def _make_lease(self, lease): self.hw_addr = sections[0] self.ip = sections[1] self.hostname = sections[2] - self.expires = sections[3] + '' '' + sections[4] + self.expires = sections[3] + ' ' + sections[4] self.manufacturer = ' '.join(sections[5:]) def get_millis(self, timestamp): diff --git a/modules/network/dhcp-1/python/src/grpc_server/dhcp_leases.py b/modules/network/dhcp-1/python/src/grpc_server/dhcp_leases.py index 698277a02..aa2945759 100644 --- a/modules/network/dhcp-1/python/src/grpc_server/dhcp_leases.py +++ b/modules/network/dhcp-1/python/src/grpc_server/dhcp_leases.py @@ -58,9 +58,9 @@ def get_leases(self): leases = [] lease_list_raw = self._get_lease_list() LOGGER.info('Raw Leases:\n' + str(lease_list_raw) + '\n') - lease_list_start = lease_list_raw.find('=========',0) - lease_list_start = lease_list_raw.find('\n',lease_list_start) - lease_list = lease_list_raw[lease_list_start+1:] + lease_list_start = lease_list_raw.find('=========', 0) + lease_list_start = lease_list_raw.find('\n', lease_list_start) + lease_list = lease_list_raw[lease_list_start + 1:] lines = lease_list.split('\n') for line in lines: try: diff --git a/modules/network/dhcp-1/python/src/grpc_server/dhcp_server.py b/modules/network/dhcp-1/python/src/grpc_server/dhcp_server.py index 5e88d59fe..aa6afb8c1 100644 --- a/modules/network/dhcp-1/python/src/grpc_server/dhcp_server.py +++ b/modules/network/dhcp-1/python/src/grpc_server/dhcp_server.py @@ -37,7 +37,7 @@ def __init__(self): def restart(self): LOGGER.info('Restarting DHCP Server') - isc_started = util.run_command('service isc-dhcp-server restart', False) + isc_started = util.run_command('isc-dhcp-service restart', False) radvd_started = self.radvd.restart() started = isc_started and radvd_started LOGGER.info('DHCP Restarted: ' + str(started)) @@ -45,7 +45,7 @@ def restart(self): def start(self): LOGGER.info('Starting DHCP Server') - isc_started = util.run_command('service isc-dhcp-server start', False) + isc_started = util.run_command('isc-dhcp-service start', False) radvd_started = self.radvd.start() started = isc_started and radvd_started LOGGER.info('DHCP Started: ' + str(started)) @@ -53,7 +53,7 @@ def start(self): def stop(self): LOGGER.info('Stopping DHCP Server') - isc_stopped = util.run_command('service isc-dhcp-server stop', False) + isc_stopped = util.run_command('isc-dhcp-service stop', False) radvd_stopped = self.radvd.stop() stopped = isc_stopped and radvd_stopped LOGGER.info('DHCP Stopped: ' + str(stopped)) @@ -61,9 +61,8 @@ def stop(self): def is_running(self): LOGGER.info('Checking DHCP Status') - response = util.run_command('service isc-dhcp-server status') - isc_running = response[ - 0] == 'Status of ISC DHCPv4 server: dhcpd is running.' + response = util.run_command('isc-dhcp-service status') + isc_running = response[0] == 'isc-dhcp service is running.' radvd_running = self.radvd.is_running() running = isc_running and radvd_running LOGGER.info('DHCP Status: ' + str(running)) @@ -107,6 +106,7 @@ def boot(self): return isc_booted and radvd_booted + def run(): dhcp_server = DHCPServer() booted = dhcp_server.boot() @@ -126,5 +126,6 @@ def run(): dhcp_server.radvd.restart() time.sleep(1) + if __name__ == '__main__': run() diff --git a/modules/network/dhcp-1/python/src/grpc_server/network_service.py b/modules/network/dhcp-1/python/src/grpc_server/network_service.py index 043ca49b3..92726025d 100644 --- a/modules/network/dhcp-1/python/src/grpc_server/network_service.py +++ b/modules/network/dhcp-1/python/src/grpc_server/network_service.py @@ -25,6 +25,7 @@ LOG_NAME = 'network_service' LOGGER = None + class NetworkService(pb2_grpc.NetworkModule): """gRPC endpoints for the DHCP Server""" diff --git a/modules/network/dhcp-2/bin/isc-dhcp-service b/modules/network/dhcp-2/bin/isc-dhcp-service new file mode 100644 index 000000000..ee6df0341 --- /dev/null +++ b/modules/network/dhcp-2/bin/isc-dhcp-service @@ -0,0 +1,56 @@ +#!/bin/bash + +# 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. + +CONFIG_FILE=/etc/dhcp/dhcpd.conf +DHCP_PID_FILE=/var/run/dhcpd.pid +DHCP_LOG_FILE=/runtime/network/dhcp2-dhcpd.log + +stop_dhcp(){ + # Directly kill by PID file reference + if [ -f "$DHCP_PID_FILE" ]; then + kill -9 $(cat $DHCP_PID_FILE) || true + rm -f $DHCP_PID_FILE + fi +} + +start_dhcp(){ + /usr/sbin/dhcpd -d &> $DHCP_LOG_FILE & +} + +case "$1" in + start) + start_dhcp + ;; + stop) + stop_dhcp + ;; + restart) + stop_dhcp + sleep 1 + start_dhcp + ;; + status) + if [ -f "$DHCP_PID_FILE" ]; then + echo "isc-dhcp service is running." + else + echo "isc-dhcp service is not running." + fi + ;; + *) + echo "Usage: $0 {start|stop|status|restart}" + exit 1 + ;; +esac \ No newline at end of file diff --git a/modules/network/dhcp-2/bin/start_network_service b/modules/network/dhcp-2/bin/start_network_service index ed7d3125e..5acea606b 100644 --- a/modules/network/dhcp-2/bin/start_network_service +++ b/modules/network/dhcp-2/bin/start_network_service @@ -44,7 +44,9 @@ cp /testrun/conf/isc-dhcp-server /etc/default/ cp /testrun/conf/dhcpd.conf /etc/dhcp/dhcpd.conf cp /testrun/conf/radvd.conf /etc/radvd.conf -# Move the radvd-sevice file to the correct location + +# Move the service files to the correct location +cp /testrun/bin/isc-dhcp-service /usr/local/bin/ cp /testrun/bin/radvd-service /usr/local/bin/ # Start the DHCP Server diff --git a/modules/network/dhcp-2/conf/dhcpd.conf b/modules/network/dhcp-2/conf/dhcpd.conf index dcc47a4fe..5a6c82410 100644 --- a/modules/network/dhcp-2/conf/dhcpd.conf +++ b/modules/network/dhcp-2/conf/dhcpd.conf @@ -1,25 +1,26 @@ -default-lease-time 300; - -failover peer "failover-peer" { - secondary; - address 10.10.10.3; - port 647; - peer address 10.10.10.2; - peer port 847; - max-response-delay 60; - max-unacked-updates 10; - load balance max seconds 3; -} - -subnet 10.10.10.0 netmask 255.255.255.0 { - option ntp-servers 10.10.10.5; - option subnet-mask 255.255.255.0; - option broadcast-address 10.10.10.255; - option routers 10.10.10.1; - option domain-name-servers 10.10.10.4; - interface veth0; - pool { - failover peer "failover-peer"; - range 10.10.10.10 10.10.10.20; - } -} +default-lease-time 30; +max-lease-time 30; + +failover peer "failover-peer" { + secondary; + address 10.10.10.3; + port 647; + peer address 10.10.10.2; + peer port 847; + max-response-delay 60; + max-unacked-updates 10; + load balance max seconds 3; +} + +subnet 10.10.10.0 netmask 255.255.255.0 { + option ntp-servers 10.10.10.5; + option subnet-mask 255.255.255.0; + option broadcast-address 10.10.10.255; + option routers 10.10.10.1; + option domain-name-servers 10.10.10.4; + interface veth0; + pool { + failover peer "failover-peer"; + range 10.10.10.10 10.10.10.20; + } +} diff --git a/modules/network/dhcp-2/python/src/grpc_server/dhcp_config.py b/modules/network/dhcp-2/python/src/grpc_server/dhcp_config.py index 5da5e4cf2..5357ba7ed 100644 --- a/modules/network/dhcp-2/python/src/grpc_server/dhcp_config.py +++ b/modules/network/dhcp-2/python/src/grpc_server/dhcp_config.py @@ -22,13 +22,15 @@ CONFIG_FILE = '/etc/dhcp/dhcpd.conf' DEFAULT_LEASE_TIME_KEY = 'default-lease-time' +MAX_LEASE_TIME_KEY = 'max-lease-time' class DHCPConfig: """Represents the DHCP Servers configuration and gives access to modify it""" def __init__(self): - self._default_lease_time = 300 + self._default_lease_time = 30 + self._max_lease_time = 30 self._subnets = [] self._peer = None self._reserved_hosts = [] @@ -122,17 +124,50 @@ def set_range(self, start, end, subnet=0, pool=0): octets[-1] = '0' dhcp_subnet = '.'.join(octets) - #Update the subnet and range - self._subnets[subnet].set_subnet(dhcp_subnet) + # Calcualte the netmask from the range + prefix = self.calculate_prefix_length(start, end) + netmask = self.calculate_netmask(prefix) + + #Update the subnet, range and netmask + self._subnets[subnet].set_subnet(dhcp_subnet, netmask) self._subnets[subnet].pools[pool].set_range(start, end) + def calculate_prefix_length(self, start_ip, end_ip): + start_octets = start_ip.split('.') + end_octets = end_ip.split('.') + + start_int = int( + ''.join(format(int(octet), '08b') for octet in start_octets), 2) + end_int = int(''.join(format(int(octet), '08b') for octet in end_octets), 2) + + xor_result = start_int ^ end_int + prefix_length = 32 - xor_result.bit_length() + + return prefix_length + + def calculate_netmask(self, prefix_length): + num_network_bits = prefix_length + num_host_bits = 32 - num_network_bits + + netmask_int = (2**num_network_bits - 1) << num_host_bits + netmask_octets = [(netmask_int >> (i * 8)) & 0xff for i in range(3, -1, -1)] + + return '.'.join(str(octet) for octet in netmask_octets) + def __str__(self): + config = ('{DEFAULT_LEASE_TIME_KEY} {DEFAULT_LEASE_TIME};' + if self._default_lease_time is not None else '') + config += ('\n\r{MAX_LEASE_TIME_KEY} {MAX_LEASE_TIME};' + if self._max_lease_time is not None else '') + # Encode the top level config options - config = """{DEFAULT_LEASE_TIME_KEY} {DEFAULT_LEASE_TIME};""" + #config = """{DEFAULT_LEASE_TIME_KEY} {DEFAULT_LEASE_TIME};""" config = config.format(length='multi-line', DEFAULT_LEASE_TIME_KEY=DEFAULT_LEASE_TIME_KEY, - DEFAULT_LEASE_TIME=self._default_lease_time) + DEFAULT_LEASE_TIME=self._default_lease_time, + MAX_LEASE_TIME_KEY=MAX_LEASE_TIME_KEY, + MAX_LEASE_TIME=self._max_lease_time) # Encode the failover peer config += '\n\n' + str(self._peer) @@ -360,12 +395,24 @@ def set_subnet(self, subnet, netmask=None): self._subnet = subnet self._subnet_mask = netmask - # Calculate the broadcast from the subnet - octets = subnet.split('.') - octets[-1] = '255' - dhcp_broadcast = '.'.join(octets) + # Calculate the broadcast from the subnet and netmask + broadcast = self.calculate_broadcast_address(subnet, netmask) + self._broadcast = broadcast + + def calculate_broadcast_address(self, subnet_address, netmask): + subnet_octets = subnet_address.split('.') + netmask_octets = netmask.split('.') + + subnet_int = int( + ''.join(format(int(octet), '08b') for octet in subnet_octets), 2) + netmask_int = int( + ''.join(format(int(octet), '08b') for octet in netmask_octets), 2) + + broadcast_int = subnet_int | (~netmask_int & 0xffffffff) + broadcast_octets = [(broadcast_int >> (i * 8)) & 0xff + for i in range(3, -1, -1)] - self._broadcast = dhcp_broadcast + return '.'.join(str(octet) for octet in broadcast_octets) def resolve_subnet(self, subnet): subnet_parts = subnet.split('\n') diff --git a/modules/network/dhcp-2/python/src/grpc_server/dhcp_config_test.py b/modules/network/dhcp-2/python/src/grpc_server/dhcp_config_test.py index b07f57b27..c283f0726 100644 --- a/modules/network/dhcp-2/python/src/grpc_server/dhcp_config_test.py +++ b/modules/network/dhcp-2/python/src/grpc_server/dhcp_config_test.py @@ -19,6 +19,7 @@ CONFIG_FILE = 'conf/dhcpd.conf' DHCP_CONFIG = None + def get_config_file_path(): current_dir = os.path.dirname(os.path.abspath(__file__)) module_dir = os.path.dirname( @@ -26,11 +27,13 @@ def get_config_file_path(): conf_file = os.path.join(module_dir, CONFIG_FILE) return conf_file + def get_config(): dhcp_config = DHCPConfig() dhcp_config.resolve_config(get_config_file_path()) return dhcp_config + class DHCPConfigTest(unittest.TestCase): @classmethod @@ -89,6 +92,16 @@ def test_resolve_config_with_hosts(self): self.assertIsNotNone(host) print('ResolveConfigWithHosts:\n' + str(config_with_hosts)) + def test_set_subnet_range(self): + range_start = '10.0.0.100' + range_end = '10.0.0.200' + DHCP_CONFIG.set_range(range_start, range_end) + subnets = DHCP_CONFIG.resolve_subnets(str(DHCP_CONFIG)) + pool = subnets[0].pools[0] + self.assertTrue(pool.range_start == range_start + and pool.range_end == range_end) + print('SetSubnetRange:\n' + str(DHCP_CONFIG)) + if __name__ == '__main__': suite = unittest.TestSuite() suite.addTest(DHCPConfigTest('test_resolve_config')) @@ -97,6 +110,6 @@ def test_resolve_config_with_hosts(self): suite.addTest(DHCPConfigTest('test_add_reserved_host')) suite.addTest(DHCPConfigTest('test_delete_reserved_host')) suite.addTest(DHCPConfigTest('test_resolve_config_with_hosts')) - + suite.addTest(DHCPConfigTest('test_set_subnet_range')) runner = unittest.TextTestRunner() runner.run(suite) diff --git a/modules/network/dhcp-2/python/src/grpc_server/dhcp_lease.py b/modules/network/dhcp-2/python/src/grpc_server/dhcp_lease.py index 0d2f43e3b..dd7ba9516 100644 --- a/modules/network/dhcp-2/python/src/grpc_server/dhcp_lease.py +++ b/modules/network/dhcp-2/python/src/grpc_server/dhcp_lease.py @@ -39,7 +39,7 @@ def _make_lease(self, lease): self.hw_addr = sections[0] self.ip = sections[1] self.hostname = sections[2] - self.expires = sections[3] + '' '' + sections[4] + self.expires = sections[3] + ' ' + sections[4] self.manufacturer = ' '.join(sections[5:]) def get_millis(self, timestamp): diff --git a/modules/network/dhcp-2/python/src/grpc_server/dhcp_server.py b/modules/network/dhcp-2/python/src/grpc_server/dhcp_server.py index 67a31c2cb..270a2c700 100644 --- a/modules/network/dhcp-2/python/src/grpc_server/dhcp_server.py +++ b/modules/network/dhcp-2/python/src/grpc_server/dhcp_server.py @@ -37,7 +37,7 @@ def __init__(self): def restart(self): LOGGER.info('Restarting DHCP Server') - isc_started = util.run_command('service isc-dhcp-server restart', False) + isc_started = util.run_command('isc-dhcp-service restart', False) radvd_started = self.radvd.restart() started = isc_started and radvd_started LOGGER.info('DHCP Restarted: ' + str(started)) @@ -45,7 +45,7 @@ def restart(self): def start(self): LOGGER.info('Starting DHCP Server') - isc_started = util.run_command('service isc-dhcp-server start', False) + isc_started = util.run_command('isc-dhcp-service start', False) radvd_started = self.radvd.start() started = isc_started and radvd_started LOGGER.info('DHCP Started: ' + str(started)) @@ -53,7 +53,7 @@ def start(self): def stop(self): LOGGER.info('Stopping DHCP Server') - isc_stopped = util.run_command('service isc-dhcp-server stop', False) + isc_stopped = util.run_command('isc-dhcp-service stop', False) radvd_stopped = self.radvd.stop() stopped = isc_stopped and radvd_stopped LOGGER.info('DHCP Stopped: ' + str(stopped)) @@ -61,9 +61,8 @@ def stop(self): def is_running(self): LOGGER.info('Checking DHCP Status') - response = util.run_command('service isc-dhcp-server status') - isc_running = response[ - 0] == 'Status of ISC DHCPv4 server: dhcpd is running.' + response = util.run_command('isc-dhcp-service status') + isc_running = response[0] == 'isc-dhcp service is running.' radvd_running = self.radvd.is_running() running = isc_running and radvd_running LOGGER.info('DHCP Status: ' + str(running)) @@ -107,6 +106,7 @@ def boot(self): return isc_booted and radvd_booted + def run(): dhcp_server = DHCPServer() booted = dhcp_server.boot() diff --git a/modules/test/base/python/src/grpc/proto/dhcp1/client.py b/modules/test/base/python/src/grpc/proto/dhcp1/client.py index 921929edb..8707957ce 100644 --- a/modules/test/base/python/src/grpc/proto/dhcp1/client.py +++ b/modules/test/base/python/src/grpc/proto/dhcp1/client.py @@ -1,13 +1,28 @@ +# 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 +"""gRPC client module for the primary DHCP Server""" import grpc import grpc_pb2_grpc as pb2_grpc import grpc_pb2 as pb2 + DEFAULT_PORT = '5001' DEFAULT_HOST = '10.10.10.2' # Default DHCP1 server class Client(): - + """gRPC Client for the primary DHCP server""" def __init__(self, port=DEFAULT_PORT, host=DEFAULT_HOST): self._port = port self._host = host @@ -86,6 +101,24 @@ def get_status(self): return response + def stop_dhcp_server(self): + # Create a request message + request = pb2.StopDHCPServerRequest() + + # Make the RPC call + response = self._stub.StopDHCPServer(request) + + return response + + def start_dhcp_server(self): + # Create a request message + request = pb2.StartDHCPServerRequest() + + # Make the RPC call + response = self._stub.StartDHCPServer(request) + + return response + def set_dhcp_range(self,start,end): # Create a request message request = pb2.SetDHCPRangeRequest() diff --git a/modules/test/base/python/src/grpc/proto/dhcp2/client.py b/modules/test/base/python/src/grpc/proto/dhcp2/client.py new file mode 100644 index 000000000..e0d953ee5 --- /dev/null +++ b/modules/test/base/python/src/grpc/proto/dhcp2/client.py @@ -0,0 +1,130 @@ +# 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 +"""gRPC client module for the secondary DHCP Server""" +import grpc +import grpc_pb2_grpc as pb2_grpc +import grpc_pb2 as pb2 + +DEFAULT_PORT = '5001' +DEFAULT_HOST = '10.10.10.3' # Default DHCP2 server + + +class Client(): + """gRPC Client for the secondary DHCP server""" + def __init__(self, port=DEFAULT_PORT, host=DEFAULT_HOST): + self._port = port + self._host = host + + # Create a gRPC channel to connect to the server + self._channel = grpc.insecure_channel(self._host + ':' + self._port) + + # Create a gRPC stub + self._stub = pb2_grpc.NetworkModuleStub(self._channel) + + def add_reserved_lease(self, hostname, hw_addr, ip_addr): + # Create a request message + request = pb2.AddReservedLeaseRequest() + request.hostname = hostname + request.hw_addr = hw_addr + request.ip_addr = ip_addr + + # Make the RPC call + response = self._stub.AddReservedLease(request) + + return response + + def delete_reserved_lease(self, hw_addr): + # Create a request message + request = pb2.DeleteReservedLeaseRequest() + request.hw_addr = hw_addr + + # Make the RPC call + response = self._stub.DeleteReservedLease(request) + + return response + + def disable_failover(self): + # Create a request message + request = pb2.DisableFailoverRequest() + + # Make the RPC call + response = self._stub.DisableFailover(request) + + return response + + def enable_failover(self): + # Create a request message + request = pb2.EnableFailoverRequest() + + # Make the RPC call + response = self._stub.EnableFailover(request) + + return response + + def get_dhcp_range(self): + # Create a request message + request = pb2.GetDHCPRangeRequest() + + # Make the RPC call + response = self._stub.GetDHCPRange(request) + + return response + + def get_lease(self,hw_addr): + # Create a request message + request = pb2.GetLeaseRequest() + request.hw_addr=hw_addr + + # Make the RPC call + response = self._stub.GetLease(request) + + return response + + def get_status(self): + # Create a request message + request = pb2.GetStatusRequest() + + # Make the RPC call + response = self._stub.GetStatus(request) + + return response + + def stop_dhcp_server(self): + # Create a request message + request = pb2.StopDHCPServerRequest() + + # Make the RPC call + response = self._stub.StopDHCPServer(request) + + return response + + def start_dhcp_server(self): + # Create a request message + request = pb2.StartDHCPServerRequest() + + # Make the RPC call + response = self._stub.StartDHCPServer(request) + + return response + + def set_dhcp_range(self,start,end): + # Create a request message + request = pb2.SetDHCPRangeRequest() + request.start=start + request.end=end + + # Make the RPC call + response = self._stub.SetDHCPRange(request) + + return response diff --git a/modules/test/conn/conf/module_config.json b/modules/test/conn/conf/module_config.json index 496b6aada..b82879544 100644 --- a/modules/test/conn/conf/module_config.json +++ b/modules/test/conn/conf/module_config.json @@ -9,7 +9,7 @@ "docker": { "depends_on": "base", "enable_container": true, - "timeout": 30 + "timeout": 600 }, "tests": [ { @@ -27,6 +27,25 @@ "description": "The device under test hs a MAC address prefix that is registered against a known manufacturer.", "expected_behavior": "The MAC address prefix is registered in the IEEE Organizationally Unique Identifier database." }, + { + "name": "connection.private_address", + "description": "The device under test accepts an IP address that is compliant with RFC 1918 Address Allocation for Private Internets.", + "expected_behavior": "The device under test accepts IP addresses within all ranges specified in RFC 1918 and communicates using these addresses. The Internet Assigned Numbers Authority (IANA) has reserved the following three blocks of the IP address space for private internets. 10.0.0.0 - 10.255.255.255.255 (10/8 prefix). 172.16.0.0 - 172.31.255.255 (172.16/12 prefix). 192.168.0.0 - 192.168.255.255 (192.168/16 prefix)", + "config": [ + { + "start": "10.0.0.100", + "end": "10.0.0.200" + }, + { + "start":"172.16.0.0", + "end":"172.16.255.255" + }, + { + "start":"192.168.0.0", + "end":"192.168.255.255" + } + ] + }, { "name": "connection.single_ip", "description": "The network switch port connected to the device reports only one IP address for the device under test.", diff --git a/modules/test/conn/python/src/connection_module.py b/modules/test/conn/python/src/connection_module.py index 0b11fde24..387b19773 100644 --- a/modules/test/conn/python/src/connection_module.py +++ b/modules/test/conn/python/src/connection_module.py @@ -14,10 +14,12 @@ """Connection test module""" import util import sys -import json -from scapy.all import rdpcap, DHCP, Ether +import time +from datetime import datetime +from scapy.all import rdpcap, DHCP, Ether, IPv6 from test_module import TestModule from dhcp1.client import Client as DHCPClient1 +from dhcp2.client import Client as DHCPClient2 LOG_NAME = 'test_connection' LOGGER = None @@ -36,6 +38,7 @@ def __init__(self, module): global LOGGER LOGGER = self._get_logger() self.dhcp1_client = DHCPClient1() + self.dhcp2_client = DHCPClient2() # ToDo: Move this into some level of testing, leave for # reference until tests are implemented with these calls @@ -64,12 +67,79 @@ 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_private_address(self, config): + # Shutdown the secondary DHCP Server + LOGGER.info('Running connection.private_address') + response = self.dhcp1_client.get_dhcp_range() + cur_range = {} + if response.code == 200: + cur_range['start'] = response.start + cur_range['end'] = response.end + LOGGER.info('Current DHCP subnet range: ' + str(cur_range)) + else: + LOGGER.error('Failed to resolve current subnet range required ' + 'for restoring network') + return None, ('Failed to resolve current subnet range required ' + 'for restoring network') + + results = [] + dhcp_setup = self.setup_single_dhcp_server() + if dhcp_setup[0]: + LOGGER.info(dhcp_setup[1]) + lease = self._get_cur_lease() + if lease is not None: + if self._is_lease_active(lease): + results = self.test_subnets(config) + else: + return None, 'Failed to confirm a valid active lease for the device' + else: + LOGGER.error(dhcp_setup[1]) + return None, 'Failed to setup DHCP server for test' + + # Process and return final results + final_result = None + final_result_details = '' + for result in results: + if final_result is None: + final_result = result['result'] + else: + final_result &= result['result'] + final_result_details += result['details'] + '\n' + + try: + # Restore failover configuration of DHCP servers + self.restore_failover_dhcp_server(cur_range) + + # Wait for the current lease to expire + self._wait_for_lease_expire(self._get_cur_lease()) + + # Wait for a new lease to be provided before exiting test + # to prevent other test modules from failing + for _ in range(5): + LOGGER.info('Checking for new lease') + lease = self._get_cur_lease() + if lease is not None: + LOGGER.info('New Lease found: ' + str(lease)) + LOGGER.info('Validating subnet for new lease...') + in_range = self.is_ip_in_range(lease['ip'], cur_range['start'], + cur_range['end']) + LOGGER.info('Lease within subnet: ' + str(in_range)) + break + else: + LOGGER.info('New lease not found. Waiting to check again') + time.sleep(5) + + except Exception as e: # pylint: disable=W0718 + LOGGER.error('Failed to restore DHCP server configuration: ' + str(e)) + + return final_result, final_result_details + def _connection_dhcp_address(self): LOGGER.info('Running connection.dhcp_address') response = self.dhcp1_client.get_lease(self._device_mac) LOGGER.info('DHCP Lease resolved:\n' + str(response)) if response.code == 200: - lease = eval(response.message) # pylint: disable=E0203 + lease = eval(response.message) # pylint: disable=W0123 if 'ip' in lease: ip_addr = lease['ip'] LOGGER.info('IP Resolved: ' + ip_addr) @@ -84,8 +154,6 @@ def _connection_dhcp_address(self): LOGGER.info('No DHCP lease found for: ' + self._device_mac) return False, 'No DHCP lease found for: ' + self._device_mac - self._ipv6_addr = None - def _connection_mac_address(self): LOGGER.info('Running connection.mac_address') if self._device_mac is not None: @@ -173,7 +241,7 @@ def _connection_ipv6_slaac(self): if ICMPv6ND_NS in packet: ipv6_addr = str(packet[ICMPv6ND_NS].tgt) if ipv6_addr.startswith(SLAAC_PREFIX): - self._ipv6_addr = ipv6_addr + self._device_ipv6_addr = ipv6_addr LOGGER.info(f"Device has formed SLAAC address {ipv6_addr}") return True @@ -186,12 +254,12 @@ def _connection_ipv6_slaac(self): def _connection_ipv6_ping(self): LOGGER.info("Running connection.ipv6_ping") - if self._ipv6_addr is None: + if self._device_ipv6_addr is None: LOGGER.info("No IPv6 SLAAC address found. Cannot ping") return - if self._ping(self._ipv6_addr): - LOGGER.info(f"Device responds to IPv6 ping on {self._ipv6_addr}") + if self._ping(self._device_ipv6_addr): + LOGGER.info(f"Device responds to IPv6 ping on {self._device_ipv6_addr}") return True else: LOGGER.info("Device does not respond to IPv6 ping") @@ -201,3 +269,163 @@ def _ping(self, host): cmd = "ping -c 1 " + str(host) success = util.run_command(cmd, output=False) return success + + def restore_failover_dhcp_server(self, subnet): + # Configure the subnet range + if self._change_subnet(subnet): + if self.enable_failover(): + response = self.dhcp2_client.start_dhcp_server() + if response.code == 200: + LOGGER.info('DHCP server configuration restored') + return True + else: + LOGGER.error('Failed to start secondary DHCP server') + return False + else: + LOGGER.error('Failed to enabled failover in primary DHCP server') + return False + else: + LOGGER.error('Failed to restore original subnet') + return False + + def setup_single_dhcp_server(self): + # Shutdown the secondary DHCP Server + LOGGER.info('Stopping secondary DHCP server') + response = self.dhcp2_client.stop_dhcp_server() + if response.code == 200: + LOGGER.info('Secondary DHCP server stop command success') + time.sleep(3) # Give some time for the server to stop + LOGGER.info('Checking secondary DHCP server status') + response = self.dhcp2_client.get_status() + if response.code == 200: + LOGGER.info('Secondary DHCP server stopped') + return True, 'Single DHCP server configured' + else: + return False, 'DHCP server still running' + else: + return False, 'DHCP server stop command failed' + + # Move primary DHCP server from failover into a single DHCP server config + LOGGER.info('Configuring primary DHCP server') + response = self.dhcp1_client.disable_failover() + if response.code == 200: + LOGGER.info('Primary DHCP server failover disabled') + else: + return False, 'Failed to disable primary DHCP server failover' + + def enable_failover(self): + # Move primary DHCP server to primary failover + LOGGER.info('Configuring primary failover DHCP server') + response = self.dhcp1_client.enable_failover() + if response.code == 200: + LOGGER.info('Primary DHCP server enabled') + return True + else: + LOGGER.error('Failed to disable primary DHCP server failover') + return False + + def is_ip_in_range(self, ip, start_ip, end_ip): + ip_int = int(''.join(format(int(octet), '08b') for octet in ip.split('.')), + 2) + start_int = int( + ''.join(format(int(octet), '08b') for octet in start_ip.split('.')), 2) + end_int = int( + ''.join(format(int(octet), '08b') for octet in end_ip.split('.')), 2) + + return start_int <= ip_int <= end_int + + def _test_subnet(self, subnet, lease): + if self._change_subnet(subnet): + expiration = datetime.strptime(lease['expires'], '%Y-%m-%d %H:%M:%S') + time_to_expire = expiration - datetime.now() + LOGGER.info('Time until lease expiration: ' + str(time_to_expire)) + LOGGER.info('Waiting for current lease to expire: ' + str(expiration)) + if time_to_expire.total_seconds() > 0: + time.sleep(time_to_expire.total_seconds() + + 5) # Wait until the expiration time and padd 5 seconds + LOGGER.info('Current lease expired. Checking for new lease') + for _ in range(5): + LOGGER.info('Checking for new lease') + lease = self._get_cur_lease() + if lease is not None: + LOGGER.info('New Lease found: ' + str(lease)) + LOGGER.info('Validating subnet for new lease...') + in_range = self.is_ip_in_range(lease['ip'], subnet['start'], + subnet['end']) + LOGGER.info('Lease within subnet: ' + str(in_range)) + return in_range + else: + LOGGER.info('New lease not found. Waiting to check again') + time.sleep(5) + + def _wait_for_lease_expire(self, lease): + expiration = datetime.strptime(lease['expires'], '%Y-%m-%d %H:%M:%S') + time_to_expire = expiration - datetime.now() + LOGGER.info('Time until lease expiration: ' + str(time_to_expire)) + LOGGER.info('Waiting for current lease to expire: ' + str(expiration)) + if time_to_expire.total_seconds() > 0: + time.sleep(time_to_expire.total_seconds() + + 5) # Wait until the expiration time and padd 5 seconds + LOGGER.info('Current lease expired.') + + def _change_subnet(self, subnet): + LOGGER.info('Changing subnet to: ' + str(subnet)) + response = self.dhcp1_client.set_dhcp_range(subnet['start'], subnet['end']) + if response.code == 200: + LOGGER.info('Subnet change request accepted. Confirming change...') + response = self.dhcp1_client.get_dhcp_range() + if response.code == 200: + if response.start == subnet['start'] and response.end == subnet['end']: + LOGGER.info('Subnet change confirmed') + return True + LOGGER.error('Failed to confirm subnet change') + else: + LOGGER.error('Subnet change request failed.') + return False + + def _get_cur_lease(self): + LOGGER.info('Checking current device lease') + response = self.dhcp1_client.get_lease(self._device_mac) + if response.code == 200: + lease = eval(response.message) # pylint: disable=W0123 + if lease: # Check if non-empty lease + return lease + else: + return None + + def _is_lease_active(self, lease): + if 'ip' in lease: + ip_addr = lease['ip'] + LOGGER.info('Lease IP Resolved: ' + ip_addr) + LOGGER.info('Attempting to ping device...') + ping_success = self._ping(self._device_ipv4_addr) + LOGGER.info('Ping Success: ' + str(ping_success)) + LOGGER.info('Current lease confirmed active in device') + return ping_success + + def test_subnets(self, subnets): + results = [] + for subnet in subnets: + result = {} + try: + lease = self._get_cur_lease() + if lease is not None: + result = self._test_subnet(subnet, lease) + if result: + result = { + 'result': + True, + 'details': + 'Subnet ' + subnet['start'] + '-' + subnet['end'] + ' passed' + } + else: + result = { + 'result': + False, + 'details': + 'Subnet ' + subnet['start'] + '-' + subnet['end'] + ' failed' + } + except Exception as e: # pylint: disable=W0718 + result = {'result': False, 'details': 'Subnet test failed: ' + str(e)} + results.append(result) + return results diff --git a/testing/test_pylint b/testing/test_pylint index 2ba696af5..3f4063812 100755 --- a/testing/test_pylint +++ b/testing/test_pylint @@ -38,4 +38,4 @@ if (( $new_errors > $ERROR_LIMIT)); then exit 1 fi -exit 0 +exit 0 \ No newline at end of file