From 4a5c1eaa532ba9970391c75ade52c6befb8719bd Mon Sep 17 00:00:00 2001 From: jhughesbiot <50999916+jhughesbiot@users.noreply.github.com> Date: Wed, 5 Jul 2023 07:26:08 -0700 Subject: [PATCH 1/4] Dhcp (#64) * Add initial work for ip control module * Implement ip control module with additional cleanup methods * Update link check to not use error stream * Add error checking around container network configurations * Add network cleanup for namespaces and links * formatting * initial work on adding grpc functions for dhcp tests * rework code to allow for better usage and unit testing * working poc for test containers and grpc client to dhcp-1 * Move grpc client code into base image * Move grpc proto builds outside of dockerfile into module startup script * Setup pythonpath var in test module base startup process misc cleanup * pylinting and logging updates * Add python path resolving to network modules Update grpc path to prevent conflicts misc pylinting * Change lease resolving method to fix pylint issue * cleanup unit tests * cleanup unit tests * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * fix line endings * misc cleanup --- local/system.json.example | 18 +- modules/network/base/base.Dockerfile | 4 + modules/network/base/bin/setup_python_path | 25 + modules/network/base/bin/start_grpc | 6 +- modules/network/base/bin/start_module | 12 +- modules/network/base/python/requirements.txt | 3 +- .../src/{grpc => grpc_server}/start_server.py | 0 modules/network/base/python/src/logger.py | 2 +- .../network/dhcp-1/bin/start_network_service | 2 +- modules/network/dhcp-1/conf/dhcpd.conf | 54 +- modules/network/dhcp-1/dhcp-1.Dockerfile | 8 +- .../dhcp-1/python/src/grpc/dhcp_config.py | 303 ----------- .../dhcp-1/python/src/grpc/network_service.py | 58 --- .../dhcp-1/python/src/grpc/proto/grpc.proto | 36 -- .../src/{grpc => grpc_server}/__init__.py | 0 .../python/src/grpc_server/dhcp_config.py | 493 ++++++++++++++++++ .../src/grpc_server/dhcp_config_test.py | 103 ++++ .../python/src/grpc_server/dhcp_lease.py | 75 +++ .../python/src/grpc_server/dhcp_leases.py | 107 ++++ .../python/src/grpc_server/network_service.py | 157 ++++++ .../python/src/grpc_server/proto/grpc.proto | 59 +++ .../network/dhcp-2/bin/start_network_service | 2 +- modules/network/dhcp-2/conf/dhcpd.conf | 35 +- .../dhcp-2/python/src/grpc/dhcp_config.py | 303 ----------- .../dhcp-2/python/src/grpc/network_service.py | 58 --- .../dhcp-2/python/src/grpc/proto/grpc.proto | 36 -- .../src/{grpc => grpc_server}/__init__.py | 0 .../python/src/grpc_server/dhcp_config.py | 493 ++++++++++++++++++ .../src/grpc_server/dhcp_config_test.py | 103 ++++ .../python/src/grpc_server/dhcp_lease.py | 75 +++ .../python/src/grpc_server/dhcp_leases.py | 107 ++++ .../python/src/grpc_server/network_service.py | 157 ++++++ .../python/src/grpc_server/proto/grpc.proto | 59 +++ modules/test/base/base.Dockerfile | 8 + modules/test/base/bin/setup_grpc_clients | 34 ++ modules/test/base/bin/setup_python_path | 25 + modules/test/base/bin/start_module | 17 +- .../python/src/grpc/proto/dhcp1/client.py | 98 ++++ modules/test/conn/conn.Dockerfile | 4 +- .../test/conn/python/src/connection_module.py | 29 ++ testing/test_baseline | 2 +- testing/unit_test/run_tests.sh | 18 + 42 files changed, 2326 insertions(+), 862 deletions(-) create mode 100644 modules/network/base/bin/setup_python_path rename modules/network/base/python/src/{grpc => grpc_server}/start_server.py (100%) delete mode 100644 modules/network/dhcp-1/python/src/grpc/dhcp_config.py delete mode 100644 modules/network/dhcp-1/python/src/grpc/network_service.py delete mode 100644 modules/network/dhcp-1/python/src/grpc/proto/grpc.proto rename modules/network/dhcp-1/python/src/{grpc => grpc_server}/__init__.py (100%) create mode 100644 modules/network/dhcp-1/python/src/grpc_server/dhcp_config.py create mode 100644 modules/network/dhcp-1/python/src/grpc_server/dhcp_config_test.py create mode 100644 modules/network/dhcp-1/python/src/grpc_server/dhcp_lease.py create mode 100644 modules/network/dhcp-1/python/src/grpc_server/dhcp_leases.py create mode 100644 modules/network/dhcp-1/python/src/grpc_server/network_service.py create mode 100644 modules/network/dhcp-1/python/src/grpc_server/proto/grpc.proto delete mode 100644 modules/network/dhcp-2/python/src/grpc/dhcp_config.py delete mode 100644 modules/network/dhcp-2/python/src/grpc/network_service.py delete mode 100644 modules/network/dhcp-2/python/src/grpc/proto/grpc.proto rename modules/network/dhcp-2/python/src/{grpc => grpc_server}/__init__.py (100%) create mode 100644 modules/network/dhcp-2/python/src/grpc_server/dhcp_config.py create mode 100644 modules/network/dhcp-2/python/src/grpc_server/dhcp_config_test.py create mode 100644 modules/network/dhcp-2/python/src/grpc_server/dhcp_lease.py create mode 100644 modules/network/dhcp-2/python/src/grpc_server/dhcp_leases.py create mode 100644 modules/network/dhcp-2/python/src/grpc_server/network_service.py create mode 100644 modules/network/dhcp-2/python/src/grpc_server/proto/grpc.proto create mode 100644 modules/test/base/bin/setup_grpc_clients create mode 100644 modules/test/base/bin/setup_python_path create mode 100644 modules/test/base/python/src/grpc/proto/dhcp1/client.py create mode 100644 testing/unit_test/run_tests.sh diff --git a/local/system.json.example b/local/system.json.example index ecf480104..e99e013f3 100644 --- a/local/system.json.example +++ b/local/system.json.example @@ -1,10 +1,10 @@ -{ - "network": { - "device_intf": "enx123456789123", - "internet_intf": "enx123456789124" - }, - "log_level": "INFO", - "startup_timeout": 60, - "monitor_period": 300, - "runtime": 1200 +{ + "network": { + "device_intf": "enx123456789123", + "internet_intf": "enx123456789124" + }, + "log_level": "INFO", + "startup_timeout": 60, + "monitor_period": 300, + "runtime": 1200 } \ No newline at end of file diff --git a/modules/network/base/base.Dockerfile b/modules/network/base/base.Dockerfile index f8fa43c57..ac964a99d 100644 --- a/modules/network/base/base.Dockerfile +++ b/modules/network/base/base.Dockerfile @@ -17,10 +17,14 @@ FROM ubuntu:jammy ARG MODULE_NAME=base ARG MODULE_DIR=modules/network/$MODULE_NAME +ARG COMMON_DIR=framework/python/src/common # Install common software RUN apt-get update && apt-get install -y net-tools iputils-ping tcpdump iproute2 jq python3 python3-pip dos2unix +# Install common python modules +COPY $COMMON_DIR/ /testrun/python/src/common + # Setup the base python requirements COPY $MODULE_DIR/python /testrun/python diff --git a/modules/network/base/bin/setup_python_path b/modules/network/base/bin/setup_python_path new file mode 100644 index 000000000..3e30e965d --- /dev/null +++ b/modules/network/base/bin/setup_python_path @@ -0,0 +1,25 @@ +#!/bin/bash + +ROOT_DIRECTORY="/testrun/python/src" + +# Function to recursively add subdirectories to PYTHONPATH +add_subdirectories_to_pythonpath() { + local directory=$1 + local subdirectories=( "$directory"/* ) + local subdirectory + + for subdirectory in "${subdirectories[@]}"; do + if [[ -d "$subdirectory" && ! "$subdirectory" = *'__pycache__' ]]; then + export PYTHONPATH="$PYTHONPATH:$subdirectory" + add_subdirectories_to_pythonpath "$subdirectory" + fi + done +} + +# Set PYTHONPATH initially to an empty string +export PYTHONPATH="$ROOT_DIRECTORY" + +# Add all subdirectories to PYTHONPATH +add_subdirectories_to_pythonpath "$ROOT_DIRECTORY" + +echo "$PYTHONPATH" \ No newline at end of file diff --git a/modules/network/base/bin/start_grpc b/modules/network/base/bin/start_grpc index 56f915db7..840bea65f 100644 --- a/modules/network/base/bin/start_grpc +++ b/modules/network/base/bin/start_grpc @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -GRPC_DIR="/testrun/python/src/grpc" +GRPC_DIR="/testrun/python/src/grpc_server" GRPC_PROTO_DIR="proto" GRPC_PROTO_FILE="grpc.proto" @@ -22,10 +22,10 @@ GRPC_PROTO_FILE="grpc.proto" pushd $GRPC_DIR >/dev/null 2>&1 #Build the grpc proto file every time before starting server -python3 -m grpc_tools.protoc --proto_path=. ./$GRPC_PROTO_DIR/$GRPC_PROTO_FILE --python_out=. --grpc_python_out=. +python3 -u -m grpc_tools.protoc --proto_path=. ./$GRPC_PROTO_DIR/$GRPC_PROTO_FILE --python_out=. --grpc_python_out=. popd >/dev/null 2>&1 #Start the grpc server -python3 -u $GRPC_DIR/start_server.py $@ +python3 -u $GRPC_DIR/start_server.py $@ & diff --git a/modules/network/base/bin/start_module b/modules/network/base/bin/start_module index e00747b43..6de62f1a5 100644 --- a/modules/network/base/bin/start_module +++ b/modules/network/base/bin/start_module @@ -60,10 +60,16 @@ else INTF=$DEFINED_IFACE fi -echo "Starting module $MODULE_NAME on local interface $INTF..." +# Setup the PYTHONPATH so all imports work as expected +echo "Setting up PYTHONPATH..." +export PYTHONPATH=$($BIN_DIR/setup_python_path) +echo "PYTHONPATH: $PYTHONPATH" +echo "Configuring binary files..." $BIN_DIR/setup_binaries $BIN_DIR +echo "Starting module $MODULE_NAME on local interface $INTF..." + # Wait for interface to become ready $BIN_DIR/wait_for_interface $INTF @@ -80,9 +86,9 @@ then if [[ ! -z $GRPC_PORT && ! $GRPC_PORT == "null" ]] then echo "gRPC port resolved from config: $GRPC_PORT" - $BIN_DIR/start_grpc "-p $GRPC_PORT" & + $BIN_DIR/start_grpc "-p $GRPC_PORT" else - $BIN_DIR/start_grpc & + $BIN_DIR/start_grpc fi fi diff --git a/modules/network/base/python/requirements.txt b/modules/network/base/python/requirements.txt index 9c4e2b056..9d9473d74 100644 --- a/modules/network/base/python/requirements.txt +++ b/modules/network/base/python/requirements.txt @@ -1,2 +1,3 @@ grpcio -grpcio-tools \ No newline at end of file +grpcio-tools +netifaces \ No newline at end of file diff --git a/modules/network/base/python/src/grpc/start_server.py b/modules/network/base/python/src/grpc_server/start_server.py similarity index 100% rename from modules/network/base/python/src/grpc/start_server.py rename to modules/network/base/python/src/grpc_server/start_server.py diff --git a/modules/network/base/python/src/logger.py b/modules/network/base/python/src/logger.py index 8893b1e8d..998a4aaae 100644 --- a/modules/network/base/python/src/logger.py +++ b/modules/network/base/python/src/logger.py @@ -35,7 +35,7 @@ log_level = logging.getLevelName(log_level_str) except OSError: # TODO: Print out warning that log level is incorrect or missing - LOG_LEVEL = _DEFAULT_LEVEL + log_level = _DEFAULT_LEVEL log_format = logging.Formatter(fmt=_LOG_FORMAT, datefmt=_DATE_FORMAT) diff --git a/modules/network/dhcp-1/bin/start_network_service b/modules/network/dhcp-1/bin/start_network_service index fbeede871..9f4a3dc51 100644 --- a/modules/network/dhcp-1/bin/start_network_service +++ b/modules/network/dhcp-1/bin/start_network_service @@ -20,7 +20,7 @@ DHCP_LOG_FILE=/runtime/network/dhcp1-dhcpd.log RA_PID_FILE=/var/run/radvd/radvd.pid RA_LOG_FILE=/runtime/network/dhcp1-radvd.log -echo "Starrting Network Service..." +echo "Starting Network Service..." #Enable IPv6 Forwarding sysctl net.ipv6.conf.all.forwarding=1 diff --git a/modules/network/dhcp-1/conf/dhcpd.conf b/modules/network/dhcp-1/conf/dhcpd.conf index 9f4fe1c28..ee171279c 100644 --- a/modules/network/dhcp-1/conf/dhcpd.conf +++ b/modules/network/dhcp-1/conf/dhcpd.conf @@ -1,26 +1,28 @@ -default-lease-time 300; - -failover peer "failover-peer" { - primary; - address 10.10.10.2; - port 847; - peer address 10.10.10.3; - peer port 647; - max-response-delay 60; - max-unacked-updates 10; - mclt 3600; - split 128; - 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; - pool { - failover peer "failover-peer"; - range 10.10.10.10 10.10.10.20; - } -} +default-lease-time 300; + +failover peer "failover-peer" { + primary; + address 10.10.10.2; + port 847; + peer address 10.10.10.3; + peer port 647; + max-response-delay 60; + max-unacked-updates 10; + mclt 3600; + split 128; + 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; + authoritative; + pool { + failover peer "failover-peer"; + range 10.10.10.10 10.10.10.20; + } +} \ No newline at end of file diff --git a/modules/network/dhcp-1/dhcp-1.Dockerfile b/modules/network/dhcp-1/dhcp-1.Dockerfile index a4eb8d90a..b47378045 100644 --- a/modules/network/dhcp-1/dhcp-1.Dockerfile +++ b/modules/network/dhcp-1/dhcp-1.Dockerfile @@ -18,6 +18,12 @@ FROM test-run/base:latest ARG MODULE_NAME=dhcp-1 ARG MODULE_DIR=modules/network/$MODULE_NAME +# Install all necessary packages +RUN apt-get install -y wget + +#Update the oui.txt file from ieee +RUN wget http://standards-oui.ieee.org/oui.txt -P /usr/local/etc/ + # Install dhcp server RUN apt-get install -y isc-dhcp-server radvd @@ -28,4 +34,4 @@ COPY $MODULE_DIR/conf /testrun/conf COPY $MODULE_DIR/bin /testrun/bin # Copy over all python files -COPY $MODULE_DIR/python /testrun/python +COPY $MODULE_DIR/python /testrun/python \ No newline at end of file diff --git a/modules/network/dhcp-1/python/src/grpc/dhcp_config.py b/modules/network/dhcp-1/python/src/grpc/dhcp_config.py deleted file mode 100644 index 99d6bdebd..000000000 --- a/modules/network/dhcp-1/python/src/grpc/dhcp_config.py +++ /dev/null @@ -1,303 +0,0 @@ -# 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. - -"""Contains all the necessary classes to maintain the -DHCP server's configuration""" -import re - -CONFIG_FILE = '/etc/dhcp/dhcpd.conf' -CONFIG_FILE_TEST = 'network/modules/dhcp-1/conf/dhcpd.conf' - -DEFAULT_LEASE_TIME_KEY = 'default-lease-time' - - -class DHCPConfig: - """Represents the DHCP Servers configuration and gives access to modify it""" - - def __init__(self): - self._default_lease_time = 300 - self.subnets = [] - self._peer = None - - def write_config(self): - conf = str(self) - print('Writing config: \n' + conf) - with open(CONFIG_FILE, 'w', encoding='UTF-8') as conf_file: - conf_file.write(conf) - - def resolve_config(self): - with open(CONFIG_FILE, 'r', encoding='UTF-8') as f: - conf = f.read() - self.resolve_subnets(conf) - self._peer = DHCPFailoverPeer(conf) - - def resolve_subnets(self, conf): - self.subnets = [] - regex = r'(subnet.*)' - subnets = re.findall(regex, conf, re.MULTILINE | re.DOTALL) - for subnet in subnets: - dhcp_subnet = DHCPSubnet(subnet) - self.subnets.append(dhcp_subnet) - - def set_range(self, start, end, subnet=0, pool=0): - print('Setting Range for pool ') - print(self.subnets[subnet].pools[pool]) - self.subnets[subnet].pools[pool].range_start = start - self.subnets[subnet].pools[pool].range_end = end - - # def resolve_settings(self, conf): - # lines = conf.split('\n') - # for line in lines: - # if DEFAULT_LEASE_TIME_KEY in line: - # self._default_lease_time = line.strip().split( - # DEFAULT_LEASE_TIME_KEY)[1].strip().split(';')[0] - - # self.peer = peer - - def __str__(self): - - config = """\r{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) - - config += '\n\n' + str(self.peer) - for subnet in self._subnets: - config += '\n\n' + str(subnet) - return str(config) - - -FAILOVER_PEER_KEY = 'failover peer' -PRIMARY_KEY = 'primary' -ADDRESS_KEY = 'address' -PORT_KEY = 'port' -PEER_ADDRESS_KEY = 'peer address' -PEER_PORT_KEY = 'peer port' -MAX_RESPONSE_DELAY_KEY = 'max-response-delay' -MAX_UNACKED_UPDATES_KEY = 'max-unacked-updates' -MCLT_KEY = 'mclt' -SPLIT_KEY = 'split' -LOAD_BALANCE_MAX_SECONDS_KEY = 'load balance max seconds' - - -class DHCPFailoverPeer: - """Contains all information to define the DHCP failover peer""" - - def __init__(self, config): - self.name = None - self.primary = False - self.address = None - self.port = None - self.peer_address = None - self.peer_port = None - self.max_response_delay = None - self.max_unacked_updates = None - self.mclt = None - self.split = None - self.load_balance_max_seconds = None - self.peer = None - - self.resolve_peer(config) - - def __str__(self): - config = '{FAILOVER_PEER_KEY} \"{FAILOVER_PEER}\" {{\n' - config += '\tprimary;' if self.primary else 'secondary;' - config += """\n\t{ADDRESS_KEY} {ADDRESS}; - {PORT_KEY} {PORT}; - {PEER_ADDRESS_KEY} {PEER_ADDRESS}; - {PEER_PORT_KEY} {PEER_PORT}; - {MAX_RESPONSE_DELAY_KEY} {MAX_RESPONSE_DELAY}; - {MAX_UNACKED_UPDATES_KEY} {MAX_UNACKED_UPDATES}; - {MCLT_KEY} {MCLT}; - {SPLIT_KEY} {SPLIT}; - {LOAD_BALANCE_MAX_SECONDS_KEY} {LOAD_BALANCE_MAX_SECONDS}; - \r}}""" - - return config.format( - length='multi-line', - FAILOVER_PEER_KEY=FAILOVER_PEER_KEY, - FAILOVER_PEER=self.name, - ADDRESS_KEY=ADDRESS_KEY, - ADDRESS=self.address, - PORT_KEY=PORT_KEY, - PORT=self.port, - PEER_ADDRESS_KEY=PEER_ADDRESS_KEY, - PEER_ADDRESS=self.peer_address, - PEER_PORT_KEY=PEER_PORT_KEY, - PEER_PORT=self.peer_port, - MAX_RESPONSE_DELAY_KEY=MAX_RESPONSE_DELAY_KEY, - MAX_RESPONSE_DELAY=self.max_response_delay, - MAX_UNACKED_UPDATES_KEY=MAX_UNACKED_UPDATES_KEY, - MAX_UNACKED_UPDATES=self.max_unacked_updates, - MCLT_KEY=MCLT_KEY, - MCLT=self.mclt, - SPLIT_KEY=SPLIT_KEY, - SPLIT=self.split, - LOAD_BALANCE_MAX_SECONDS_KEY=LOAD_BALANCE_MAX_SECONDS_KEY, - LOAD_BALANCE_MAX_SECONDS=self.load_balance_max_seconds) - - def resolve_peer(self, conf): - peer = '' - lines = conf.split('\n') - for line in lines: - if line.startswith(FAILOVER_PEER_KEY) or len(peer) > 0: - if len(peer) <= 0: - self.name = line.strip().split(FAILOVER_PEER_KEY)[1].strip().split( - '{')[0].split('\"')[1] - peer += line + '\n' - if PRIMARY_KEY in line: - self.primary = True - elif ADDRESS_KEY in line and PEER_ADDRESS_KEY not in line: - self.address = line.strip().split(ADDRESS_KEY)[1].strip().split( - ';')[0] - elif PORT_KEY in line and PEER_PORT_KEY not in line: - self.port = line.strip().split(PORT_KEY)[1].strip().split(';')[0] - elif PEER_ADDRESS_KEY in line: - self.peer_address = line.strip().split( - PEER_ADDRESS_KEY)[1].strip().split(';')[0] - elif PEER_PORT_KEY in line: - self.peer_port = line.strip().split(PEER_PORT_KEY)[1].strip().split( - ';')[0] - elif MAX_RESPONSE_DELAY_KEY in line: - self.max_response_delay = line.strip().split( - MAX_RESPONSE_DELAY_KEY)[1].strip().split(';')[0] - elif MAX_UNACKED_UPDATES_KEY in line: - self.max_unacked_updates = line.strip().split( - MAX_UNACKED_UPDATES_KEY)[1].strip().split(';')[0] - elif MCLT_KEY in line: - self.mclt = line.strip().split(MCLT_KEY)[1].strip().split(';')[0] - elif SPLIT_KEY in line: - self.split = line.strip().split(SPLIT_KEY)[1].strip().split(';')[0] - elif LOAD_BALANCE_MAX_SECONDS_KEY in line: - self.load_balance_max_seconds = line.strip().split( - LOAD_BALANCE_MAX_SECONDS_KEY)[1].strip().split(';')[0] - if line.endswith('}') and len(peer) > 0: - break - self.peer = peer - - -NTP_OPTION_KEY = 'option ntp-servers' -SUBNET_MASK_OPTION_KEY = 'option subnet-mask' -BROADCAST_OPTION_KEY = 'option broadcast-address' -ROUTER_OPTION_KEY = 'option routers' -DNS_OPTION_KEY = 'option domain-name-servers' - - -class DHCPSubnet: - """Represents the DHCP Servers subnet configuration""" - - def __init__(self, subnet): - self._ntp_servers = None - self._subnet_mask = None - self._broadcast = None - self._routers = None - self._dns_servers = None - self.pools = [] - - self.resolve_subnet(subnet) - self.resolve_pools(subnet) - - def __str__(self): - config = """subnet 10.10.10.0 netmask {SUBNET_MASK_OPTION} {{ - \r\t{NTP_OPTION_KEY} {NTP_OPTION}; - \r\t{SUBNET_MASK_OPTION_KEY} {SUBNET_MASK_OPTION}; - \r\t{BROADCAST_OPTION_KEY} {BROADCAST_OPTION}; - \r\t{ROUTER_OPTION_KEY} {ROUTER_OPTION}; - \r\t{DNS_OPTION_KEY} {DNS_OPTION};""" - - config = config.format(length='multi-line', - NTP_OPTION_KEY=NTP_OPTION_KEY, - NTP_OPTION=self._ntp_servers, - SUBNET_MASK_OPTION_KEY=SUBNET_MASK_OPTION_KEY, - SUBNET_MASK_OPTION=self._subnet_mask, - BROADCAST_OPTION_KEY=BROADCAST_OPTION_KEY, - BROADCAST_OPTION=self._broadcast, - ROUTER_OPTION_KEY=ROUTER_OPTION_KEY, - ROUTER_OPTION=self._routers, - DNS_OPTION_KEY=DNS_OPTION_KEY, - DNS_OPTION=self._dns_servers) - for pool in self.pools: - config += '\n\t' + str(pool) - - config += '\n\r}' - return config - - def resolve_subnet(self, subnet): - subnet_parts = subnet.split('\n') - for part in subnet_parts: - if NTP_OPTION_KEY in part: - self._ntp_servers = part.strip().split(NTP_OPTION_KEY)[1].strip().split( - ';')[0] - elif SUBNET_MASK_OPTION_KEY in part: - self._subnet_mask = part.strip().split( - SUBNET_MASK_OPTION_KEY)[1].strip().split(';')[0] - elif BROADCAST_OPTION_KEY in part: - self._broadcast = part.strip().split( - BROADCAST_OPTION_KEY)[1].strip().split(';')[0] - elif ROUTER_OPTION_KEY in part: - self._routers = part.strip().split(ROUTER_OPTION_KEY)[1].strip().split( - ';')[0] - elif DNS_OPTION_KEY in part: - self._dns_servers = part.strip().split(DNS_OPTION_KEY)[1].strip().split( - ';')[0] - - def resolve_pools(self, subnet): - regex = r'(pool.*)\}' - pools = re.findall(regex, subnet, re.MULTILINE | re.DOTALL) - for pool in pools: - dhcp_pool = DHCPPool(pool) - self.pools.append(dhcp_pool) - - -FAILOVER_KEY = 'failover peer' -RANGE_KEY = 'range' - - -class DHCPPool: - """Represents a DHCP Servers subnet pool configuration""" - - def __init__(self, pool): - self.failover_peer = None - self.range_start = None - self.range_end = None - self.resolve_pool(pool) - - def __str__(self): - - config = """pool {{ - \r\t\t{FAILOVER_KEY} "{FAILOVER}"; - \r\t\t{RANGE_KEY} {RANGE_START} {RANGE_END}; - \r\t}}""" - - return config.format( - length='multi-line', - FAILOVER_KEY=FAILOVER_KEY, - FAILOVER=self.failover_peer, - RANGE_KEY=RANGE_KEY, - RANGE_START=self.range_start, - RANGE_END=self.range_end, - ) - - def resolve_pool(self, pool): - pool_parts = pool.split('\n') - # pool_parts = pool.split("\n") - for part in pool_parts: - if FAILOVER_KEY in part: - self.failover_peer = part.strip().split(FAILOVER_KEY)[1].strip().split( - ';')[0].replace('\"', '') - if RANGE_KEY in part: - pool_range = part.strip().split(RANGE_KEY)[1].strip().split(';')[0] - self.range_start = pool_range.split(' ')[0].strip() - self.range_end = pool_range.split(' ')[1].strip() diff --git a/modules/network/dhcp-1/python/src/grpc/network_service.py b/modules/network/dhcp-1/python/src/grpc/network_service.py deleted file mode 100644 index 64aab8a07..000000000 --- a/modules/network/dhcp-1/python/src/grpc/network_service.py +++ /dev/null @@ -1,58 +0,0 @@ -# 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 Network Service for the DHCP Server network module""" -import proto.grpc_pb2_grpc as pb2_grpc -import proto.grpc_pb2 as pb2 - -from dhcp_config import DHCPConfig - - -class NetworkService(pb2_grpc.NetworkModule): - """gRPC endpoints for the DHCP Server""" - - def __init__(self): - self._dhcp_config = DHCPConfig() - - def GetDHCPRange(self, request, context): # pylint: disable=W0613 - """ - Resolve the current DHCP configuration and return - the first range from the first subnet in the file - """ - self._dhcp_config.resolve_config() - pool = self._dhcp_config.subnets[0].pools[0] - return pb2.DHCPRange(code=200, start=pool.range_start, end=pool.range_end) - - def SetDHCPRange(self, request, context): # pylint: disable=W0613 - """ - Change DHCP configuration and set the - the first range from the first subnet in the configuration - """ - - print('Setting DHCPRange') - print('Start: ' + request.start) - print('End: ' + request.end) - self._dhcp_config.resolve_config() - self._dhcp_config.set_range(request.start, request.end, 0, 0) - self._dhcp_config.write_config() - return pb2.Response(code=200, message='DHCP Range Set') - - def GetStatus(self, request, context): # pylint: disable=W0613 - """ - Return the current status of the network module - """ - # ToDo: Figure out how to resolve the current DHCP status - dhcp_status = True - message = str({'dhcpStatus': dhcp_status}) - return pb2.Response(code=200, message=message) diff --git a/modules/network/dhcp-1/python/src/grpc/proto/grpc.proto b/modules/network/dhcp-1/python/src/grpc/proto/grpc.proto deleted file mode 100644 index 8e2732620..000000000 --- a/modules/network/dhcp-1/python/src/grpc/proto/grpc.proto +++ /dev/null @@ -1,36 +0,0 @@ -syntax = "proto3"; - -service NetworkModule { - - rpc GetDHCPRange(GetDHCPRangeRequest) returns (DHCPRange) {}; - - rpc SetDHCPRange(DHCPRange) returns (Response) {}; - - rpc GetStatus(GetStatusRequest) returns (Response) {}; - - rpc GetIPAddress(GetIPAddressRequest) returns (Response) {}; - - rpc SetLeaseAddress(SetLeaseAddressRequest) returns (Response) {}; - -} - -message Response { - int32 code = 1; - string message = 2; -} - -message DHCPRange { - int32 code = 1; - string start = 2; - string end = 3; -} - -message GetDHCPRangeRequest {} - -message GetIPAddressRequest {} - -message GetStatusRequest {} - -message SetLeaseAddressRequest { - string ipAddress = 1; -} \ No newline at end of file diff --git a/modules/network/dhcp-1/python/src/grpc/__init__.py b/modules/network/dhcp-1/python/src/grpc_server/__init__.py similarity index 100% rename from modules/network/dhcp-1/python/src/grpc/__init__.py rename to modules/network/dhcp-1/python/src/grpc_server/__init__.py 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 new file mode 100644 index 000000000..444faa87c --- /dev/null +++ b/modules/network/dhcp-1/python/src/grpc_server/dhcp_config.py @@ -0,0 +1,493 @@ +# 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. +"""Contains all the necessary classes to maintain the +DHCP server's configuration""" +import re +from common import logger + +LOG_NAME = 'dhcp_config' +LOGGER = None + +CONFIG_FILE = '/etc/dhcp/dhcpd.conf' + +DEFAULT_LEASE_TIME_KEY = 'default-lease-time' + + +class DHCPConfig: + """Represents the DHCP Servers configuration and gives access to modify it""" + + def __init__(self): + self._default_lease_time = 300 + self._subnets = [] + self._peer = None + self._reserved_hosts = [] + global LOGGER + LOGGER = logger.get_logger(LOG_NAME, 'dhcp-1') + + def add_reserved_host(self, hostname, hw_addr, ip_addr): + host = DHCPReservedHost(hostname=hostname, + hw_addr=hw_addr, + fixed_addr=ip_addr) + self._reserved_hosts.append(host) + + def delete_reserved_host(self, hw_addr): + for host in self._reserved_hosts: + if hw_addr == host.hw_addr: + self._reserved_hosts.remove(host) + + def disable_failover(self): + self._peer.disable() + for subnet in self._subnets: + subnet.disable_peer() + + def enable_failover(self): + self._peer.enable() + for subnet in self._subnets: + subnet.enable_peer() + + def get_reserved_host(self, hw_addr): + for host in self._reserved_hosts: + if hw_addr == host.hw_addr: + return host + + def write_config(self, config=None): + if config is None: + conf = str(self) + with open(CONFIG_FILE, 'w', encoding='UTF-8') as conf_file: + conf_file.write(conf) + else: + with open(CONFIG_FILE, 'w', encoding='UTF-8') as conf_file: + conf_file.write(config) + + def _get_config(self, config_file=CONFIG_FILE): + content = None + with open(config_file, 'r', encoding='UTF-8') as f: + content = f.read() + return content + + def make(self, conf): + try: + self._subnets = self.resolve_subnets(conf) + self._peer = DHCPFailoverPeer(conf) + self._reserved_hosts = self.resolve_reserved_hosts(conf) + except Exception as e: # pylint: disable=W0718 + print('Failed to make DHCPConfig: ' + str(e)) + + def resolve_config(self, config_file=CONFIG_FILE): + try: + conf = self._get_config(config_file) + self._subnets = self.resolve_subnets(conf) + self._peer = DHCPFailoverPeer(conf) + self._reserved_hosts = self.resolve_reserved_hosts(conf) + except Exception as e: # pylint: disable=W0718 + print('Failed to resolve config: ' + str(e)) + + def resolve_subnets(self, conf): + subnets = [] + regex = r'(subnet.*)' + subnets_conf = re.findall(regex, conf, re.MULTILINE | re.DOTALL) + for subnet in subnets_conf: + dhcp_subnet = DHCPSubnet(subnet) + subnets.append(dhcp_subnet) + return subnets + + def resolve_reserved_hosts(self, conf): + hosts = [] + host_start = 0 + while True: + host_start = conf.find('host', host_start) + if host_start < 0: + break + else: + host_end = conf.find('}', host_start) + host = DHCPReservedHost(config=conf[host_start:host_end + 1]) + hosts.append(host) + host_start = host_end + 1 + return hosts + + def set_range(self, start, end, subnet=0, pool=0): + # Calculate the subnet from the range + octets = start.split('.') + octets[-1] = '0' + dhcp_subnet = '.'.join(octets) + + #Update the subnet and range + self._subnets[subnet].set_subnet(dhcp_subnet) + self._subnets[subnet].pools[pool].set_range(start, end) + + def __str__(self): + + # Encode the top level config options + 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) + + # Encode the failover peer + config += '\n\n' + str(self._peer) + + # Encode the subnets + for subnet in self._subnets: + config += '\n\n' + str(subnet) + + # Encode the reserved hosts + for host in self._reserved_hosts: + config += '\n' + str(host) + + return str(config) + + +FAILOVER_PEER_KEY = 'failover peer' +PRIMARY_KEY = 'primary' +ADDRESS_KEY = 'address' +PORT_KEY = 'port' +PEER_ADDRESS_KEY = 'peer address' +PEER_PORT_KEY = 'peer port' +MAX_RESPONSE_DELAY_KEY = 'max-response-delay' +MAX_UNACKED_UPDATES_KEY = 'max-unacked-updates' +MCLT_KEY = 'mclt' +SPLIT_KEY = 'split' +LOAD_BALANCE_MAX_SECONDS_KEY = 'load balance max seconds' + + +class DHCPFailoverPeer: + """Contains all information to define the DHCP failover peer""" + + def __init__(self, config): + self.name = None + self.primary = False + self.address = None + self.port = None + self.peer_address = None + self.peer_port = None + self.max_response_delay = None + self.max_unacked_updates = None + self.mclt = None + self.split = None + self.load_balance_max_seconds = None + self.peer = None + self.enabled = True + + self.resolve_peer(config) + + def __str__(self): + config = '{FAILOVER_PEER_KEY} \"{FAILOVER_PEER}\" {{\n' + config += '\tprimary;' if self.primary else 'secondary;' + config += '\n\t{ADDRESS_KEY} {ADDRESS};' if self.address is not None else '' + config += '\n\t{PORT_KEY} {PORT};' if self.port is not None else '' + config += '\n\t{PEER_ADDRESS_KEY} {PEER_ADDRESS};' if self.peer_address is not None else '' + config += '\n\t{PEER_PORT_KEY} {PEER_PORT};' if self.peer_port is not None else '' + config += '\n\t{MAX_RESPONSE_DELAY_KEY} {MAX_RESPONSE_DELAY};' if self.max_response_delay is not None else '' + config += '\n\t{MAX_UNACKED_UPDATES_KEY} {MAX_UNACKED_UPDATES};' if self.max_unacked_updates is not None else '' + config += '\n\t{MCLT_KEY} {MCLT};' if self.mclt is not None else '' + config += '\n\t{SPLIT_KEY} {SPLIT};' if self.split is not None else '' + config += '\n\t{LOAD_BALANCE_MAX_SECONDS_KEY} {LOAD_BALANCE_MAX_SECONDS};' if self.load_balance_max_seconds is not None else '' + config += '\n\r}}' + + config = config.format( + length='multi-line', + FAILOVER_PEER_KEY=FAILOVER_PEER_KEY, + FAILOVER_PEER=self.name, + ADDRESS_KEY=ADDRESS_KEY, + ADDRESS=self.address, + PORT_KEY=PORT_KEY, + PORT=self.port, + PEER_ADDRESS_KEY=PEER_ADDRESS_KEY, + PEER_ADDRESS=self.peer_address, + PEER_PORT_KEY=PEER_PORT_KEY, + PEER_PORT=self.peer_port, + MAX_RESPONSE_DELAY_KEY=MAX_RESPONSE_DELAY_KEY, + MAX_RESPONSE_DELAY=self.max_response_delay, + MAX_UNACKED_UPDATES_KEY=MAX_UNACKED_UPDATES_KEY, + MAX_UNACKED_UPDATES=self.max_unacked_updates, + MCLT_KEY=MCLT_KEY, + MCLT=self.mclt, + SPLIT_KEY=SPLIT_KEY, + SPLIT=self.split, + LOAD_BALANCE_MAX_SECONDS_KEY=LOAD_BALANCE_MAX_SECONDS_KEY, + LOAD_BALANCE_MAX_SECONDS=self.load_balance_max_seconds) + + if not self.enabled: + lines = config.strip().split('\n') + for i in range(len(lines)-1): + lines[i] = '#' + lines[i] + lines[-1] = '#' + lines[-1].strip() # Handle the last line separately + config = '\n'.join(lines) + + return config + + def disable(self): + self.enabled = False + + def enable(self): + self.enabled = True + + def resolve_peer(self, conf): + peer = '' + lines = conf.split('\n') + for line in lines: + if line.startswith(FAILOVER_PEER_KEY) or len(peer) > 0: + if len(peer) <= 0: + self.name = line.strip().split(FAILOVER_PEER_KEY)[1].strip().split( + '{')[0].split('\"')[1] + peer += line + '\n' + if PRIMARY_KEY in line: + self.primary = True + elif ADDRESS_KEY in line and PEER_ADDRESS_KEY not in line: + self.address = line.strip().split(ADDRESS_KEY)[1].strip().split( + ';')[0] + elif PORT_KEY in line and PEER_PORT_KEY not in line: + self.port = line.strip().split(PORT_KEY)[1].strip().split(';')[0] + elif PEER_ADDRESS_KEY in line: + self.peer_address = line.strip().split( + PEER_ADDRESS_KEY)[1].strip().split(';')[0] + elif PEER_PORT_KEY in line: + self.peer_port = line.strip().split(PEER_PORT_KEY)[1].strip().split( + ';')[0] + elif MAX_RESPONSE_DELAY_KEY in line: + self.max_response_delay = line.strip().split( + MAX_RESPONSE_DELAY_KEY)[1].strip().split(';')[0] + elif MAX_UNACKED_UPDATES_KEY in line: + self.max_unacked_updates = line.strip().split( + MAX_UNACKED_UPDATES_KEY)[1].strip().split(';')[0] + elif MCLT_KEY in line: + self.mclt = line.strip().split(MCLT_KEY)[1].strip().split(';')[0] + elif SPLIT_KEY in line: + self.split = line.strip().split(SPLIT_KEY)[1].strip().split(';')[0] + elif LOAD_BALANCE_MAX_SECONDS_KEY in line: + self.load_balance_max_seconds = line.strip().split( + LOAD_BALANCE_MAX_SECONDS_KEY)[1].strip().split(';')[0] + if line.endswith('}') and len(peer) > 0: + break + self.peer = peer + + +SUBNET_KEY = 'subnet' +NTP_OPTION_KEY = 'option ntp-servers' +SUBNET_MASK_OPTION_KEY = 'option subnet-mask' +BROADCAST_OPTION_KEY = 'option broadcast-address' +ROUTER_OPTION_KEY = 'option routers' +DNS_OPTION_KEY = 'option domain-name-servers' +INTERFACE_KEY = 'interface' +AUTHORITATIVE_KEY = 'authoritative' + + +class DHCPSubnet: + """Represents the DHCP Servers subnet configuration""" + + def __init__(self, subnet): + self._authoritative = False + self._subnet = None + self._ntp_servers = None + self._subnet_mask = None + self._broadcast = None + self._routers = None + self._dns_servers = None + self._interface = None + self.pools = [] + + self.resolve_subnet(subnet) + self.resolve_pools(subnet) + + def __str__(self): + config = 'subnet {SUBNET_OPTION} netmask {SUBNET_MASK_OPTION} {{' + config += '\n\t{NTP_OPTION_KEY} {NTP_OPTION};' if self._ntp_servers is not None else '' + config += '\n\t{SUBNET_MASK_OPTION_KEY} {SUBNET_MASK_OPTION};' if self._subnet_mask is not None else '' + config += '\n\t{BROADCAST_OPTION_KEY} {BROADCAST_OPTION};' if self._broadcast is not None else '' + config += '\n\t{ROUTER_OPTION_KEY} {ROUTER_OPTION};' if self._routers is not None else '' + config += '\n\t{DNS_OPTION_KEY} {DNS_OPTION};' if self._dns_servers is not None else '' + config += '\n\t{INTERFACE_KEY} {INTERFACE_OPTION};' if self._interface is not None else '' + config += '\n\t{AUTHORITATIVE_KEY};' if self._authoritative else '' + + + config = config.format(length='multi-line', + SUBNET_OPTION=self._subnet, + NTP_OPTION_KEY=NTP_OPTION_KEY, + NTP_OPTION=self._ntp_servers, + SUBNET_MASK_OPTION_KEY=SUBNET_MASK_OPTION_KEY, + SUBNET_MASK_OPTION=self._subnet_mask, + BROADCAST_OPTION_KEY=BROADCAST_OPTION_KEY, + BROADCAST_OPTION=self._broadcast, + ROUTER_OPTION_KEY=ROUTER_OPTION_KEY, + ROUTER_OPTION=self._routers, + DNS_OPTION_KEY=DNS_OPTION_KEY, + DNS_OPTION=self._dns_servers, + INTERFACE_KEY=INTERFACE_KEY, + INTERFACE_OPTION=self._interface, + AUTHORITATIVE_KEY=AUTHORITATIVE_KEY) + + # if not self._authoritative: + # config = config.replace(AUTHORITATIVE_KEY, '#' + AUTHORITATIVE_KEY) + + for pool in self.pools: + config += '\n\t' + str(pool) + + config += '\n}' + return config + + def disable_peer(self): + for pool in self.pools: + pool.disable_peer() + + def enable_peer(self): + for pool in self.pools: + pool.enable_peer() + + def set_subnet(self, subnet, netmask=None): + if netmask is None: + netmask = '255.255.255.0' + self._subnet = subnet + self._subnet_mask = netmask + + # Calculate the broadcast from the subnet + octets = subnet.split('.') + octets[-1] = '255' + dhcp_broadcast = '.'.join(octets) + + self._broadcast = dhcp_broadcast + + def resolve_subnet(self, subnet): + subnet_parts = subnet.split('\n') + for part in subnet_parts: + if part.strip().startswith(SUBNET_KEY): + self._subnet = part.strip().split()[1] + elif NTP_OPTION_KEY in part: + self._ntp_servers = part.strip().split(NTP_OPTION_KEY)[1].strip().split( + ';')[0] + elif SUBNET_MASK_OPTION_KEY in part: + self._subnet_mask = part.strip().split( + SUBNET_MASK_OPTION_KEY)[1].strip().split(';')[0] + elif BROADCAST_OPTION_KEY in part: + self._broadcast = part.strip().split( + BROADCAST_OPTION_KEY)[1].strip().split(';')[0] + elif ROUTER_OPTION_KEY in part: + self._routers = part.strip().split(ROUTER_OPTION_KEY)[1].strip().split( + ';')[0] + elif DNS_OPTION_KEY in part: + self._dns_servers = part.strip().split(DNS_OPTION_KEY)[1].strip().split( + ';')[0] + elif INTERFACE_KEY in part: + self._interface = part.strip().split(INTERFACE_KEY)[1].strip().split( + ';')[0] + elif AUTHORITATIVE_KEY in part: + self._authoritative = True + + def resolve_pools(self, subnet): + regex = r'(pool.*)\}' + pools = re.findall(regex, subnet, re.MULTILINE | re.DOTALL) + for pool in pools: + dhcp_pool = DHCPPool(pool) + self.pools.append(dhcp_pool) + + +FAILOVER_KEY = 'failover peer' +RANGE_KEY = 'range' + + +class DHCPPool: + """Represents a DHCP Servers subnet pool configuration""" + + def __init__(self, pool): + self.failover_peer = None + self.range_start = None + self.range_end = None + self.resolve_pool(pool) + self._peer_enabled = True + + def __str__(self): + config = 'pool {{' + config += '\n\t\t{FAILOVER_KEY} "{FAILOVER}";' if self.failover_peer is not None else '' + config += '\n\t\t{RANGE_KEY} {RANGE_START} {RANGE_END};' if self.range_start is not None and self.range_end is not None else '' + config += '\n\t}}' + + config = config.format( + length='multi-line', + FAILOVER_KEY=FAILOVER_KEY, + FAILOVER=self.failover_peer, + RANGE_KEY=RANGE_KEY, + RANGE_START=self.range_start, + RANGE_END=self.range_end, + ) + + if not self._peer_enabled: + config = config.replace(FAILOVER_KEY, '#' + FAILOVER_KEY) + + return config + + def disable_peer(self): + self._peer_enabled = False + + def enable_peer(self): + self._peer_enabled = True + + def set_range(self, start, end): + self.range_start = start + self.range_end = end + + def resolve_pool(self, pool): + pool_parts = pool.split('\n') + for part in pool_parts: + if FAILOVER_KEY in part: + self.failover_peer = part.strip().split(FAILOVER_KEY)[1].strip().split( + ';')[0].replace('\"', '') + if RANGE_KEY in part: + pool_range = part.strip().split(RANGE_KEY)[1].strip().split(';')[0] + self.range_start = pool_range.split(' ')[0].strip() + self.range_end = pool_range.split(' ')[1].strip() + + +HOST_KEY = 'host' +HARDWARE_KEY = 'hardware ethernet' +FIXED_ADDRESS_KEY = 'fixed-address' + + +class DHCPReservedHost: + """Represents a DHCP Servers subnet pool configuration""" + + def __init__(self, hostname=None, hw_addr=None, fixed_addr=None, config=None): + if config is None: + self.host = hostname + self.hw_addr = hw_addr + self.fixed_addr = fixed_addr + else: + self.resolve_host(config) + + def __str__(self): + + config = """{HOST_KEY} {HOSTNAME} {{ + \r\t{HARDWARE_KEY} {HW_ADDR}; + \r\t{FIXED_ADDRESS_KEY} {RESERVED_IP}; + \r}}""" + + config = config.format( + length='multi-line', + HOST_KEY=HOST_KEY, + HOSTNAME=self.host, + HARDWARE_KEY=HARDWARE_KEY, + HW_ADDR=self.hw_addr, + FIXED_ADDRESS_KEY=FIXED_ADDRESS_KEY, + RESERVED_IP=self.fixed_addr, + ) + return config + + def resolve_host(self, reserved_host): + host_parts = reserved_host.split('\n') + for part in host_parts: + if HOST_KEY in part: + self.host = part.strip().split(HOST_KEY)[1].strip().split('{')[0] + elif HARDWARE_KEY in part: + self.hw_addr = part.strip().split(HARDWARE_KEY)[1].strip().split(';')[0] + elif FIXED_ADDRESS_KEY in part: + self.fixed_addr = part.strip().split( + FIXED_ADDRESS_KEY)[1].strip().split(';')[0] 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 new file mode 100644 index 000000000..2cc78403a --- /dev/null +++ b/modules/network/dhcp-1/python/src/grpc_server/dhcp_config_test.py @@ -0,0 +1,103 @@ +# 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. +"""Unit Testing for the DHCP Server config""" +import unittest +from dhcp_config import DHCPConfig +import os + +CONFIG_FILE = 'conf/dhcpd.conf' + +DHCP_CONFIG = None + +def get_config_file_path(): + dhcp_config = DHCPConfig() + current_dir = os.path.dirname(os.path.abspath(__file__)) + module_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(current_dir)))) + 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 + def setUpClass(cls): + # Resolve the config + global DHCP_CONFIG + DHCP_CONFIG = get_config() + + def test_resolve_config(self): + print('Test Resolve Config:\n' + str(DHCP_CONFIG)) + + # Resolve the raw config file + with open(get_config_file_path(),'r') as f: + lines = f.readlines() + + # Get the resolved config as a + conf_parts = str(DHCP_CONFIG).split('\n') + + # dhcpd conf is not picky about spacing so we just + # need to check contents of each line for matching + # to make sure evertying matches + for i in range(len(lines)): + self.assertEqual(lines[i].strip(),conf_parts[i].strip()) + + def test_disable_failover(self): + DHCP_CONFIG.disable_failover() + print('Test Disable Config:\n' + str(DHCP_CONFIG)) + config_lines = str(DHCP_CONFIG._peer).split('\n') + for line in config_lines: + self.assertTrue(line.startswith('#')) + + def test_enable_failover(self): + DHCP_CONFIG.enable_failover() + print('Test Enable Config:\n' + str(DHCP_CONFIG)) + config_lines = str(DHCP_CONFIG._peer).split('\n') + for line in config_lines: + self.assertFalse(line.startswith('#')) + + def test_add_reserved_host(self): + DHCP_CONFIG.add_reserved_host('test','00:11:22:33:44:55','192.168.10.5') + host = DHCP_CONFIG.get_reserved_host('00:11:22:33:44:55') + self.assertIsNotNone(host) + print('AddHostConfig:\n' + str(DHCP_CONFIG)) + + def test_delete_reserved_host(self): + DHCP_CONFIG.delete_reserved_host('00:11:22:33:44:55') + host = DHCP_CONFIG.get_reserved_host('00:11:22:33:44:55') + self.assertIsNone(host) + print('DeleteHostConfig:\n' + str(DHCP_CONFIG)) + + def test_resolve_config_with_hosts(self): + DHCP_CONFIG.add_reserved_host('test','00:11:22:33:44:55','192.168.10.5') + config_with_hosts = DHCPConfig() + config_with_hosts.make(str(DHCP_CONFIG)) + host = config_with_hosts.get_reserved_host('00:11:22:33:44:55') + self.assertIsNotNone(host) + print("ResolveConfigWithHosts:\n" + str(config_with_hosts)) + +if __name__ == '__main__': + suite = unittest.TestSuite() + suite.addTest(DHCPConfigTest('test_resolve_config')) + suite.addTest(DHCPConfigTest('test_disable_failover')) + suite.addTest(DHCPConfigTest('test_enable_failover')) + suite.addTest(DHCPConfigTest('test_add_reserved_host')) + suite.addTest(DHCPConfigTest('test_delete_reserved_host')) + suite.addTest(DHCPConfigTest('test_resolve_config_with_hosts')) + + runner = unittest.TextTestRunner() + runner.run(suite) \ No newline at end of file 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 new file mode 100644 index 000000000..0d2f43e3b --- /dev/null +++ b/modules/network/dhcp-1/python/src/grpc_server/dhcp_lease.py @@ -0,0 +1,75 @@ +# 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. +"""Contains all the necessary methods to create and monitor DHCP +leases on the server""" +from datetime import datetime +import time + +time_format = '%Y-%m-%d %H:%M:%S' + + +class DHCPLease(object): + """Represents a DHCP Server lease""" + hw_addr = None + ip = None + hostname = None + expires = None + + def __init__(self, lease): + self._make_lease(lease) + + def _make_lease(self, lease): + if lease is not None: + sections_raw = lease.split(' ') + sections = [] + for section in sections_raw: + if section.strip(): + sections.append(section) + self.hw_addr = sections[0] + self.ip = sections[1] + self.hostname = sections[2] + self.expires = sections[3] + '' '' + sections[4] + self.manufacturer = ' '.join(sections[5:]) + + def get_millis(self, timestamp): + dt_obj = datetime.strptime(timestamp, time_format) + millis = dt_obj.timestamp() * 1000 + return millis + + def get_expires_millis(self): + return self.get_millis(self.expires) + + def is_expired(self): + expires_millis = self.get_expires_millis() + cur_time = int(round(time.time()) * 1000) + return cur_time >= expires_millis + + def __str__(self): + lease = {} + if self.hw_addr is not None: + lease['hw_addr'] = self.hw_addr + + if self.ip is not None: + lease['ip'] = self.ip + + if self.hostname is not None: + lease['hostname'] = self.hostname + + if self.expires is not None: + lease['expires'] = self.expires + + if self.manufacturer is not None: + lease['manufacturer'] = self.manufacturer + + return str(lease) 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 new file mode 100644 index 000000000..698277a02 --- /dev/null +++ b/modules/network/dhcp-1/python/src/grpc_server/dhcp_leases.py @@ -0,0 +1,107 @@ +# 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. +"""Used to resolve the DHCP servers lease information""" +import os +from dhcp_lease import DHCPLease +import logger +from common import util + +LOG_NAME = 'dhcp_lease' +LOGGER = None + +DHCP_LEASE_FILES = [ + '/var/lib/dhcp/dhcpd.leases', '/var/lib/dhcp/dhcpd.leases~', + '/var/lib/dhcp/dhcpd6.leases', '/var/lib/dhcp/dhcpd6.leases~' +] +DHCP_CONFIG_FILE = '/etc/dhcp/dhcpd.conf' + + +class DHCPLeases: + """Leases for the DHCP server""" + + def __init__(self): + global LOGGER + LOGGER = logger.get_logger(LOG_NAME, 'dhcp-1') + + def delete_all_hosts(self): + LOGGER.info('Deleting hosts') + for lease in DHCP_LEASE_FILES: + LOGGER.info('Checking file: ' + lease) + if os.path.exists(lease): + LOGGER.info('File Exists: ' + lease) + try: + # Delete existing lease file + os.remove(lease) + except OSError as e: + LOGGER.info(f'Error occurred while deleting the file: {e}') + # Create an empty lease file + with open(lease, 'w', encoding='UTF-8'): + pass + + def get_lease(self, hw_addr): + for lease in self.get_leases(): + if lease.hw_addr == hw_addr: + return lease + + 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:] + lines = lease_list.split('\n') + for line in lines: + try: + lease = DHCPLease(line) + leases.append(lease) + except Exception as e: # pylint: disable=W0718 + # Let non lease lines file without extra checks + LOGGER.error('Making Lease Error: ' + str(e)) + LOGGER.error('Not a valid lease line: ' + line) + return leases + + def delete_lease(self, ip_addr): + LOGGER.info('Deleting lease') + for lease in DHCP_LEASE_FILES: + LOGGER.info('Checking file: ' + lease) + if os.path.exists(lease): + LOGGER.info('File Exists: ' + lease) + try: + # Delete existing lease file + with (open(lease, 'r', encoding='UTF-8')) as f: + contents = f.read() + + while ip_addr in contents: + ix_ip = contents.find(ip_addr) + lease_start = contents.rindex('lease', 0, ix_ip) + lease_end = contents.find('}', lease_start) + LOGGER.info('Lease Location: ' + str(lease_start) + ':' + + str(lease_end)) + contents = contents[0:lease_start] + contents[lease_end + 1:] + + except OSError as e: + LOGGER.info(f'Error occurred while deleting the lease: {e}') + + def _get_lease_list(self): + LOGGER.info('Running lease list command') + try: + result = util.run_command('dhcp-lease-list') + return result[0] + except Exception as e: # pylint: disable=W0718 + LOGGER.error('Error lease list: ' + str(e)) + + def _write_config(self, config): + with open(DHCP_CONFIG_FILE, 'w', encoding='UTF-8') as f: + f.write(config) 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 new file mode 100644 index 000000000..bf2b98803 --- /dev/null +++ b/modules/network/dhcp-1/python/src/grpc_server/network_service.py @@ -0,0 +1,157 @@ +# 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 Network Service for the DHCP Server network module""" +import proto.grpc_pb2_grpc as pb2_grpc +import proto.grpc_pb2 as pb2 + +from dhcp_config import DHCPConfig +from dhcp_leases import DHCPLeases + +import traceback +from common import logger + +LOG_NAME = 'network_service' +LOGGER = None + +class NetworkService(pb2_grpc.NetworkModule): + """gRPC endpoints for the DHCP Server""" + + def __init__(self): + self._dhcp_config = None + self.dhcp_leases = DHCPLeases() + global LOGGER + LOGGER = logger.get_logger(LOG_NAME, 'dhcp-1') + + def _get_dhcp_config(self): + if self._dhcp_config is None: + self._dhcp_config = DHCPConfig() + self._dhcp_config.resolve_config() + return self._dhcp_config + + def AddReservedLease(self, request, context): # pylint: disable=W0613 + LOGGER.info('Add reserved lease called') + try: + dhcp_config = self._get_dhcp_config() + dhcp_config.add_reserved_host(request.hostname, request.hw_addr, + request.ip_addr) + dhcp_config.write_config() + LOGGER.info('Reserved lease added') + return pb2.Response(code=200, message='{}') + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to add reserved lease: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.Response(code=500, message=fail_message) + + def DeleteReservedLease(self, request, context): # pylint: disable=W0613 + LOGGER.info('Delete reserved lease called') + try: + dhcp_config = self._get_dhcp_config() + dhcp_config.delete_reserved_host(request.hw_addr) + dhcp_config.write_config() + LOGGER.info('Reserved lease deleted') + return pb2.Response(code=200, message='{}') + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to delete reserved lease: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.Response(code=500, message=fail_message) + + def DisableFailover(self, request, contest): # pylint: disable=W0613 + LOGGER.info('Disable failover called') + try: + dhcp_config = self._get_dhcp_config() + dhcp_config.disable_failover() + dhcp_config.write_config() + LOGGER.info('Failover disabled') + return pb2.Response(code=200, message='{}') + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to disable failover: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.Response(code=500, message=fail_message) + + def EnableFailover(self, request, contest): # pylint: disable=W0613 + LOGGER.info('Enable failover called') + try: + dhcp_config = self._get_dhcp_config() + dhcp_config.enable_failover() + dhcp_config.write_config() + LOGGER.info('Failover enabled') + return pb2.Response(code=200, message='{}') + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to enable failover: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.Response(code=500, message=fail_message) + + def GetDHCPRange(self, request, context): # pylint: disable=W0613 + """ + Resolve the current DHCP configuration and return + the first range from the first subnet in the file + """ + LOGGER.info('Get DHCP range called') + try: + pool = self._get_dhcp_config()._subnets[0].pools[0] + return pb2.DHCPRange(code=200, start=pool.range_start, end=pool.range_end) + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to get DHCP range: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.Response(code=500, message=fail_message) + + def GetLease(self, request, context): # pylint: disable=W0613 + """ + Resolve the current DHCP leased address for the + provided MAC address + """ + LOGGER.info('Get lease called') + try: + lease = self.dhcp_leases.get_lease(request.hw_addr) + if lease is not None: + return pb2.Response(code=200, message=str(lease)) + else: + return pb2.Response(code=200, message='{}') + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to get lease: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.Response(code=500, message=fail_message) + + def SetDHCPRange(self, request, context): # pylint: disable=W0613 + """ + Change DHCP configuration and set the + the first range from the first subnet in the configuration + """ + LOGGER.info('Set DHCP range called') + try: + dhcp_config = self._get_dhcp_config() + dhcp_config.set_range(request.start, request.end, 0, 0) + dhcp_config.write_config() + LOGGER.info('DHCP range set') + return pb2.Response(code=200, message='DHCP Range Set') + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to set DHCP range: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.Response(code=500, message=fail_message) + + def GetStatus(self, request, context): # pylint: disable=W0613 + """ + Return the current status of the network module + """ + # ToDo: Figure out how to resolve the current DHCP status + dhcp_status = True + message = str({'dhcpStatus': dhcp_status}) + return pb2.Response(code=200, message=message) diff --git a/modules/network/dhcp-1/python/src/grpc_server/proto/grpc.proto b/modules/network/dhcp-1/python/src/grpc_server/proto/grpc.proto new file mode 100644 index 000000000..d9f56213e --- /dev/null +++ b/modules/network/dhcp-1/python/src/grpc_server/proto/grpc.proto @@ -0,0 +1,59 @@ +syntax = "proto3"; + +service NetworkModule { + + rpc AddReservedLease(AddReservedLeaseRequest) returns (Response) {}; + + rpc DeleteReservedLease(DeleteReservedLeaseRequest) returns (Response) {}; + + rpc DisableFailover(DisableFailoverRequest) returns (Response) {}; + + rpc EnableFailover(EnableFailoverRequest) returns (Response) {}; + + rpc GetDHCPRange(GetDHCPRangeRequest) returns (DHCPRange) {}; + + rpc GetLease(GetLeaseRequest) returns (Response) {}; + + rpc GetStatus(GetStatusRequest) returns (Response) {}; + + rpc SetDHCPRange(SetDHCPRangeRequest) returns (Response) {}; +} + +message AddReservedLeaseRequest { + string hostname = 1; + string hw_addr = 2; + string ip_addr = 3; +} + +message DeleteReservedLeaseRequest { + string hw_addr = 1; +} + +message DisableFailoverRequest {} + +message EnableFailoverRequest {} + +message GetDHCPRangeRequest {} + +message GetLeaseRequest { + string hw_addr = 1; +} + +message GetStatusRequest {} + +message SetDHCPRangeRequest { + int32 code = 1; + string start = 2; + string end = 3; +} + +message Response { + int32 code = 1; + string message = 2; +} + +message DHCPRange { + int32 code = 1; + string start = 2; + string end = 3; +} diff --git a/modules/network/dhcp-2/bin/start_network_service b/modules/network/dhcp-2/bin/start_network_service index 550854d49..723689278 100644 --- a/modules/network/dhcp-2/bin/start_network_service +++ b/modules/network/dhcp-2/bin/start_network_service @@ -20,7 +20,7 @@ DHCP_LOG_FILE=/runtime/network/dhcp2-dhcpd.log RA_PID_FILE=/var/run/radvd/radvd.pid RA_LOG_FILE=/runtime/network/dhcp2-radvd.log -echo "Starrting Network Service..." +echo "Starting Network Service..." #Enable IPv6 Forwarding sysctl net.ipv6.conf.all.forwarding=1 diff --git a/modules/network/dhcp-2/conf/dhcpd.conf b/modules/network/dhcp-2/conf/dhcpd.conf index e73a81441..dcc47a4fe 100644 --- a/modules/network/dhcp-2/conf/dhcpd.conf +++ b/modules/network/dhcp-2/conf/dhcpd.conf @@ -1,24 +1,25 @@ 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; + 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; - pool { - failover peer "failover-peer"; - range 10.10.10.10 10.10.10.20; - } + 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/dhcp_config.py b/modules/network/dhcp-2/python/src/grpc/dhcp_config.py deleted file mode 100644 index f6e79a2ec..000000000 --- a/modules/network/dhcp-2/python/src/grpc/dhcp_config.py +++ /dev/null @@ -1,303 +0,0 @@ -# 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. - -"""Contains all the necessary classes to maintain the -DHCP server's configuration""" -import re - -CONFIG_FILE = '/etc/dhcp/dhcpd.conf' -CONFIG_FILE_TEST = 'network/modules/dhcp-2/conf/dhcpd.conf' - -DEFAULT_LEASE_TIME_KEY = 'default-lease-time' - - -class DHCPConfig: - """Represents the DHCP Servers configuration and gives access to modify it""" - - def __init__(self): - self._default_lease_time = 300 - self.subnets = [] - self._peer = None - - def write_config(self): - conf = str(self) - print('Writing config: \n' + conf) - with open(CONFIG_FILE, 'w', encoding='UTF-8') as conf_file: - conf_file.write(conf) - - def resolve_config(self): - with open(CONFIG_FILE, 'r', encoding='UTF-8') as f: - conf = f.read() - self.resolve_subnets(conf) - self._peer = DHCPFailoverPeer(conf) - - def resolve_subnets(self, conf): - self.subnets = [] - regex = r'(subnet.*)' - subnets = re.findall(regex, conf, re.MULTILINE | re.DOTALL) - for subnet in subnets: - dhcp_subnet = DHCPSubnet(subnet) - self.subnets.append(dhcp_subnet) - - def set_range(self, start, end, subnet=0, pool=0): - print('Setting Range for pool ') - print(self.subnets[subnet].pools[pool]) - self.subnets[subnet].pools[pool].range_start = start - self.subnets[subnet].pools[pool].range_end = end - - # def resolve_settings(self, conf): - # lines = conf.split('\n') - # for line in lines: - # if DEFAULT_LEASE_TIME_KEY in line: - # self._default_lease_time = line.strip().split( - # DEFAULT_LEASE_TIME_KEY)[1].strip().split(';')[0] - - # self.peer = peer - - def __str__(self): - - config = """\r{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) - - config += '\n\n' + str(self.peer) - for subnet in self._subnets: - config += '\n\n' + str(subnet) - return str(config) - - -FAILOVER_PEER_KEY = 'failover peer' -PRIMARY_KEY = 'primary' -ADDRESS_KEY = 'address' -PORT_KEY = 'port' -PEER_ADDRESS_KEY = 'peer address' -PEER_PORT_KEY = 'peer port' -MAX_RESPONSE_DELAY_KEY = 'max-response-delay' -MAX_UNACKED_UPDATES_KEY = 'max-unacked-updates' -MCLT_KEY = 'mclt' -SPLIT_KEY = 'split' -LOAD_BALANCE_MAX_SECONDS_KEY = 'load balance max seconds' - - -class DHCPFailoverPeer: - """Contains all information to define the DHCP failover peer""" - - def __init__(self, config): - self.name = None - self.primary = False - self.address = None - self.port = None - self.peer_address = None - self.peer_port = None - self.max_response_delay = None - self.max_unacked_updates = None - self.mclt = None - self.split = None - self.load_balance_max_seconds = None - self.peer = None - - self.resolve_peer(config) - - def __str__(self): - config = '{FAILOVER_PEER_KEY} \"{FAILOVER_PEER}\" {{\n' - config += '\tprimary;' if self.primary else 'secondary;' - config += """\n\t{ADDRESS_KEY} {ADDRESS}; - {PORT_KEY} {PORT}; - {PEER_ADDRESS_KEY} {PEER_ADDRESS}; - {PEER_PORT_KEY} {PEER_PORT}; - {MAX_RESPONSE_DELAY_KEY} {MAX_RESPONSE_DELAY}; - {MAX_UNACKED_UPDATES_KEY} {MAX_UNACKED_UPDATES}; - {MCLT_KEY} {MCLT}; - {SPLIT_KEY} {SPLIT}; - {LOAD_BALANCE_MAX_SECONDS_KEY} {LOAD_BALANCE_MAX_SECONDS}; - \r}}""" - - return config.format( - length='multi-line', - FAILOVER_PEER_KEY=FAILOVER_PEER_KEY, - FAILOVER_PEER=self.name, - ADDRESS_KEY=ADDRESS_KEY, - ADDRESS=self.address, - PORT_KEY=PORT_KEY, - PORT=self.port, - PEER_ADDRESS_KEY=PEER_ADDRESS_KEY, - PEER_ADDRESS=self.peer_address, - PEER_PORT_KEY=PEER_PORT_KEY, - PEER_PORT=self.peer_port, - MAX_RESPONSE_DELAY_KEY=MAX_RESPONSE_DELAY_KEY, - MAX_RESPONSE_DELAY=self.max_response_delay, - MAX_UNACKED_UPDATES_KEY=MAX_UNACKED_UPDATES_KEY, - MAX_UNACKED_UPDATES=self.max_unacked_updates, - MCLT_KEY=MCLT_KEY, - MCLT=self.mclt, - SPLIT_KEY=SPLIT_KEY, - SPLIT=self.split, - LOAD_BALANCE_MAX_SECONDS_KEY=LOAD_BALANCE_MAX_SECONDS_KEY, - LOAD_BALANCE_MAX_SECONDS=self.load_balance_max_seconds) - - def resolve_peer(self, conf): - peer = '' - lines = conf.split('\n') - for line in lines: - if line.startswith(FAILOVER_PEER_KEY) or len(peer) > 0: - if len(peer) <= 0: - self.name = line.strip().split(FAILOVER_PEER_KEY)[1].strip().split( - '{')[0].split('\"')[1] - peer += line + '\n' - if PRIMARY_KEY in line: - self.primary = True - elif ADDRESS_KEY in line and PEER_ADDRESS_KEY not in line: - self.address = line.strip().split(ADDRESS_KEY)[1].strip().split( - ';')[0] - elif PORT_KEY in line and PEER_PORT_KEY not in line: - self.port = line.strip().split(PORT_KEY)[1].strip().split(';')[0] - elif PEER_ADDRESS_KEY in line: - self.peer_address = line.strip().split( - PEER_ADDRESS_KEY)[1].strip().split(';')[0] - elif PEER_PORT_KEY in line: - self.peer_port = line.strip().split(PEER_PORT_KEY)[1].strip().split( - ';')[0] - elif MAX_RESPONSE_DELAY_KEY in line: - self.max_response_delay = line.strip().split( - MAX_RESPONSE_DELAY_KEY)[1].strip().split(';')[0] - elif MAX_UNACKED_UPDATES_KEY in line: - self.max_unacked_updates = line.strip().split( - MAX_UNACKED_UPDATES_KEY)[1].strip().split(';')[0] - elif MCLT_KEY in line: - self.mclt = line.strip().split(MCLT_KEY)[1].strip().split(';')[0] - elif SPLIT_KEY in line: - self.split = line.strip().split(SPLIT_KEY)[1].strip().split(';')[0] - elif LOAD_BALANCE_MAX_SECONDS_KEY in line: - self.load_balance_max_seconds = line.strip().split( - LOAD_BALANCE_MAX_SECONDS_KEY)[1].strip().split(';')[0] - if line.endswith('}') and len(peer) > 0: - break - self.peer = peer - - -NTP_OPTION_KEY = 'option ntp-servers' -SUBNET_MASK_OPTION_KEY = 'option subnet-mask' -BROADCAST_OPTION_KEY = 'option broadcast-address' -ROUTER_OPTION_KEY = 'option routers' -DNS_OPTION_KEY = 'option domain-name-servers' - - -class DHCPSubnet: - """Represents the DHCP Servers subnet configuration""" - - def __init__(self, subnet): - self._ntp_servers = None - self._subnet_mask = None - self._broadcast = None - self._routers = None - self._dns_servers = None - self.pools = [] - - self.resolve_subnet(subnet) - self.resolve_pools(subnet) - - def __str__(self): - config = """subnet 10.10.10.0 netmask {SUBNET_MASK_OPTION} {{ - \r\t{NTP_OPTION_KEY} {NTP_OPTION}; - \r\t{SUBNET_MASK_OPTION_KEY} {SUBNET_MASK_OPTION}; - \r\t{BROADCAST_OPTION_KEY} {BROADCAST_OPTION}; - \r\t{ROUTER_OPTION_KEY} {ROUTER_OPTION}; - \r\t{DNS_OPTION_KEY} {DNS_OPTION};""" - - config = config.format(length='multi-line', - NTP_OPTION_KEY=NTP_OPTION_KEY, - NTP_OPTION=self._ntp_servers, - SUBNET_MASK_OPTION_KEY=SUBNET_MASK_OPTION_KEY, - SUBNET_MASK_OPTION=self._subnet_mask, - BROADCAST_OPTION_KEY=BROADCAST_OPTION_KEY, - BROADCAST_OPTION=self._broadcast, - ROUTER_OPTION_KEY=ROUTER_OPTION_KEY, - ROUTER_OPTION=self._routers, - DNS_OPTION_KEY=DNS_OPTION_KEY, - DNS_OPTION=self._dns_servers) - for pool in self.pools: - config += '\n\t' + str(pool) - - config += '\n\r}' - return config - - def resolve_subnet(self, subnet): - subnet_parts = subnet.split('\n') - for part in subnet_parts: - if NTP_OPTION_KEY in part: - self._ntp_servers = part.strip().split(NTP_OPTION_KEY)[1].strip().split( - ';')[0] - elif SUBNET_MASK_OPTION_KEY in part: - self._subnet_mask = part.strip().split( - SUBNET_MASK_OPTION_KEY)[1].strip().split(';')[0] - elif BROADCAST_OPTION_KEY in part: - self._broadcast = part.strip().split( - BROADCAST_OPTION_KEY)[1].strip().split(';')[0] - elif ROUTER_OPTION_KEY in part: - self._routers = part.strip().split(ROUTER_OPTION_KEY)[1].strip().split( - ';')[0] - elif DNS_OPTION_KEY in part: - self._dns_servers = part.strip().split(DNS_OPTION_KEY)[1].strip().split( - ';')[0] - - def resolve_pools(self, subnet): - regex = r'(pool.*)\}' - pools = re.findall(regex, subnet, re.MULTILINE | re.DOTALL) - for pool in pools: - dhcp_pool = DHCPPool(pool) - self.pools.append(dhcp_pool) - - -FAILOVER_KEY = 'failover peer' -RANGE_KEY = 'range' - - -class DHCPPool: - """Represents a DHCP Servers subnet pool configuration""" - - def __init__(self, pool): - self.failover_peer = None - self.range_start = None - self.range_end = None - self.resolve_pool(pool) - - def __str__(self): - - config = """pool {{ - \r\t\t{FAILOVER_KEY} "{FAILOVER}"; - \r\t\t{RANGE_KEY} {RANGE_START} {RANGE_END}; - \r\t}}""" - - return config.format( - length='multi-line', - FAILOVER_KEY=FAILOVER_KEY, - FAILOVER=self.failover_peer, - RANGE_KEY=RANGE_KEY, - RANGE_START=self.range_start, - RANGE_END=self.range_end, - ) - - def resolve_pool(self, pool): - pool_parts = pool.split('\n') - # pool_parts = pool.split("\n") - for part in pool_parts: - if FAILOVER_KEY in part: - self.failover_peer = part.strip().split(FAILOVER_KEY)[1].strip().split( - ';')[0].replace('\"', '') - if RANGE_KEY in part: - pool_range = part.strip().split(RANGE_KEY)[1].strip().split(';')[0] - self.range_start = pool_range.split(' ')[0].strip() - self.range_end = pool_range.split(' ')[1].strip() diff --git a/modules/network/dhcp-2/python/src/grpc/network_service.py b/modules/network/dhcp-2/python/src/grpc/network_service.py deleted file mode 100644 index 64aab8a07..000000000 --- a/modules/network/dhcp-2/python/src/grpc/network_service.py +++ /dev/null @@ -1,58 +0,0 @@ -# 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 Network Service for the DHCP Server network module""" -import proto.grpc_pb2_grpc as pb2_grpc -import proto.grpc_pb2 as pb2 - -from dhcp_config import DHCPConfig - - -class NetworkService(pb2_grpc.NetworkModule): - """gRPC endpoints for the DHCP Server""" - - def __init__(self): - self._dhcp_config = DHCPConfig() - - def GetDHCPRange(self, request, context): # pylint: disable=W0613 - """ - Resolve the current DHCP configuration and return - the first range from the first subnet in the file - """ - self._dhcp_config.resolve_config() - pool = self._dhcp_config.subnets[0].pools[0] - return pb2.DHCPRange(code=200, start=pool.range_start, end=pool.range_end) - - def SetDHCPRange(self, request, context): # pylint: disable=W0613 - """ - Change DHCP configuration and set the - the first range from the first subnet in the configuration - """ - - print('Setting DHCPRange') - print('Start: ' + request.start) - print('End: ' + request.end) - self._dhcp_config.resolve_config() - self._dhcp_config.set_range(request.start, request.end, 0, 0) - self._dhcp_config.write_config() - return pb2.Response(code=200, message='DHCP Range Set') - - def GetStatus(self, request, context): # pylint: disable=W0613 - """ - Return the current status of the network module - """ - # ToDo: Figure out how to resolve the current DHCP status - dhcp_status = True - message = str({'dhcpStatus': dhcp_status}) - return pb2.Response(code=200, message=message) diff --git a/modules/network/dhcp-2/python/src/grpc/proto/grpc.proto b/modules/network/dhcp-2/python/src/grpc/proto/grpc.proto deleted file mode 100644 index 8e2732620..000000000 --- a/modules/network/dhcp-2/python/src/grpc/proto/grpc.proto +++ /dev/null @@ -1,36 +0,0 @@ -syntax = "proto3"; - -service NetworkModule { - - rpc GetDHCPRange(GetDHCPRangeRequest) returns (DHCPRange) {}; - - rpc SetDHCPRange(DHCPRange) returns (Response) {}; - - rpc GetStatus(GetStatusRequest) returns (Response) {}; - - rpc GetIPAddress(GetIPAddressRequest) returns (Response) {}; - - rpc SetLeaseAddress(SetLeaseAddressRequest) returns (Response) {}; - -} - -message Response { - int32 code = 1; - string message = 2; -} - -message DHCPRange { - int32 code = 1; - string start = 2; - string end = 3; -} - -message GetDHCPRangeRequest {} - -message GetIPAddressRequest {} - -message GetStatusRequest {} - -message SetLeaseAddressRequest { - string ipAddress = 1; -} \ No newline at end of file diff --git a/modules/network/dhcp-2/python/src/grpc/__init__.py b/modules/network/dhcp-2/python/src/grpc_server/__init__.py similarity index 100% rename from modules/network/dhcp-2/python/src/grpc/__init__.py rename to modules/network/dhcp-2/python/src/grpc_server/__init__.py 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 new file mode 100644 index 000000000..444faa87c --- /dev/null +++ b/modules/network/dhcp-2/python/src/grpc_server/dhcp_config.py @@ -0,0 +1,493 @@ +# 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. +"""Contains all the necessary classes to maintain the +DHCP server's configuration""" +import re +from common import logger + +LOG_NAME = 'dhcp_config' +LOGGER = None + +CONFIG_FILE = '/etc/dhcp/dhcpd.conf' + +DEFAULT_LEASE_TIME_KEY = 'default-lease-time' + + +class DHCPConfig: + """Represents the DHCP Servers configuration and gives access to modify it""" + + def __init__(self): + self._default_lease_time = 300 + self._subnets = [] + self._peer = None + self._reserved_hosts = [] + global LOGGER + LOGGER = logger.get_logger(LOG_NAME, 'dhcp-1') + + def add_reserved_host(self, hostname, hw_addr, ip_addr): + host = DHCPReservedHost(hostname=hostname, + hw_addr=hw_addr, + fixed_addr=ip_addr) + self._reserved_hosts.append(host) + + def delete_reserved_host(self, hw_addr): + for host in self._reserved_hosts: + if hw_addr == host.hw_addr: + self._reserved_hosts.remove(host) + + def disable_failover(self): + self._peer.disable() + for subnet in self._subnets: + subnet.disable_peer() + + def enable_failover(self): + self._peer.enable() + for subnet in self._subnets: + subnet.enable_peer() + + def get_reserved_host(self, hw_addr): + for host in self._reserved_hosts: + if hw_addr == host.hw_addr: + return host + + def write_config(self, config=None): + if config is None: + conf = str(self) + with open(CONFIG_FILE, 'w', encoding='UTF-8') as conf_file: + conf_file.write(conf) + else: + with open(CONFIG_FILE, 'w', encoding='UTF-8') as conf_file: + conf_file.write(config) + + def _get_config(self, config_file=CONFIG_FILE): + content = None + with open(config_file, 'r', encoding='UTF-8') as f: + content = f.read() + return content + + def make(self, conf): + try: + self._subnets = self.resolve_subnets(conf) + self._peer = DHCPFailoverPeer(conf) + self._reserved_hosts = self.resolve_reserved_hosts(conf) + except Exception as e: # pylint: disable=W0718 + print('Failed to make DHCPConfig: ' + str(e)) + + def resolve_config(self, config_file=CONFIG_FILE): + try: + conf = self._get_config(config_file) + self._subnets = self.resolve_subnets(conf) + self._peer = DHCPFailoverPeer(conf) + self._reserved_hosts = self.resolve_reserved_hosts(conf) + except Exception as e: # pylint: disable=W0718 + print('Failed to resolve config: ' + str(e)) + + def resolve_subnets(self, conf): + subnets = [] + regex = r'(subnet.*)' + subnets_conf = re.findall(regex, conf, re.MULTILINE | re.DOTALL) + for subnet in subnets_conf: + dhcp_subnet = DHCPSubnet(subnet) + subnets.append(dhcp_subnet) + return subnets + + def resolve_reserved_hosts(self, conf): + hosts = [] + host_start = 0 + while True: + host_start = conf.find('host', host_start) + if host_start < 0: + break + else: + host_end = conf.find('}', host_start) + host = DHCPReservedHost(config=conf[host_start:host_end + 1]) + hosts.append(host) + host_start = host_end + 1 + return hosts + + def set_range(self, start, end, subnet=0, pool=0): + # Calculate the subnet from the range + octets = start.split('.') + octets[-1] = '0' + dhcp_subnet = '.'.join(octets) + + #Update the subnet and range + self._subnets[subnet].set_subnet(dhcp_subnet) + self._subnets[subnet].pools[pool].set_range(start, end) + + def __str__(self): + + # Encode the top level config options + 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) + + # Encode the failover peer + config += '\n\n' + str(self._peer) + + # Encode the subnets + for subnet in self._subnets: + config += '\n\n' + str(subnet) + + # Encode the reserved hosts + for host in self._reserved_hosts: + config += '\n' + str(host) + + return str(config) + + +FAILOVER_PEER_KEY = 'failover peer' +PRIMARY_KEY = 'primary' +ADDRESS_KEY = 'address' +PORT_KEY = 'port' +PEER_ADDRESS_KEY = 'peer address' +PEER_PORT_KEY = 'peer port' +MAX_RESPONSE_DELAY_KEY = 'max-response-delay' +MAX_UNACKED_UPDATES_KEY = 'max-unacked-updates' +MCLT_KEY = 'mclt' +SPLIT_KEY = 'split' +LOAD_BALANCE_MAX_SECONDS_KEY = 'load balance max seconds' + + +class DHCPFailoverPeer: + """Contains all information to define the DHCP failover peer""" + + def __init__(self, config): + self.name = None + self.primary = False + self.address = None + self.port = None + self.peer_address = None + self.peer_port = None + self.max_response_delay = None + self.max_unacked_updates = None + self.mclt = None + self.split = None + self.load_balance_max_seconds = None + self.peer = None + self.enabled = True + + self.resolve_peer(config) + + def __str__(self): + config = '{FAILOVER_PEER_KEY} \"{FAILOVER_PEER}\" {{\n' + config += '\tprimary;' if self.primary else 'secondary;' + config += '\n\t{ADDRESS_KEY} {ADDRESS};' if self.address is not None else '' + config += '\n\t{PORT_KEY} {PORT};' if self.port is not None else '' + config += '\n\t{PEER_ADDRESS_KEY} {PEER_ADDRESS};' if self.peer_address is not None else '' + config += '\n\t{PEER_PORT_KEY} {PEER_PORT};' if self.peer_port is not None else '' + config += '\n\t{MAX_RESPONSE_DELAY_KEY} {MAX_RESPONSE_DELAY};' if self.max_response_delay is not None else '' + config += '\n\t{MAX_UNACKED_UPDATES_KEY} {MAX_UNACKED_UPDATES};' if self.max_unacked_updates is not None else '' + config += '\n\t{MCLT_KEY} {MCLT};' if self.mclt is not None else '' + config += '\n\t{SPLIT_KEY} {SPLIT};' if self.split is not None else '' + config += '\n\t{LOAD_BALANCE_MAX_SECONDS_KEY} {LOAD_BALANCE_MAX_SECONDS};' if self.load_balance_max_seconds is not None else '' + config += '\n\r}}' + + config = config.format( + length='multi-line', + FAILOVER_PEER_KEY=FAILOVER_PEER_KEY, + FAILOVER_PEER=self.name, + ADDRESS_KEY=ADDRESS_KEY, + ADDRESS=self.address, + PORT_KEY=PORT_KEY, + PORT=self.port, + PEER_ADDRESS_KEY=PEER_ADDRESS_KEY, + PEER_ADDRESS=self.peer_address, + PEER_PORT_KEY=PEER_PORT_KEY, + PEER_PORT=self.peer_port, + MAX_RESPONSE_DELAY_KEY=MAX_RESPONSE_DELAY_KEY, + MAX_RESPONSE_DELAY=self.max_response_delay, + MAX_UNACKED_UPDATES_KEY=MAX_UNACKED_UPDATES_KEY, + MAX_UNACKED_UPDATES=self.max_unacked_updates, + MCLT_KEY=MCLT_KEY, + MCLT=self.mclt, + SPLIT_KEY=SPLIT_KEY, + SPLIT=self.split, + LOAD_BALANCE_MAX_SECONDS_KEY=LOAD_BALANCE_MAX_SECONDS_KEY, + LOAD_BALANCE_MAX_SECONDS=self.load_balance_max_seconds) + + if not self.enabled: + lines = config.strip().split('\n') + for i in range(len(lines)-1): + lines[i] = '#' + lines[i] + lines[-1] = '#' + lines[-1].strip() # Handle the last line separately + config = '\n'.join(lines) + + return config + + def disable(self): + self.enabled = False + + def enable(self): + self.enabled = True + + def resolve_peer(self, conf): + peer = '' + lines = conf.split('\n') + for line in lines: + if line.startswith(FAILOVER_PEER_KEY) or len(peer) > 0: + if len(peer) <= 0: + self.name = line.strip().split(FAILOVER_PEER_KEY)[1].strip().split( + '{')[0].split('\"')[1] + peer += line + '\n' + if PRIMARY_KEY in line: + self.primary = True + elif ADDRESS_KEY in line and PEER_ADDRESS_KEY not in line: + self.address = line.strip().split(ADDRESS_KEY)[1].strip().split( + ';')[0] + elif PORT_KEY in line and PEER_PORT_KEY not in line: + self.port = line.strip().split(PORT_KEY)[1].strip().split(';')[0] + elif PEER_ADDRESS_KEY in line: + self.peer_address = line.strip().split( + PEER_ADDRESS_KEY)[1].strip().split(';')[0] + elif PEER_PORT_KEY in line: + self.peer_port = line.strip().split(PEER_PORT_KEY)[1].strip().split( + ';')[0] + elif MAX_RESPONSE_DELAY_KEY in line: + self.max_response_delay = line.strip().split( + MAX_RESPONSE_DELAY_KEY)[1].strip().split(';')[0] + elif MAX_UNACKED_UPDATES_KEY in line: + self.max_unacked_updates = line.strip().split( + MAX_UNACKED_UPDATES_KEY)[1].strip().split(';')[0] + elif MCLT_KEY in line: + self.mclt = line.strip().split(MCLT_KEY)[1].strip().split(';')[0] + elif SPLIT_KEY in line: + self.split = line.strip().split(SPLIT_KEY)[1].strip().split(';')[0] + elif LOAD_BALANCE_MAX_SECONDS_KEY in line: + self.load_balance_max_seconds = line.strip().split( + LOAD_BALANCE_MAX_SECONDS_KEY)[1].strip().split(';')[0] + if line.endswith('}') and len(peer) > 0: + break + self.peer = peer + + +SUBNET_KEY = 'subnet' +NTP_OPTION_KEY = 'option ntp-servers' +SUBNET_MASK_OPTION_KEY = 'option subnet-mask' +BROADCAST_OPTION_KEY = 'option broadcast-address' +ROUTER_OPTION_KEY = 'option routers' +DNS_OPTION_KEY = 'option domain-name-servers' +INTERFACE_KEY = 'interface' +AUTHORITATIVE_KEY = 'authoritative' + + +class DHCPSubnet: + """Represents the DHCP Servers subnet configuration""" + + def __init__(self, subnet): + self._authoritative = False + self._subnet = None + self._ntp_servers = None + self._subnet_mask = None + self._broadcast = None + self._routers = None + self._dns_servers = None + self._interface = None + self.pools = [] + + self.resolve_subnet(subnet) + self.resolve_pools(subnet) + + def __str__(self): + config = 'subnet {SUBNET_OPTION} netmask {SUBNET_MASK_OPTION} {{' + config += '\n\t{NTP_OPTION_KEY} {NTP_OPTION};' if self._ntp_servers is not None else '' + config += '\n\t{SUBNET_MASK_OPTION_KEY} {SUBNET_MASK_OPTION};' if self._subnet_mask is not None else '' + config += '\n\t{BROADCAST_OPTION_KEY} {BROADCAST_OPTION};' if self._broadcast is not None else '' + config += '\n\t{ROUTER_OPTION_KEY} {ROUTER_OPTION};' if self._routers is not None else '' + config += '\n\t{DNS_OPTION_KEY} {DNS_OPTION};' if self._dns_servers is not None else '' + config += '\n\t{INTERFACE_KEY} {INTERFACE_OPTION};' if self._interface is not None else '' + config += '\n\t{AUTHORITATIVE_KEY};' if self._authoritative else '' + + + config = config.format(length='multi-line', + SUBNET_OPTION=self._subnet, + NTP_OPTION_KEY=NTP_OPTION_KEY, + NTP_OPTION=self._ntp_servers, + SUBNET_MASK_OPTION_KEY=SUBNET_MASK_OPTION_KEY, + SUBNET_MASK_OPTION=self._subnet_mask, + BROADCAST_OPTION_KEY=BROADCAST_OPTION_KEY, + BROADCAST_OPTION=self._broadcast, + ROUTER_OPTION_KEY=ROUTER_OPTION_KEY, + ROUTER_OPTION=self._routers, + DNS_OPTION_KEY=DNS_OPTION_KEY, + DNS_OPTION=self._dns_servers, + INTERFACE_KEY=INTERFACE_KEY, + INTERFACE_OPTION=self._interface, + AUTHORITATIVE_KEY=AUTHORITATIVE_KEY) + + # if not self._authoritative: + # config = config.replace(AUTHORITATIVE_KEY, '#' + AUTHORITATIVE_KEY) + + for pool in self.pools: + config += '\n\t' + str(pool) + + config += '\n}' + return config + + def disable_peer(self): + for pool in self.pools: + pool.disable_peer() + + def enable_peer(self): + for pool in self.pools: + pool.enable_peer() + + def set_subnet(self, subnet, netmask=None): + if netmask is None: + netmask = '255.255.255.0' + self._subnet = subnet + self._subnet_mask = netmask + + # Calculate the broadcast from the subnet + octets = subnet.split('.') + octets[-1] = '255' + dhcp_broadcast = '.'.join(octets) + + self._broadcast = dhcp_broadcast + + def resolve_subnet(self, subnet): + subnet_parts = subnet.split('\n') + for part in subnet_parts: + if part.strip().startswith(SUBNET_KEY): + self._subnet = part.strip().split()[1] + elif NTP_OPTION_KEY in part: + self._ntp_servers = part.strip().split(NTP_OPTION_KEY)[1].strip().split( + ';')[0] + elif SUBNET_MASK_OPTION_KEY in part: + self._subnet_mask = part.strip().split( + SUBNET_MASK_OPTION_KEY)[1].strip().split(';')[0] + elif BROADCAST_OPTION_KEY in part: + self._broadcast = part.strip().split( + BROADCAST_OPTION_KEY)[1].strip().split(';')[0] + elif ROUTER_OPTION_KEY in part: + self._routers = part.strip().split(ROUTER_OPTION_KEY)[1].strip().split( + ';')[0] + elif DNS_OPTION_KEY in part: + self._dns_servers = part.strip().split(DNS_OPTION_KEY)[1].strip().split( + ';')[0] + elif INTERFACE_KEY in part: + self._interface = part.strip().split(INTERFACE_KEY)[1].strip().split( + ';')[0] + elif AUTHORITATIVE_KEY in part: + self._authoritative = True + + def resolve_pools(self, subnet): + regex = r'(pool.*)\}' + pools = re.findall(regex, subnet, re.MULTILINE | re.DOTALL) + for pool in pools: + dhcp_pool = DHCPPool(pool) + self.pools.append(dhcp_pool) + + +FAILOVER_KEY = 'failover peer' +RANGE_KEY = 'range' + + +class DHCPPool: + """Represents a DHCP Servers subnet pool configuration""" + + def __init__(self, pool): + self.failover_peer = None + self.range_start = None + self.range_end = None + self.resolve_pool(pool) + self._peer_enabled = True + + def __str__(self): + config = 'pool {{' + config += '\n\t\t{FAILOVER_KEY} "{FAILOVER}";' if self.failover_peer is not None else '' + config += '\n\t\t{RANGE_KEY} {RANGE_START} {RANGE_END};' if self.range_start is not None and self.range_end is not None else '' + config += '\n\t}}' + + config = config.format( + length='multi-line', + FAILOVER_KEY=FAILOVER_KEY, + FAILOVER=self.failover_peer, + RANGE_KEY=RANGE_KEY, + RANGE_START=self.range_start, + RANGE_END=self.range_end, + ) + + if not self._peer_enabled: + config = config.replace(FAILOVER_KEY, '#' + FAILOVER_KEY) + + return config + + def disable_peer(self): + self._peer_enabled = False + + def enable_peer(self): + self._peer_enabled = True + + def set_range(self, start, end): + self.range_start = start + self.range_end = end + + def resolve_pool(self, pool): + pool_parts = pool.split('\n') + for part in pool_parts: + if FAILOVER_KEY in part: + self.failover_peer = part.strip().split(FAILOVER_KEY)[1].strip().split( + ';')[0].replace('\"', '') + if RANGE_KEY in part: + pool_range = part.strip().split(RANGE_KEY)[1].strip().split(';')[0] + self.range_start = pool_range.split(' ')[0].strip() + self.range_end = pool_range.split(' ')[1].strip() + + +HOST_KEY = 'host' +HARDWARE_KEY = 'hardware ethernet' +FIXED_ADDRESS_KEY = 'fixed-address' + + +class DHCPReservedHost: + """Represents a DHCP Servers subnet pool configuration""" + + def __init__(self, hostname=None, hw_addr=None, fixed_addr=None, config=None): + if config is None: + self.host = hostname + self.hw_addr = hw_addr + self.fixed_addr = fixed_addr + else: + self.resolve_host(config) + + def __str__(self): + + config = """{HOST_KEY} {HOSTNAME} {{ + \r\t{HARDWARE_KEY} {HW_ADDR}; + \r\t{FIXED_ADDRESS_KEY} {RESERVED_IP}; + \r}}""" + + config = config.format( + length='multi-line', + HOST_KEY=HOST_KEY, + HOSTNAME=self.host, + HARDWARE_KEY=HARDWARE_KEY, + HW_ADDR=self.hw_addr, + FIXED_ADDRESS_KEY=FIXED_ADDRESS_KEY, + RESERVED_IP=self.fixed_addr, + ) + return config + + def resolve_host(self, reserved_host): + host_parts = reserved_host.split('\n') + for part in host_parts: + if HOST_KEY in part: + self.host = part.strip().split(HOST_KEY)[1].strip().split('{')[0] + elif HARDWARE_KEY in part: + self.hw_addr = part.strip().split(HARDWARE_KEY)[1].strip().split(';')[0] + elif FIXED_ADDRESS_KEY in part: + self.fixed_addr = part.strip().split( + FIXED_ADDRESS_KEY)[1].strip().split(';')[0] 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 new file mode 100644 index 000000000..2cc78403a --- /dev/null +++ b/modules/network/dhcp-2/python/src/grpc_server/dhcp_config_test.py @@ -0,0 +1,103 @@ +# 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. +"""Unit Testing for the DHCP Server config""" +import unittest +from dhcp_config import DHCPConfig +import os + +CONFIG_FILE = 'conf/dhcpd.conf' + +DHCP_CONFIG = None + +def get_config_file_path(): + dhcp_config = DHCPConfig() + current_dir = os.path.dirname(os.path.abspath(__file__)) + module_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(current_dir)))) + 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 + def setUpClass(cls): + # Resolve the config + global DHCP_CONFIG + DHCP_CONFIG = get_config() + + def test_resolve_config(self): + print('Test Resolve Config:\n' + str(DHCP_CONFIG)) + + # Resolve the raw config file + with open(get_config_file_path(),'r') as f: + lines = f.readlines() + + # Get the resolved config as a + conf_parts = str(DHCP_CONFIG).split('\n') + + # dhcpd conf is not picky about spacing so we just + # need to check contents of each line for matching + # to make sure evertying matches + for i in range(len(lines)): + self.assertEqual(lines[i].strip(),conf_parts[i].strip()) + + def test_disable_failover(self): + DHCP_CONFIG.disable_failover() + print('Test Disable Config:\n' + str(DHCP_CONFIG)) + config_lines = str(DHCP_CONFIG._peer).split('\n') + for line in config_lines: + self.assertTrue(line.startswith('#')) + + def test_enable_failover(self): + DHCP_CONFIG.enable_failover() + print('Test Enable Config:\n' + str(DHCP_CONFIG)) + config_lines = str(DHCP_CONFIG._peer).split('\n') + for line in config_lines: + self.assertFalse(line.startswith('#')) + + def test_add_reserved_host(self): + DHCP_CONFIG.add_reserved_host('test','00:11:22:33:44:55','192.168.10.5') + host = DHCP_CONFIG.get_reserved_host('00:11:22:33:44:55') + self.assertIsNotNone(host) + print('AddHostConfig:\n' + str(DHCP_CONFIG)) + + def test_delete_reserved_host(self): + DHCP_CONFIG.delete_reserved_host('00:11:22:33:44:55') + host = DHCP_CONFIG.get_reserved_host('00:11:22:33:44:55') + self.assertIsNone(host) + print('DeleteHostConfig:\n' + str(DHCP_CONFIG)) + + def test_resolve_config_with_hosts(self): + DHCP_CONFIG.add_reserved_host('test','00:11:22:33:44:55','192.168.10.5') + config_with_hosts = DHCPConfig() + config_with_hosts.make(str(DHCP_CONFIG)) + host = config_with_hosts.get_reserved_host('00:11:22:33:44:55') + self.assertIsNotNone(host) + print("ResolveConfigWithHosts:\n" + str(config_with_hosts)) + +if __name__ == '__main__': + suite = unittest.TestSuite() + suite.addTest(DHCPConfigTest('test_resolve_config')) + suite.addTest(DHCPConfigTest('test_disable_failover')) + suite.addTest(DHCPConfigTest('test_enable_failover')) + suite.addTest(DHCPConfigTest('test_add_reserved_host')) + suite.addTest(DHCPConfigTest('test_delete_reserved_host')) + suite.addTest(DHCPConfigTest('test_resolve_config_with_hosts')) + + runner = unittest.TextTestRunner() + runner.run(suite) \ No newline at end of file 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 new file mode 100644 index 000000000..0d2f43e3b --- /dev/null +++ b/modules/network/dhcp-2/python/src/grpc_server/dhcp_lease.py @@ -0,0 +1,75 @@ +# 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. +"""Contains all the necessary methods to create and monitor DHCP +leases on the server""" +from datetime import datetime +import time + +time_format = '%Y-%m-%d %H:%M:%S' + + +class DHCPLease(object): + """Represents a DHCP Server lease""" + hw_addr = None + ip = None + hostname = None + expires = None + + def __init__(self, lease): + self._make_lease(lease) + + def _make_lease(self, lease): + if lease is not None: + sections_raw = lease.split(' ') + sections = [] + for section in sections_raw: + if section.strip(): + sections.append(section) + self.hw_addr = sections[0] + self.ip = sections[1] + self.hostname = sections[2] + self.expires = sections[3] + '' '' + sections[4] + self.manufacturer = ' '.join(sections[5:]) + + def get_millis(self, timestamp): + dt_obj = datetime.strptime(timestamp, time_format) + millis = dt_obj.timestamp() * 1000 + return millis + + def get_expires_millis(self): + return self.get_millis(self.expires) + + def is_expired(self): + expires_millis = self.get_expires_millis() + cur_time = int(round(time.time()) * 1000) + return cur_time >= expires_millis + + def __str__(self): + lease = {} + if self.hw_addr is not None: + lease['hw_addr'] = self.hw_addr + + if self.ip is not None: + lease['ip'] = self.ip + + if self.hostname is not None: + lease['hostname'] = self.hostname + + if self.expires is not None: + lease['expires'] = self.expires + + if self.manufacturer is not None: + lease['manufacturer'] = self.manufacturer + + return str(lease) diff --git a/modules/network/dhcp-2/python/src/grpc_server/dhcp_leases.py b/modules/network/dhcp-2/python/src/grpc_server/dhcp_leases.py new file mode 100644 index 000000000..08e6feabe --- /dev/null +++ b/modules/network/dhcp-2/python/src/grpc_server/dhcp_leases.py @@ -0,0 +1,107 @@ +# 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. +"""Used to resolve the DHCP servers lease information""" +import os +from dhcp_lease import DHCPLease +import logger +from common import util + +LOG_NAME = 'dhcp_lease' +LOGGER = None + +DHCP_LEASE_FILES = [ + '/var/lib/dhcp/dhcpd.leases', '/var/lib/dhcp/dhcpd.leases~', + '/var/lib/dhcp/dhcpd6.leases', '/var/lib/dhcp/dhcpd6.leases~' +] +DHCP_CONFIG_FILE = '/etc/dhcp/dhcpd.conf' + + +class DHCPLeases: + """Leases for the DHCP server""" + + def __init__(self): + global LOGGER + LOGGER = logger.get_logger(LOG_NAME, 'dhcp-2') + + def delete_all_hosts(self): + LOGGER.info('Deleting hosts') + for lease in DHCP_LEASE_FILES: + LOGGER.info('Checking file: ' + lease) + if os.path.exists(lease): + LOGGER.info('File Exists: ' + lease) + try: + # Delete existing lease file + os.remove(lease) + except OSError as e: + LOGGER.info(f'Error occurred while deleting the file: {e}') + # Create an empty lease file + with open(lease, 'w', encoding='UTF-8'): + pass + + def get_lease(self, hw_addr): + for lease in self.get_leases(): + if lease.hw_addr == hw_addr: + return lease + + 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:] + lines = lease_list.split('\n') + for line in lines: + try: + lease = DHCPLease(line) + leases.append(lease) + except Exception as e: # pylint: disable=W0718 + # Let non lease lines file without extra checks + LOGGER.error('Making Lease Error: ' + str(e)) + LOGGER.error('Not a valid lease line: ' + line) + return leases + + def delete_lease(self, ip_addr): + LOGGER.info('Deleting lease') + for lease in DHCP_LEASE_FILES: + LOGGER.info('Checking file: ' + lease) + if os.path.exists(lease): + LOGGER.info('File Exists: ' + lease) + try: + # Delete existing lease file + with (open(lease, 'r', encoding='UTF-8')) as f: + contents = f.read() + + while ip_addr in contents: + ix_ip = contents.find(ip_addr) + lease_start = contents.rindex('lease', 0, ix_ip) + lease_end = contents.find('}', lease_start) + LOGGER.info('Lease Location: ' + str(lease_start) + ':' + + str(lease_end)) + contents = contents[0:lease_start] + contents[lease_end + 1:] + + except OSError as e: + LOGGER.info(f'Error occurred while deleting the lease: {e}') + + def _get_lease_list(self): + LOGGER.info('Running lease list command') + try: + result = util.run_command('dhcp-lease-list') + return result[0] + except Exception as e: # pylint: disable=W0718 + LOGGER.error('Error lease list: ' + str(e)) + + def _write_config(self, config): + with open(DHCP_CONFIG_FILE, 'w', encoding='UTF-8') as f: + f.write(config) diff --git a/modules/network/dhcp-2/python/src/grpc_server/network_service.py b/modules/network/dhcp-2/python/src/grpc_server/network_service.py new file mode 100644 index 000000000..053d26d6b --- /dev/null +++ b/modules/network/dhcp-2/python/src/grpc_server/network_service.py @@ -0,0 +1,157 @@ +# 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 Network Service for the DHCP Server network module""" +import proto.grpc_pb2_grpc as pb2_grpc +import proto.grpc_pb2 as pb2 + +from dhcp_config import DHCPConfig +from dhcp_leases import DHCPLeases + +import traceback +from common import logger + +LOG_NAME = 'network_service' +LOGGER = None + +class NetworkService(pb2_grpc.NetworkModule): + """gRPC endpoints for the DHCP Server""" + + def __init__(self): + self._dhcp_config = None + self.dhcp_leases = DHCPLeases() + global LOGGER + LOGGER = logger.get_logger(LOG_NAME, 'dhcp-2') + + def _get_dhcp_config(self): + if self._dhcp_config is None: + self._dhcp_config = DHCPConfig() + self._dhcp_config.resolve_config() + return self._dhcp_config + + def AddReservedLease(self, request, context): # pylint: disable=W0613 + LOGGER.info('Add reserved lease called') + try: + dhcp_config = self._get_dhcp_config() + dhcp_config.add_reserved_host(request.hostname, request.hw_addr, + request.ip_addr) + dhcp_config.write_config() + LOGGER.info('Reserved lease added') + return pb2.Response(code=200, message='{}') + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to add reserved lease: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.Response(code=500, message=fail_message) + + def DeleteReservedLease(self, request, context): # pylint: disable=W0613 + LOGGER.info('Delete reserved lease called') + try: + dhcp_config = self._get_dhcp_config() + dhcp_config.delete_reserved_host(request.hw_addr) + dhcp_config.write_config() + LOGGER.info('Reserved lease deleted') + return pb2.Response(code=200, message='{}') + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to delete reserved lease: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.Response(code=500, message=fail_message) + + def DisableFailover(self, request, contest): # pylint: disable=W0613 + LOGGER.info('Disable failover called') + try: + dhcp_config = self._get_dhcp_config() + dhcp_config.disable_failover() + dhcp_config.write_config() + LOGGER.info('Failover disabled') + return pb2.Response(code=200, message='{}') + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to disable failover: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.Response(code=500, message=fail_message) + + def EnableFailover(self, request, contest): # pylint: disable=W0613 + LOGGER.info('Enable failover called') + try: + dhcp_config = self._get_dhcp_config() + dhcp_config.enable_failover() + dhcp_config.write_config() + LOGGER.info('Failover enabled') + return pb2.Response(code=200, message='{}') + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to enable failover: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.Response(code=500, message=fail_message) + + def GetDHCPRange(self, request, context): # pylint: disable=W0613 + """ + Resolve the current DHCP configuration and return + the first range from the first subnet in the file + """ + LOGGER.info('Get DHCP range called') + try: + pool = self._get_dhcp_config()._subnets[0].pools[0] + return pb2.DHCPRange(code=200, start=pool.range_start, end=pool.range_end) + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to get DHCP range: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.Response(code=500, message=fail_message) + + def GetLease(self, request, context): # pylint: disable=W0613 + """ + Resolve the current DHCP leased address for the + provided MAC address + """ + LOGGER.info('Get lease called') + try: + lease = self.dhcp_leases.get_lease(request.hw_addr) + if lease is not None: + return pb2.Response(code=200, message=str(lease)) + else: + return pb2.Response(code=200, message='{}') + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to get lease: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.Response(code=500, message=fail_message) + + def SetDHCPRange(self, request, context): # pylint: disable=W0613 + """ + Change DHCP configuration and set the + the first range from the first subnet in the configuration + """ + LOGGER.info('Set DHCP range called') + try: + dhcp_config = self._get_dhcp_config() + dhcp_config.set_range(request.start, request.end, 0, 0) + dhcp_config.write_config() + LOGGER.info('DHCP range set') + return pb2.Response(code=200, message='DHCP Range Set') + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to set DHCP range: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.Response(code=500, message=fail_message) + + def GetStatus(self, request, context): # pylint: disable=W0613 + """ + Return the current status of the network module + """ + # ToDo: Figure out how to resolve the current DHCP status + dhcp_status = True + message = str({'dhcpStatus': dhcp_status}) + return pb2.Response(code=200, message=message) diff --git a/modules/network/dhcp-2/python/src/grpc_server/proto/grpc.proto b/modules/network/dhcp-2/python/src/grpc_server/proto/grpc.proto new file mode 100644 index 000000000..b6a11a75b --- /dev/null +++ b/modules/network/dhcp-2/python/src/grpc_server/proto/grpc.proto @@ -0,0 +1,59 @@ +syntax = "proto3"; + +service NetworkModule { + + rpc AddReservedLease(AddReservedLeaseRequest) returns (Response) {}; + + rpc DeleteReservedLease(DeleteReservedLeaseRequest) returns (Response) {}; + + rpc DisableFailover(DisableFailoverRequest) returns (Response) {}; + + rpc EnableFailover(EnableFailoverRequest) returns (Response) {}; + + rpc GetDHCPRange(GetDHCPRangeRequest) returns (DHCPRange) {}; + + rpc GetLease(GetLeaseRequest) returns (Response) {}; + + rpc GetStatus(GetStatusRequest) returns (Response) {}; + + rpc SetDHCPRange(SetDHCPRangeRequest) returns (Response) {}; +} + +message AddReservedLeaseRequest { + string hostname = 1; + string hw_addr = 2; + string ip_addr = 3; +} + +message DeleteReservedLeaseRequest { + string hw_addr = 1; +} + +message DisableFailoverRequest {} + +message EnableFailoverRequest {} + +message GetDHCPRangeRequest {} + +message GetLeaseRequest { + string hw_addr = 1; +} + +message GetStatusRequest {} + +message SetDHCPRangeRequest { + int32 code = 1; + string start = 2; + string end = 3; +} + +message Response { + int32 code = 1; + string message = 2; +} + +message DHCPRange { + int32 code = 1; + string start = 2; + string end = 3; +} diff --git a/modules/test/base/base.Dockerfile b/modules/test/base/base.Dockerfile index 9c7f2bac2..10344cbc7 100644 --- a/modules/test/base/base.Dockerfile +++ b/modules/test/base/base.Dockerfile @@ -36,5 +36,13 @@ RUN dos2unix /testrun/bin/* # Make sure all the bin files are executable RUN chmod u+x /testrun/bin/* +# Copy over all network module gRPC proto files +ARG NET_MODULE_DIR=modules/network +ARG NET_MODULE_PROTO_DIR=python/src/grpc_server/proto/grpc.proto +ARG CONTAINER_PROTO_DIR=testrun/python/src/grpc_server/proto + +COPY $NET_MODULE_DIR/dhcp-1/$NET_MODULE_PROTO_DIR $CONTAINER_PROTO_DIR/dhcp1/ +COPY $NET_MODULE_DIR/dhcp-2/$NET_MODULE_PROTO_DIR $CONTAINER_PROTO_DIR/dhcp2/ + # Start the test module ENTRYPOINT [ "/testrun/bin/start_module" ] \ No newline at end of file diff --git a/modules/test/base/bin/setup_grpc_clients b/modules/test/base/bin/setup_grpc_clients new file mode 100644 index 000000000..30efe5002 --- /dev/null +++ b/modules/test/base/bin/setup_grpc_clients @@ -0,0 +1,34 @@ +#!/bin/bash -e + +GRPC_DIR="/testrun/python/src/grpc_server" +GRPC_PROTO_DIR="proto" +GRPC_PROTO_FILE="grpc.proto" + +# Build the grpc proto file +build_grpc_client(){ + MODULE=$1 + echo "Building gRPC proto: $MODULE" + python3 -m grpc_tools.protoc --proto_path=. ./$GRPC_PROTO_DIR/$MODULE/$GRPC_PROTO_FILE --python_out=. --grpc_python_out=. +} + +# Build the grpc proto files for every module that has a proto defined +build_grpc_clients(){ + + for dir in "$GRPC_DIR/$GRPC_PROTO_DIR"/*/;do + if [ -f $dir/$GRPC_PROTO_FILE ];then + # Extract the last folder name + last_folder="${dir%%/}" + last_folder="${last_folder##*/}" + build_grpc_client "$last_folder" + fi + done +} + +# Move into the grpc directory. +# This is necessary to build the proto files +# with the correct import paths +pushd $GRPC_DIR >/dev/null 2>&1 + +build_grpc_clients + +popd >/dev/null 2>&1 \ No newline at end of file diff --git a/modules/test/base/bin/setup_python_path b/modules/test/base/bin/setup_python_path new file mode 100644 index 000000000..8201bbb36 --- /dev/null +++ b/modules/test/base/bin/setup_python_path @@ -0,0 +1,25 @@ +#!/bin/bash + +ROOT_DIRECTORY="/testrun/python/src" + +# Function to recursively add subdirectories to PYTHONPATH +add_subdirectories_to_pythonpath() { + local directory=$1 + local subdirectories=( "$directory"/* ) + local subdirectory + + for subdirectory in "${subdirectories[@]}"; do + if [ -d "$subdirectory" ]; then + export PYTHONPATH="$PYTHONPATH:$subdirectory" + add_subdirectories_to_pythonpath "$subdirectory" + fi + done +} + +# Set PYTHONPATH initially to an empty string +export PYTHONPATH="" + +# Add all subdirectories to PYTHONPATH +add_subdirectories_to_pythonpath "$ROOT_DIRECTORY" + +echo "$PYTHONPATH" \ No newline at end of file diff --git a/modules/test/base/bin/start_module b/modules/test/base/bin/start_module index 5f6e1ee35..82c9d26bf 100644 --- a/modules/test/base/bin/start_module +++ b/modules/test/base/bin/start_module @@ -57,10 +57,21 @@ then exit 1 fi -echo "Starting module $MODULE_NAME..." +# Setup the PYTHONPATH so all imports work as expected +echo "Setting up PYTHONPATH..." +export PYTHONPATH=$($BIN_DIR/setup_python_path) +echo "PYTHONPATH: $PYTHONPATH" + +# Build all gRPC files from the proto for use in +# gRPC clients for communications to network modules +echo "Building gRPC files from available proto files..." +$BIN_DIR/setup_grpc_clients +echo "Configuring binary files..." $BIN_DIR/setup_binaries $BIN_DIR +echo "Starting module $MODULE_NAME..." + # Only start network services if the test container needs # a network connection to run its tests if [ $NETWORK_REQUIRED == "true" ];then @@ -78,9 +89,9 @@ then if [[ ! -z $GRPC_PORT && ! $GRPC_PORT == "null" ]] then echo "gRPC port resolved from config: $GRPC_PORT" - $BIN_DIR/start_grpc "-p $GRPC_PORT" & + $BIN_DIR/start_grpc "-p $GRPC_PORT" else - $BIN_DIR/start_grpc & + $BIN_DIR/start_grpc fi fi diff --git a/modules/test/base/python/src/grpc/proto/dhcp1/client.py b/modules/test/base/python/src/grpc/proto/dhcp1/client.py new file mode 100644 index 000000000..921929edb --- /dev/null +++ b/modules/test/base/python/src/grpc/proto/dhcp1/client.py @@ -0,0 +1,98 @@ +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(): + + 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 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/conn.Dockerfile b/modules/test/conn/conn.Dockerfile index 1714f49f2..5d8148335 100644 --- a/modules/test/conn/conn.Dockerfile +++ b/modules/test/conn/conn.Dockerfile @@ -17,6 +17,8 @@ FROM test-run/base-test:latest ARG MODULE_NAME=conn ARG MODULE_DIR=modules/test/$MODULE_NAME +ARG GRPC_PROTO_DIR=/testrun/python/src/grpc/proto/dhcp +ARG GRPC_PROTO_FILE="grpc.proto" # Install all necessary packages RUN apt-get install -y wget @@ -37,4 +39,4 @@ COPY $MODULE_DIR/conf /testrun/conf COPY $MODULE_DIR/bin /testrun/bin # Copy over all python files -COPY $MODULE_DIR/python /testrun/python +COPY $MODULE_DIR/python /testrun/python \ No newline at end of file diff --git a/modules/test/conn/python/src/connection_module.py b/modules/test/conn/python/src/connection_module.py index 196c335d8..a1727df23 100644 --- a/modules/test/conn/python/src/connection_module.py +++ b/modules/test/conn/python/src/connection_module.py @@ -17,6 +17,7 @@ import sys from scapy.all import * from test_module import TestModule +from dhcp1.client import Client as DHCPClient1 LOG_NAME = "test_connection" LOGGER = None @@ -33,6 +34,34 @@ def __init__(self, module): super().__init__(module_name=module, log_name=LOG_NAME) global LOGGER LOGGER = self._get_logger() + self.dhcp1_client = DHCPClient1() + + # ToDo: Move this into some level of testing, leave for + # reference until tests are implemented with these calls + # response = self.dhcp1_client.add_reserved_lease('test','00:11:22:33:44:55','10.10.10.21') + # print("AddLeaseResp: " + str(response)) + + # response = self.dhcp1_client.delete_reserved_lease('00:11:22:33:44:55') + # print("DelLeaseResp: " + str(response)) + + # response = self.dhcp1_client.disable_failover() + # print("FailoverDisabled: " + str(response)) + + # response = self.dhcp1_client.enable_failover() + # print("FailoverEnabled: " + str(response)) + + # response = self.dhcp1_client.get_dhcp_range() + # print("DHCP Range: " + str(response)) + + # response = self.dhcp1_client.get_lease(self._device_mac) + # print("Lease: " + str(response)) + + # response = self.dhcp1_client.get_status() + # print("Status: " + str(response)) + + # response = self.dhcp1_client.set_dhcp_range('10.10.10.20','10.10.10.30') + # print("Set Range: " + str(response)) + def _connection_mac_address(self): LOGGER.info("Running connection.mac_address") diff --git a/testing/test_baseline b/testing/test_baseline index ac47a5cfa..f12d124de 100755 --- a/testing/test_baseline +++ b/testing/test_baseline @@ -82,4 +82,4 @@ more $TESTRUN_OUT pytest testing/ -exit $? +exit $? \ No newline at end of file diff --git a/testing/unit_test/run_tests.sh b/testing/unit_test/run_tests.sh new file mode 100644 index 000000000..5b1ed6257 --- /dev/null +++ b/testing/unit_test/run_tests.sh @@ -0,0 +1,18 @@ +#!/bin/bash -e + +# This script should be run from within the unit_test directory. If +# it is run outside this directory, paths will not be resolved correctly. + +# Move into the root directory of test-run +pushd ../../ >/dev/null 2>&1 + +echo "Root Dir: $PWD" + +# Setup the python path +export PYTHONPATH="$PWD/framework/python/src" + +# Run the DHCP Unit tests +python3 -u $PWD/modules/network/dhcp-1/python/src/grpc_server/dhcp_config_test.py +python3 -u $PWD/modules/network/dhcp-2/python/src/grpc_server/dhcp_config_test.py + +popd >/dev/null 2>&1 \ No newline at end of file From af8367c704ca02acd4e2b1937f667610986618d6 Mon Sep 17 00:00:00 2001 From: jhughesbiot <50999916+jhughesbiot@users.noreply.github.com> Date: Thu, 6 Jul 2023 12:31:31 -0700 Subject: [PATCH 2/4] Dhcp (#67) * Add initial work for ip control module * Implement ip control module with additional cleanup methods * Update link check to not use error stream * Add error checking around container network configurations * Add network cleanup for namespaces and links * formatting * initial work on adding grpc functions for dhcp tests * rework code to allow for better usage and unit testing * working poc for test containers and grpc client to dhcp-1 * Move grpc client code into base image * Move grpc proto builds outside of dockerfile into module startup script * Setup pythonpath var in test module base startup process misc cleanup * pylinting and logging updates * Add python path resolving to network modules Update grpc path to prevent conflicts misc pylinting * Change lease resolving method to fix pylint issue * cleanup unit tests * cleanup unit tests * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * fix line endings * misc cleanup * Move isc-dhcp-server and radvd to services Move DHCP server monitoring and booting to python script * Add grpc methods to interact with dhcp_server module Update dhcp_server to control radvd server directly from calls Fix radvd service status method * Add updates to dhcp2 module Update radvd service * Add license headers --- modules/network/dhcp-1/bin/radvd-service | 55 ++++++++ .../network/dhcp-1/bin/start_network_service | 56 ++------ modules/network/dhcp-1/conf/isc-dhcp-server | 4 + modules/network/dhcp-1/dhcp-1.Dockerfile | 2 +- .../python/src/grpc_server/dhcp_server.py | 130 ++++++++++++++++++ .../python/src/grpc_server/network_service.py | 43 +++++- .../python/src/grpc_server/proto/grpc.proto | 16 ++- .../python/src/grpc_server/radvd_server.py | 55 ++++++++ modules/network/dhcp-2/bin/radvd-service | 55 ++++++++ .../network/dhcp-2/bin/start_network_service | 56 ++------ modules/network/dhcp-2/conf/isc-dhcp-server | 4 + modules/network/dhcp-2/dhcp-2.Dockerfile | 11 +- .../python/src/grpc_server/dhcp_config.py | 4 +- .../python/src/grpc_server/dhcp_server.py | 130 ++++++++++++++++++ .../python/src/grpc_server/network_service.py | 43 +++++- .../python/src/grpc_server/proto/grpc.proto | 14 +- .../python/src/grpc_server/radvd_server.py | 55 ++++++++ 17 files changed, 622 insertions(+), 111 deletions(-) create mode 100644 modules/network/dhcp-1/bin/radvd-service create mode 100644 modules/network/dhcp-1/conf/isc-dhcp-server create mode 100644 modules/network/dhcp-1/python/src/grpc_server/dhcp_server.py create mode 100644 modules/network/dhcp-1/python/src/grpc_server/radvd_server.py create mode 100644 modules/network/dhcp-2/bin/radvd-service create mode 100644 modules/network/dhcp-2/conf/isc-dhcp-server create mode 100644 modules/network/dhcp-2/python/src/grpc_server/dhcp_server.py create mode 100644 modules/network/dhcp-2/python/src/grpc_server/radvd_server.py diff --git a/modules/network/dhcp-1/bin/radvd-service b/modules/network/dhcp-1/bin/radvd-service new file mode 100644 index 000000000..1cfe499cb --- /dev/null +++ b/modules/network/dhcp-1/bin/radvd-service @@ -0,0 +1,55 @@ +#!/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. + +RA_PID_FILE=/var/run/radvd/radvd.pid +RA_LOG_FILE=/runtime/network/dhcp1-radvd.log + +stop_radvd(){ + # Directly kill by PID file reference + if [ -f "$RA_PID_FILE" ]; then + kill -9 $(cat $RA_PID_FILE) || true + rm -f $RA_PID_FILE + fi +} + +start_radvd(){ + /usr/sbin/radvd -m logfile -l $RA_LOG_FILE -p $RA_PID_FILE +} + +case "$1" in + start) + start_radvd + ;; + stop) + stop_radvd + ;; + restart) + stop_radvd + sleep 1 + start_radvd + ;; + status) + if [ -f "$RA_PID_FILE" ]; then + echo "radvd service is running." + else + echo "radvd 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 9f4a3dc51..82b4c6e33 100644 --- a/modules/network/dhcp-1/bin/start_network_service +++ b/modules/network/dhcp-1/bin/start_network_service @@ -29,63 +29,23 @@ sysctl -p # Create leases file if needed touch /var/lib/dhcp/dhcpd.leases -#Create directory for radvd +# Create directory for radvd mkdir /var/run/radvd -#Create and set permissions on the log files +# Create and set permissions on the log files touch $DHCP_LOG_FILE touch $RA_LOG_FILE chown $HOST_USER $DHCP_LOG_FILE chown $HOST_USER $RA_LOG_FILE -#Move the config files to the correct location +# Move the config files to the correct location +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 -# Restart dhcp server when config changes -while true; do +# Move the radvd-sevice file to the correct location +cp /testrun/bin/radvd-service /usr/local/bin/ - new_checksum=$(md5sum $CONFIG_FILE) - - if [ "$checksum" == "$new_checksum" ]; then - sleep 2 - continue - fi - - echo Config changed. Restarting dhcp server at $(date).. - - if [ -f $DHCP_PID_FILE ]; then - kill -9 $(cat $DHCP_PID_FILE) || true - rm -f $DHCP_PID_FILE - fi - - if [ -f $RA_PID_FILE ]; then - kill -9 $(cat $RA_PID_FILE) || true - rm -f $RA_PID_FILE - fi - - checksum=$new_checksum - - echo Starting isc-dhcp-server at $(date) - - radvd -m logfile -l $RA_LOG_FILE -p $RA_PID_FILE - dhcpd -d &> $DHCP_LOG_FILE & - - while [ ! -f $DHCP_PID_FILE ]; do - echo Waiting for $DHCP_PID_FILE... - sleep 2 - done - - echo $DHCP_PID_FILE now available - - while [ ! -f $RA_PID_FILE ]; do - echo Waiting for $RA_PID_FILE... - sleep 2 - done - - echo $RA_PID_FILE now available - - echo Server now stable - -done \ No newline at end of file +# Start the DHCP Server +python3 -u /testrun/python/src/grpc_server/dhcp_server.py \ No newline at end of file diff --git a/modules/network/dhcp-1/conf/isc-dhcp-server b/modules/network/dhcp-1/conf/isc-dhcp-server new file mode 100644 index 000000000..44db95cd9 --- /dev/null +++ b/modules/network/dhcp-1/conf/isc-dhcp-server @@ -0,0 +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" diff --git a/modules/network/dhcp-1/dhcp-1.Dockerfile b/modules/network/dhcp-1/dhcp-1.Dockerfile index b47378045..6b941d878 100644 --- a/modules/network/dhcp-1/dhcp-1.Dockerfile +++ b/modules/network/dhcp-1/dhcp-1.Dockerfile @@ -25,7 +25,7 @@ RUN apt-get install -y wget RUN wget http://standards-oui.ieee.org/oui.txt -P /usr/local/etc/ # Install dhcp server -RUN apt-get install -y isc-dhcp-server radvd +RUN apt-get install -y isc-dhcp-server radvd systemd # Copy over all configuration files COPY $MODULE_DIR/conf /testrun/conf 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 new file mode 100644 index 000000000..2f67b0c2d --- /dev/null +++ b/modules/network/dhcp-1/python/src/grpc_server/dhcp_server.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. +"""Contains all the necessary classes to maintain the +DHCP server""" +import time +from common import logger +from common import util +from dhcp_config import DHCPConfig +from radvd_server import RADVDServer + +CONFIG_FILE = '/etc/dhcp/dhcpd.conf' +LOG_NAME = 'dhcp_server' +LOGGER = None + + +class DHCPServer: + """Represents the DHCP Server""" + + def __init__(self): + global LOGGER + LOGGER = logger.get_logger(LOG_NAME, 'dhcp-1') + self.dhcp_config = DHCPConfig() + self.radvd = RADVDServer() + self.dhcp_config.resolve_config() + + def restart(self): + LOGGER.info("Restarting DHCP Server") + isc_started = util.run_command("service isc-dhcp-server restart", False) + radvd_started = self.radvd.restart() + started = isc_started and radvd_started + LOGGER.info("DHCP Restarted: " + str(started)) + return started + + def start(self): + LOGGER.info("Starting DHCP Server") + isc_started = util.run_command("service isc-dhcp-server start", False) + radvd_started = self.radvd.start() + started = isc_started and radvd_started + LOGGER.info("DHCP Started: " + str(started)) + return started + + def stop(self): + LOGGER.info("Stopping DHCP Server") + isc_stopped = util.run_command("service isc-dhcp-server stop", False) + radvd_stopped = self.radvd.stop() + stopped = isc_stopped and radvd_stopped + LOGGER.info("DHCP Stopped: " + str(stopped)) + return stopped + + 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.' + radvd_running = self.radvd.is_running() + running = isc_running and radvd_running + LOGGER.info("DHCP Status: " + str(running)) + return running + + def boot(self): + LOGGER.info("Booting DHCP Server") + isc_booted = False + radvd_booted = False + if self.is_running(): + LOGGER.info("Stopping isc-dhcp-server") + stopped = self.stop() + LOGGER.info("isc-dhcp-server stopped: " + str(stopped)) + + if self.radvd.is_running(): + LOGGER.info("Stopping RADVD") + stopped = self.radvd.stop() + LOGGER.info("radvd stopped: " + str(stopped)) + + LOGGER.info("Starting isc-dhcp-server") + if self.start(): + isc_booted = False + # Scan for 5 seconds if not yet ready + for i in range(5): + time.sleep(1) + isc_booted = self.is_running() + if isc_booted: + break; + LOGGER.info("isc-dhcp-server started: " + str(isc_booted)) + + LOGGER.info("Starting RADVD") + if self.radvd.start(): + radvd_booted = False + # Scan for 5 seconds if not yet ready + for i in range(5): + time.sleep(1) + radvd_booted = self.radvd.is_running() + if radvd_booted: + break; + LOGGER.info("RADVD started: " + str(radvd_booted)) + + + + return isc_booted and radvd_booted + +def run(): + dhcp_server = DHCPServer() + booted = dhcp_server.boot() + + if not booted: + LOGGER.error('DHCP Server Failed to boot. Exiting') + sys.exit(1) + + config = str(dhcp_server.dhcp_config) + while True: + dhcp_server.dhcp_config.resolve_config() + new_config = str(dhcp_server.dhcp_config) + if config != new_config: + LOGGER.info("DHCP Config Changed") + config = new_config + success = dhcp_server.restart() + success = 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 bf2b98803..a693ac3a1 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 @@ -15,6 +15,7 @@ import proto.grpc_pb2_grpc as pb2_grpc import proto.grpc_pb2 as pb2 +from dhcp_server import DHCPServer from dhcp_config import DHCPConfig from dhcp_leases import DHCPLeases @@ -28,6 +29,7 @@ class NetworkService(pb2_grpc.NetworkModule): """gRPC endpoints for the DHCP Server""" def __init__(self): + self._dhcp_server = DHCPServer() self._dhcp_config = None self.dhcp_leases = DHCPLeases() global LOGGER @@ -39,6 +41,42 @@ def _get_dhcp_config(self): self._dhcp_config.resolve_config() return self._dhcp_config + def RestartDHCPServer(self, request, context): # pylint: disable=W0613 + LOGGER.info('Restarting DHCP server') + try: + started = self._dhcp_server.restart() + LOGGER.info('DHCP server restarted: ' + (str(started))) + return pb2.Response(code=200, message='{}') + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to restart DHCP server: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.Response(code=500, message=fail_message) + + def StartDHCPServer(self, request, context): # pylint: disable=W0613 + LOGGER.info('Starting DHCP server') + try: + started = self._dhcp_server.start() + LOGGER.info('DHCP server started: ' + (str(started))) + return pb2.Response(code=200, message='{}') + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to start DHCP server: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.Response(code=500, message=fail_message) + + def StopDHCPServer(self, request, context): # pylint: disable=W0613 + LOGGER.info('Stopping DHCP server') + try: + stopped = self._dhcp_server.stop() + LOGGER.info('DHCP server stopped: ' + (str(stopped))) + return pb2.Response(code=200, message='{}') + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to stop DHCP server: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.Response(code=500, message=fail_message) + def AddReservedLease(self, request, context): # pylint: disable=W0613 LOGGER.info('Add reserved lease called') try: @@ -151,7 +189,6 @@ def GetStatus(self, request, context): # pylint: disable=W0613 """ Return the current status of the network module """ - # ToDo: Figure out how to resolve the current DHCP status - dhcp_status = True + dhcp_status = self._dhcp_server.is_running() message = str({'dhcpStatus': dhcp_status}) - return pb2.Response(code=200, message=message) + return pb2.Response(code=200, message=message) \ No newline at end of file diff --git a/modules/network/dhcp-1/python/src/grpc_server/proto/grpc.proto b/modules/network/dhcp-1/python/src/grpc_server/proto/grpc.proto index d9f56213e..e6abda674 100644 --- a/modules/network/dhcp-1/python/src/grpc_server/proto/grpc.proto +++ b/modules/network/dhcp-1/python/src/grpc_server/proto/grpc.proto @@ -2,6 +2,12 @@ syntax = "proto3"; service NetworkModule { + rpc RestartDHCPServer(RestartDHCPServerRequest) returns (Response) {}; + + rpc StartDHCPServer(StartDHCPServerRequest) returns (Response) {}; + + rpc StopDHCPServer(StopDHCPServerRequest) returns (Response) {}; + rpc AddReservedLease(AddReservedLeaseRequest) returns (Response) {}; rpc DeleteReservedLease(DeleteReservedLeaseRequest) returns (Response) {}; @@ -29,6 +35,12 @@ message DeleteReservedLeaseRequest { string hw_addr = 1; } +message RestartDHCPServerRequest {} + +message StartDHCPServerRequest {} + +message StopDHCPServerRequest {} + message DisableFailoverRequest {} message EnableFailoverRequest {} @@ -53,7 +65,7 @@ message Response { } message DHCPRange { - int32 code = 1; + int32 code = 1; string start = 2; string end = 3; -} +} \ No newline at end of file diff --git a/modules/network/dhcp-1/python/src/grpc_server/radvd_server.py b/modules/network/dhcp-1/python/src/grpc_server/radvd_server.py new file mode 100644 index 000000000..48e063e61 --- /dev/null +++ b/modules/network/dhcp-1/python/src/grpc_server/radvd_server.py @@ -0,0 +1,55 @@ +# 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. +"""Contains all the necessary classes to maintain the +DHCP server""" +import time +from common import logger +from common import util +from dhcp_config import DHCPConfig + +CONFIG_FILE = '/etc/dhcp/dhcpd.conf' +LOG_NAME = 'radvd' +LOGGER = None + + +class RADVDServer: + """Represents the RADVD Server""" + + def __init__(self): + global LOGGER + LOGGER = logger.get_logger(LOG_NAME, 'dhcp-1') + + def restart(self): + LOGGER.info("Restarting RADVD Server") + response = util.run_command("radvd-service restart", False) + LOGGER.info("RADVD Restarted: " + str(response)) + return response + + def start(self): + LOGGER.info("Starting RADVD Server") + response = util.run_command("radvd-service start", False) + LOGGER.info("RADVD Started: " + str(response)) + return response + + def stop(self): + LOGGER.info("Stopping RADVD Server") + response = util.run_command("radvd-service stop", False) + LOGGER.info("RADVD Stopped: " + str(response)) + return response + + def is_running(self): + LOGGER.info("Checking RADVD Status") + response = util.run_command("radvd-service status") + LOGGER.info("RADVD Status: " + str(response)) + return response[0] == 'radvd service is running.' diff --git a/modules/network/dhcp-2/bin/radvd-service b/modules/network/dhcp-2/bin/radvd-service new file mode 100644 index 000000000..912c64ee3 --- /dev/null +++ b/modules/network/dhcp-2/bin/radvd-service @@ -0,0 +1,55 @@ +#!/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. + +RA_PID_FILE=/var/run/radvd/radvd.pid +RA_LOG_FILE=/runtime/network/dhcp2-radvd.log + +stop_radvd(){ + # Directly kill by PID file reference + if [ -f "$RA_PID_FILE" ]; then + kill -9 $(cat $RA_PID_FILE) || true + rm -f $RA_PID_FILE + fi +} + +start_radvd(){ + /usr/sbin/radvd -m logfile -l $RA_LOG_FILE -p $RA_PID_FILE +} + +case "$1" in + start) + start_radvd + ;; + stop) + stop_radvd + ;; + restart) + stop_radvd + sleep 1 + start_radvd + ;; + status) + if [ -f "$RA_PID_FILE" ]; then + echo "radvd service is running." + else + echo "radvd 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 723689278..ed7d3125e 100644 --- a/modules/network/dhcp-2/bin/start_network_service +++ b/modules/network/dhcp-2/bin/start_network_service @@ -29,63 +29,23 @@ sysctl -p # Create leases file if needed touch /var/lib/dhcp/dhcpd.leases -#Create directory for radvd +# Create directory for radvd mkdir /var/run/radvd -#Create and set permissions on the log files +# Create and set permissions on the log files touch $DHCP_LOG_FILE touch $RA_LOG_FILE chown $HOST_USER $DHCP_LOG_FILE chown $HOST_USER $RA_LOG_FILE -#Move the config files to the correct location +# Move the config files to the correct location +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 -# Restart dhcp server when config changes -while true; do +# Move the radvd-sevice file to the correct location +cp /testrun/bin/radvd-service /usr/local/bin/ - new_checksum=$(md5sum $CONFIG_FILE) - - if [ "$checksum" == "$new_checksum" ]; then - sleep 2 - continue - fi - - echo Config changed. Restarting dhcp server at $(date).. - - if [ -f $DHCP_PID_FILE ]; then - kill -9 $(cat $DHCP_PID_FILE) || true - rm -f $DHCP_PID_FILE - fi - - if [ -f $RA_PID_FILE ]; then - kill -9 $(cat $RA_PID_FILE) || true - rm -f $RA_PID_FILE - fi - - checksum=$new_checksum - - echo Starting isc-dhcp-server at $(date) - - radvd -m logfile -l $RA_LOG_FILE -p $RA_PID_FILE - dhcpd -d &> $DHCP_LOG_FILE & - - while [ ! -f $DHCP_PID_FILE ]; do - echo Waiting for $DHCP_PID_FILE... - sleep 2 - done - - echo $DHCP_PID_FILE now available - - while [ ! -f $RA_PID_FILE ]; do - echo Waiting for $RA_PID_FILE... - sleep 2 - done - - echo $RA_PID_FILE now available - - echo Server now stable - -done \ No newline at end of file +# Start the DHCP Server +python3 -u /testrun/python/src/grpc_server/dhcp_server.py \ No newline at end of file diff --git a/modules/network/dhcp-2/conf/isc-dhcp-server b/modules/network/dhcp-2/conf/isc-dhcp-server new file mode 100644 index 000000000..44db95cd9 --- /dev/null +++ b/modules/network/dhcp-2/conf/isc-dhcp-server @@ -0,0 +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" diff --git a/modules/network/dhcp-2/dhcp-2.Dockerfile b/modules/network/dhcp-2/dhcp-2.Dockerfile index df77cb811..153aa50e7 100644 --- a/modules/network/dhcp-2/dhcp-2.Dockerfile +++ b/modules/network/dhcp-2/dhcp-2.Dockerfile @@ -18,8 +18,14 @@ FROM test-run/base:latest ARG MODULE_NAME=dhcp-2 ARG MODULE_DIR=modules/network/$MODULE_NAME +# Install all necessary packages +RUN apt-get install -y wget + +#Update the oui.txt file from ieee +RUN wget http://standards-oui.ieee.org/oui.txt -P /usr/local/etc/ + # Install dhcp server -RUN apt-get install -y isc-dhcp-server radvd +RUN apt-get install -y isc-dhcp-server radvd systemd # Copy over all configuration files COPY $MODULE_DIR/conf /testrun/conf @@ -28,5 +34,4 @@ COPY $MODULE_DIR/conf /testrun/conf COPY $MODULE_DIR/bin /testrun/bin # Copy over all python files -COPY $MODULE_DIR/python /testrun/python - +COPY $MODULE_DIR/python /testrun/python \ No newline at end of file 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 444faa87c..33cb5938c 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 @@ -33,7 +33,7 @@ def __init__(self): self._peer = None self._reserved_hosts = [] global LOGGER - LOGGER = logger.get_logger(LOG_NAME, 'dhcp-1') + LOGGER = logger.get_logger(LOG_NAME, 'dhcp-2') def add_reserved_host(self, hostname, hw_addr, ip_addr): host = DHCPReservedHost(hostname=hostname, @@ -490,4 +490,4 @@ def resolve_host(self, reserved_host): self.hw_addr = part.strip().split(HARDWARE_KEY)[1].strip().split(';')[0] elif FIXED_ADDRESS_KEY in part: self.fixed_addr = part.strip().split( - FIXED_ADDRESS_KEY)[1].strip().split(';')[0] + FIXED_ADDRESS_KEY)[1].strip().split(';')[0] \ No newline at end of file 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 new file mode 100644 index 000000000..1431d6ddd --- /dev/null +++ b/modules/network/dhcp-2/python/src/grpc_server/dhcp_server.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. +"""Contains all the necessary classes to maintain the +DHCP server""" +import time +from common import logger +from common import util +from dhcp_config import DHCPConfig +from radvd_server import RADVDServer + +CONFIG_FILE = '/etc/dhcp/dhcpd.conf' +LOG_NAME = 'dhcp_server' +LOGGER = None + + +class DHCPServer: + """Represents the DHCP Server""" + + def __init__(self): + global LOGGER + LOGGER = logger.get_logger(LOG_NAME, 'dhcp-2') + self.dhcp_config = DHCPConfig() + self.radvd = RADVDServer() + self.dhcp_config.resolve_config() + + def restart(self): + LOGGER.info("Restarting DHCP Server") + isc_started = util.run_command("service isc-dhcp-server restart", False) + radvd_started = self.radvd.restart() + started = isc_started and radvd_started + LOGGER.info("DHCP Restarted: " + str(started)) + return started + + def start(self): + LOGGER.info("Starting DHCP Server") + isc_started = util.run_command("service isc-dhcp-server start", False) + radvd_started = self.radvd.start() + started = isc_started and radvd_started + LOGGER.info("DHCP Started: " + str(started)) + return started + + def stop(self): + LOGGER.info("Stopping DHCP Server") + isc_stopped = util.run_command("service isc-dhcp-server stop", False) + radvd_stopped = self.radvd.stop() + stopped = isc_stopped and radvd_stopped + LOGGER.info("DHCP Stopped: " + str(stopped)) + return stopped + + 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.' + radvd_running = self.radvd.is_running() + running = isc_running and radvd_running + LOGGER.info("DHCP Status: " + str(running)) + return running + + def boot(self): + LOGGER.info("Booting DHCP Server") + isc_booted = False + radvd_booted = False + if self.is_running(): + LOGGER.info("Stopping isc-dhcp-server") + stopped = self.stop() + LOGGER.info("isc-dhcp-server stopped: " + str(stopped)) + + if self.radvd.is_running(): + LOGGER.info("Stopping RADVD") + stopped = self.radvd.stop() + LOGGER.info("radvd stopped: " + str(stopped)) + + LOGGER.info("Starting isc-dhcp-server") + if self.start(): + isc_booted = False + # Scan for 5 seconds if not yet ready + for i in range(5): + time.sleep(1) + isc_booted = self.is_running() + if isc_booted: + break; + LOGGER.info("isc-dhcp-server started: " + str(isc_booted)) + + LOGGER.info("Starting RADVD") + if self.radvd.start(): + radvd_booted = False + # Scan for 5 seconds if not yet ready + for i in range(5): + time.sleep(1) + radvd_booted = self.radvd.is_running() + if radvd_booted: + break; + LOGGER.info("RADVD started: " + str(radvd_booted)) + + + + return isc_booted and radvd_booted + +def run(): + dhcp_server = DHCPServer() + booted = dhcp_server.boot() + + if not booted: + LOGGER.error('DHCP Server Failed to boot. Exiting') + sys.exit(1) + + config = str(dhcp_server.dhcp_config) + while True: + dhcp_server.dhcp_config.resolve_config() + new_config = str(dhcp_server.dhcp_config) + if config != new_config: + LOGGER.info("DHCP Config Changed") + config = new_config + success = dhcp_server.restart() + success = dhcp_server.radvd.restart() + time.sleep(1) + +if __name__ == '__main__': + run() diff --git a/modules/network/dhcp-2/python/src/grpc_server/network_service.py b/modules/network/dhcp-2/python/src/grpc_server/network_service.py index 053d26d6b..5af9e6c44 100644 --- a/modules/network/dhcp-2/python/src/grpc_server/network_service.py +++ b/modules/network/dhcp-2/python/src/grpc_server/network_service.py @@ -15,6 +15,7 @@ import proto.grpc_pb2_grpc as pb2_grpc import proto.grpc_pb2 as pb2 +from dhcp_server import DHCPServer from dhcp_config import DHCPConfig from dhcp_leases import DHCPLeases @@ -28,6 +29,7 @@ class NetworkService(pb2_grpc.NetworkModule): """gRPC endpoints for the DHCP Server""" def __init__(self): + self._dhcp_server = DHCPServer() self._dhcp_config = None self.dhcp_leases = DHCPLeases() global LOGGER @@ -39,6 +41,42 @@ def _get_dhcp_config(self): self._dhcp_config.resolve_config() return self._dhcp_config + def RestartDHCPServer(self, request, context): # pylint: disable=W0613 + LOGGER.info('Restarting DHCP server') + try: + started = self._dhcp_server.restart() + LOGGER.info('DHCP server restarted: ' + (str(started))) + return pb2.Response(code=200, message='{}') + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to restart DHCP server: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.Response(code=500, message=fail_message) + + def StartDHCPServer(self, request, context): # pylint: disable=W0613 + LOGGER.info('Starting DHCP server') + try: + started = self._dhcp_server.start() + LOGGER.info('DHCP server started: ' + (str(started))) + return pb2.Response(code=200, message='{}') + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to start DHCP server: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.Response(code=500, message=fail_message) + + def StopDHCPServer(self, request, context): # pylint: disable=W0613 + LOGGER.info('Stopping DHCP server') + try: + stopped = self._dhcp_server.stop() + LOGGER.info('DHCP server stopped: ' + (str(stopped))) + return pb2.Response(code=200, message='{}') + except Exception as e: # pylint: disable=W0718 + fail_message = 'Failed to stop DHCP server: ' + str(e) + LOGGER.error(fail_message) + LOGGER.error(traceback.format_exc()) + return pb2.Response(code=500, message=fail_message) + def AddReservedLease(self, request, context): # pylint: disable=W0613 LOGGER.info('Add reserved lease called') try: @@ -151,7 +189,6 @@ def GetStatus(self, request, context): # pylint: disable=W0613 """ Return the current status of the network module """ - # ToDo: Figure out how to resolve the current DHCP status - dhcp_status = True + dhcp_status = self._dhcp_server.is_running() message = str({'dhcpStatus': dhcp_status}) - return pb2.Response(code=200, message=message) + return pb2.Response(code=200, message=message) \ No newline at end of file diff --git a/modules/network/dhcp-2/python/src/grpc_server/proto/grpc.proto b/modules/network/dhcp-2/python/src/grpc_server/proto/grpc.proto index b6a11a75b..e6abda674 100644 --- a/modules/network/dhcp-2/python/src/grpc_server/proto/grpc.proto +++ b/modules/network/dhcp-2/python/src/grpc_server/proto/grpc.proto @@ -2,6 +2,12 @@ syntax = "proto3"; service NetworkModule { + rpc RestartDHCPServer(RestartDHCPServerRequest) returns (Response) {}; + + rpc StartDHCPServer(StartDHCPServerRequest) returns (Response) {}; + + rpc StopDHCPServer(StopDHCPServerRequest) returns (Response) {}; + rpc AddReservedLease(AddReservedLeaseRequest) returns (Response) {}; rpc DeleteReservedLease(DeleteReservedLeaseRequest) returns (Response) {}; @@ -29,6 +35,12 @@ message DeleteReservedLeaseRequest { string hw_addr = 1; } +message RestartDHCPServerRequest {} + +message StartDHCPServerRequest {} + +message StopDHCPServerRequest {} + message DisableFailoverRequest {} message EnableFailoverRequest {} @@ -56,4 +68,4 @@ message DHCPRange { int32 code = 1; string start = 2; string end = 3; -} +} \ No newline at end of file diff --git a/modules/network/dhcp-2/python/src/grpc_server/radvd_server.py b/modules/network/dhcp-2/python/src/grpc_server/radvd_server.py new file mode 100644 index 000000000..0c6ef90d6 --- /dev/null +++ b/modules/network/dhcp-2/python/src/grpc_server/radvd_server.py @@ -0,0 +1,55 @@ +# 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. +"""Contains all the necessary classes to maintain the +DHCP server""" +import time +from common import logger +from common import util +from dhcp_config import DHCPConfig + +CONFIG_FILE = '/etc/dhcp/dhcpd.conf' +LOG_NAME = 'radvd' +LOGGER = None + + +class RADVDServer: + """Represents the RADVD Server""" + + def __init__(self): + global LOGGER + LOGGER = logger.get_logger(LOG_NAME, 'dhcp-2') + + def restart(self): + LOGGER.info("Restarting RADVD Server") + response = util.run_command("radvd-service restart", False) + LOGGER.info("RADVD Restarted: " + str(response)) + return response + + def start(self): + LOGGER.info("Starting RADVD Server") + response = util.run_command("radvd-service start", False) + LOGGER.info("RADVD Started: " + str(response)) + return response + + def stop(self): + LOGGER.info("Stopping RADVD Server") + response = util.run_command("radvd-service stop", False) + LOGGER.info("RADVD Stopped: " + str(response)) + return response + + def is_running(self): + LOGGER.info("Checking RADVD Status") + response = util.run_command("radvd-service status") + LOGGER.info("RADVD Status: " + str(response)) + return response[0] == 'radvd service is running.' From 7dd5772a275ac6530b594c7b149da1677e026a6c Mon Sep 17 00:00:00 2001 From: jhughesbiot <50999916+jhughesbiot@users.noreply.github.com> Date: Thu, 6 Jul 2023 15:22:52 -0700 Subject: [PATCH 3/4] Add connection.dhcp_address test (#68) --- modules/test/conn/conf/module_config.json | 5 +++++ .../test/conn/python/src/connection_module.py | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/modules/test/conn/conf/module_config.json b/modules/test/conn/conf/module_config.json index 0f599c5d3..4053b4e26 100644 --- a/modules/test/conn/conf/module_config.json +++ b/modules/test/conn/conf/module_config.json @@ -12,6 +12,11 @@ "timeout": 30 }, "tests": [ + { + "name": "connection.dhcp_address", + "description": "The device under test has received an IP address from the DHCP server and responds to an ICMP echo (ping) request", + "expected_behavior": "The device is not setup with a static IP address. The device accepts an IP address from a DHCP server (RFC 2131) and responds succesfully to an ICMP echo (ping) request." + }, { "name": "connection.mac_address", "description": "Check and note device physical address.", diff --git a/modules/test/conn/python/src/connection_module.py b/modules/test/conn/python/src/connection_module.py index a1727df23..5b3bf7038 100644 --- a/modules/test/conn/python/src/connection_module.py +++ b/modules/test/conn/python/src/connection_module.py @@ -62,6 +62,25 @@ 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_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) + if 'ip' in lease: + ip_addr = lease['ip'] + LOGGER.info("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)) + if ping_success: + return True, "Device responded to leased ip address" + else: + return False, "Device did not respond to leased ip address" + else: + LOGGER.info("No DHCP lease found for: " + self._device_mac) + return False, "No DHCP lease found for: " + self._device_mac def _connection_mac_address(self): LOGGER.info("Running connection.mac_address") From 8a3ebce4d3fbdf88f3125aa46a688005e3284f62 Mon Sep 17 00:00:00 2001 From: jhughesbiot Date: Fri, 7 Jul 2023 16:08:21 -0600 Subject: [PATCH 4/4] formatting --- .../python/src/grpc_server/dhcp_config.py | 45 ++++-- .../src/grpc_server/dhcp_config_test.py | 150 +++++++++--------- .../python/src/grpc_server/dhcp_server.py | 74 ++++----- .../python/src/grpc_server/network_service.py | 25 +-- .../python/src/grpc_server/radvd_server.py | 26 ++- .../python/src/grpc_server/dhcp_config.py | 47 ++++-- .../src/grpc_server/dhcp_config_test.py | 150 +++++++++--------- .../python/src/grpc_server/dhcp_server.py | 74 ++++----- .../python/src/grpc_server/network_service.py | 25 +-- .../python/src/grpc_server/radvd_server.py | 26 ++- .../test/conn/python/src/connection_module.py | 101 ++++++------ 11 files changed, 389 insertions(+), 354 deletions(-) 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 444faa87c..ae5fb1d97 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 @@ -186,13 +186,18 @@ def __str__(self): config += '\tprimary;' if self.primary else 'secondary;' config += '\n\t{ADDRESS_KEY} {ADDRESS};' if self.address is not None else '' config += '\n\t{PORT_KEY} {PORT};' if self.port is not None else '' - config += '\n\t{PEER_ADDRESS_KEY} {PEER_ADDRESS};' if self.peer_address is not None else '' - config += '\n\t{PEER_PORT_KEY} {PEER_PORT};' if self.peer_port is not None else '' - config += '\n\t{MAX_RESPONSE_DELAY_KEY} {MAX_RESPONSE_DELAY};' if self.max_response_delay is not None else '' - config += '\n\t{MAX_UNACKED_UPDATES_KEY} {MAX_UNACKED_UPDATES};' if self.max_unacked_updates is not None else '' + config += ('\n\t{PEER_ADDRESS_KEY} {PEER_ADDRESS};' + if self.peer_address is not None else '') + config += ('\n\t{PEER_PORT_KEY} {PEER_PORT};' + if self.peer_port is not None else '') + config += ('\n\t{MAX_RESPONSE_DELAY_KEY} {MAX_RESPONSE_DELAY};' + if self.max_response_delay is not None else '') + config += ('\n\t{MAX_UNACKED_UPDATES_KEY} {MAX_UNACKED_UPDATES};' + if self.max_unacked_updates is not None else '') config += '\n\t{MCLT_KEY} {MCLT};' if self.mclt is not None else '' config += '\n\t{SPLIT_KEY} {SPLIT};' if self.split is not None else '' - config += '\n\t{LOAD_BALANCE_MAX_SECONDS_KEY} {LOAD_BALANCE_MAX_SECONDS};' if self.load_balance_max_seconds is not None else '' + config += ('\n\t{LOAD_BALANCE_MAX_SECONDS_KEY} {LOAD_BALANCE_MAX_SECONDS};' + if self.load_balance_max_seconds is not None else '') config += '\n\r}}' config = config.format( @@ -220,9 +225,9 @@ def __str__(self): if not self.enabled: lines = config.strip().split('\n') - for i in range(len(lines)-1): + for i in range(len(lines) - 1): lines[i] = '#' + lines[i] - lines[-1] = '#' + lines[-1].strip() # Handle the last line separately + lines[-1] = '#' + lines[-1].strip() # Handle the last line separately config = '\n'.join(lines) return config @@ -302,15 +307,20 @@ def __init__(self, subnet): def __str__(self): config = 'subnet {SUBNET_OPTION} netmask {SUBNET_MASK_OPTION} {{' - config += '\n\t{NTP_OPTION_KEY} {NTP_OPTION};' if self._ntp_servers is not None else '' - config += '\n\t{SUBNET_MASK_OPTION_KEY} {SUBNET_MASK_OPTION};' if self._subnet_mask is not None else '' - config += '\n\t{BROADCAST_OPTION_KEY} {BROADCAST_OPTION};' if self._broadcast is not None else '' - config += '\n\t{ROUTER_OPTION_KEY} {ROUTER_OPTION};' if self._routers is not None else '' - config += '\n\t{DNS_OPTION_KEY} {DNS_OPTION};' if self._dns_servers is not None else '' - config += '\n\t{INTERFACE_KEY} {INTERFACE_OPTION};' if self._interface is not None else '' + config += ('\n\t{NTP_OPTION_KEY} {NTP_OPTION};' + if self._ntp_servers is not None else '') + config += ('\n\t{SUBNET_MASK_OPTION_KEY} {SUBNET_MASK_OPTION};' + if self._subnet_mask is not None else '') + config += ('\n\t{BROADCAST_OPTION_KEY} {BROADCAST_OPTION};' + if self._broadcast is not None else '') + config += ('\n\t{ROUTER_OPTION_KEY} {ROUTER_OPTION};' + if self._routers is not None else '') + config += ('\n\t{DNS_OPTION_KEY} {DNS_OPTION};' + if self._dns_servers is not None else '') + config += ('\n\t{INTERFACE_KEY} {INTERFACE_OPTION};' + if self._interface is not None else '') config += '\n\t{AUTHORITATIVE_KEY};' if self._authoritative else '' - config = config.format(length='multi-line', SUBNET_OPTION=self._subnet, NTP_OPTION_KEY=NTP_OPTION_KEY, @@ -407,8 +417,11 @@ def __init__(self, pool): def __str__(self): config = 'pool {{' - config += '\n\t\t{FAILOVER_KEY} "{FAILOVER}";' if self.failover_peer is not None else '' - config += '\n\t\t{RANGE_KEY} {RANGE_START} {RANGE_END};' if self.range_start is not None and self.range_end is not None else '' + config += ('\n\t\t{FAILOVER_KEY} "{FAILOVER}";' + if self.failover_peer is not None else '') + config += ('\n\t\t{RANGE_KEY} {RANGE_START} {RANGE_END};' + if self.range_start is not None and self.range_end is not None + else '') config += '\n\t}}' config = config.format( 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 2cc78403a..d9a0b0190 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 @@ -20,84 +20,88 @@ DHCP_CONFIG = None + def get_config_file_path(): - dhcp_config = DHCPConfig() - current_dir = os.path.dirname(os.path.abspath(__file__)) - module_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(current_dir)))) - conf_file = os.path.join(module_dir,CONFIG_FILE) - return conf_file + current_dir = os.path.dirname(os.path.abspath(__file__)) + module_dir = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(current_dir)))) + 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 + dhcp_config = DHCPConfig() + dhcp_config.resolve_config(get_config_file_path()) + return dhcp_config + class DHCPConfigTest(unittest.TestCase): - @classmethod - def setUpClass(cls): - # Resolve the config - global DHCP_CONFIG - DHCP_CONFIG = get_config() - - def test_resolve_config(self): - print('Test Resolve Config:\n' + str(DHCP_CONFIG)) - - # Resolve the raw config file - with open(get_config_file_path(),'r') as f: - lines = f.readlines() - - # Get the resolved config as a - conf_parts = str(DHCP_CONFIG).split('\n') - - # dhcpd conf is not picky about spacing so we just - # need to check contents of each line for matching - # to make sure evertying matches - for i in range(len(lines)): - self.assertEqual(lines[i].strip(),conf_parts[i].strip()) - - def test_disable_failover(self): - DHCP_CONFIG.disable_failover() - print('Test Disable Config:\n' + str(DHCP_CONFIG)) - config_lines = str(DHCP_CONFIG._peer).split('\n') - for line in config_lines: - self.assertTrue(line.startswith('#')) - - def test_enable_failover(self): - DHCP_CONFIG.enable_failover() - print('Test Enable Config:\n' + str(DHCP_CONFIG)) - config_lines = str(DHCP_CONFIG._peer).split('\n') - for line in config_lines: - self.assertFalse(line.startswith('#')) - - def test_add_reserved_host(self): - DHCP_CONFIG.add_reserved_host('test','00:11:22:33:44:55','192.168.10.5') - host = DHCP_CONFIG.get_reserved_host('00:11:22:33:44:55') - self.assertIsNotNone(host) - print('AddHostConfig:\n' + str(DHCP_CONFIG)) - - def test_delete_reserved_host(self): - DHCP_CONFIG.delete_reserved_host('00:11:22:33:44:55') - host = DHCP_CONFIG.get_reserved_host('00:11:22:33:44:55') - self.assertIsNone(host) - print('DeleteHostConfig:\n' + str(DHCP_CONFIG)) - - def test_resolve_config_with_hosts(self): - DHCP_CONFIG.add_reserved_host('test','00:11:22:33:44:55','192.168.10.5') - config_with_hosts = DHCPConfig() - config_with_hosts.make(str(DHCP_CONFIG)) - host = config_with_hosts.get_reserved_host('00:11:22:33:44:55') - self.assertIsNotNone(host) - print("ResolveConfigWithHosts:\n" + str(config_with_hosts)) + @classmethod + def setUpClass(cls): + # Resolve the config + global DHCP_CONFIG + DHCP_CONFIG = get_config() + + def test_resolve_config(self): + print('Test Resolve Config:\n' + str(DHCP_CONFIG)) + + # Resolve the raw config file + with open(get_config_file_path(), 'r', encoding='UTF-8') as f: + lines = f.readlines() + + # Get the resolved config as a + conf_parts = str(DHCP_CONFIG).split('\n') + + # dhcpd conf is not picky about spacing so we just + # need to check contents of each line for matching + # to make sure evertying matches + for i in range(len(lines)): + self.assertEqual(lines[i].strip(), conf_parts[i].strip()) + + def test_disable_failover(self): + DHCP_CONFIG.disable_failover() + print('Test Disable Config:\n' + str(DHCP_CONFIG)) + config_lines = str(DHCP_CONFIG._peer).split('\n') + for line in config_lines: + self.assertTrue(line.startswith('#')) + + def test_enable_failover(self): + DHCP_CONFIG.enable_failover() + print('Test Enable Config:\n' + str(DHCP_CONFIG)) + config_lines = str(DHCP_CONFIG._peer).split('\n') + for line in config_lines: + self.assertFalse(line.startswith('#')) + + def test_add_reserved_host(self): + DHCP_CONFIG.add_reserved_host('test', '00:11:22:33:44:55', '192.168.10.5') + host = DHCP_CONFIG.get_reserved_host('00:11:22:33:44:55') + self.assertIsNotNone(host) + print('AddHostConfig:\n' + str(DHCP_CONFIG)) + + def test_delete_reserved_host(self): + DHCP_CONFIG.delete_reserved_host('00:11:22:33:44:55') + host = DHCP_CONFIG.get_reserved_host('00:11:22:33:44:55') + self.assertIsNone(host) + print('DeleteHostConfig:\n' + str(DHCP_CONFIG)) + + def test_resolve_config_with_hosts(self): + DHCP_CONFIG.add_reserved_host('test', '00:11:22:33:44:55', '192.168.10.5') + config_with_hosts = DHCPConfig() + config_with_hosts.make(str(DHCP_CONFIG)) + host = config_with_hosts.get_reserved_host('00:11:22:33:44:55') + self.assertIsNotNone(host) + print('ResolveConfigWithHosts:\n' + str(config_with_hosts)) + if __name__ == '__main__': - suite = unittest.TestSuite() - suite.addTest(DHCPConfigTest('test_resolve_config')) - suite.addTest(DHCPConfigTest('test_disable_failover')) - suite.addTest(DHCPConfigTest('test_enable_failover')) - suite.addTest(DHCPConfigTest('test_add_reserved_host')) - suite.addTest(DHCPConfigTest('test_delete_reserved_host')) - suite.addTest(DHCPConfigTest('test_resolve_config_with_hosts')) - - runner = unittest.TextTestRunner() - runner.run(suite) \ No newline at end of file + suite = unittest.TestSuite() + suite.addTest(DHCPConfigTest('test_resolve_config')) + suite.addTest(DHCPConfigTest('test_disable_failover')) + suite.addTest(DHCPConfigTest('test_enable_failover')) + suite.addTest(DHCPConfigTest('test_add_reserved_host')) + suite.addTest(DHCPConfigTest('test_delete_reserved_host')) + suite.addTest(DHCPConfigTest('test_resolve_config_with_hosts')) + + runner = unittest.TextTestRunner() + runner.run(suite) 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 2f67b0c2d..bc64a868e 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 @@ -13,6 +13,7 @@ # limitations under the License. """Contains all the necessary classes to maintain the DHCP server""" +import sys import time from common import logger from common import util @@ -35,78 +36,78 @@ def __init__(self): self.dhcp_config.resolve_config() def restart(self): - LOGGER.info("Restarting DHCP Server") - isc_started = util.run_command("service isc-dhcp-server restart", False) + LOGGER.info('Restarting DHCP Server') + isc_started = util.run_command('service isc-dhcp-server restart', False) radvd_started = self.radvd.restart() started = isc_started and radvd_started - LOGGER.info("DHCP Restarted: " + str(started)) + LOGGER.info('DHCP Restarted: ' + str(started)) return started def start(self): - LOGGER.info("Starting DHCP Server") - isc_started = util.run_command("service isc-dhcp-server start", False) + LOGGER.info('Starting DHCP Server') + isc_started = util.run_command('service isc-dhcp-server start', False) radvd_started = self.radvd.start() started = isc_started and radvd_started - LOGGER.info("DHCP Started: " + str(started)) + LOGGER.info('DHCP Started: ' + str(started)) return started def stop(self): - LOGGER.info("Stopping DHCP Server") - isc_stopped = util.run_command("service isc-dhcp-server stop", False) + LOGGER.info('Stopping DHCP Server') + isc_stopped = util.run_command('service isc-dhcp-server stop', False) radvd_stopped = self.radvd.stop() stopped = isc_stopped and radvd_stopped - LOGGER.info("DHCP Stopped: " + str(stopped)) + LOGGER.info('DHCP Stopped: ' + str(stopped)) return stopped 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.' + 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.' radvd_running = self.radvd.is_running() running = isc_running and radvd_running - LOGGER.info("DHCP Status: " + str(running)) + LOGGER.info('DHCP Status: ' + str(running)) return running def boot(self): - LOGGER.info("Booting DHCP Server") + LOGGER.info('Booting DHCP Server') isc_booted = False radvd_booted = False if self.is_running(): - LOGGER.info("Stopping isc-dhcp-server") + LOGGER.info('Stopping isc-dhcp-server') stopped = self.stop() - LOGGER.info("isc-dhcp-server stopped: " + str(stopped)) + LOGGER.info('isc-dhcp-server stopped: ' + str(stopped)) if self.radvd.is_running(): - LOGGER.info("Stopping RADVD") + LOGGER.info('Stopping RADVD') stopped = self.radvd.stop() - LOGGER.info("radvd stopped: " + str(stopped)) + LOGGER.info('radvd stopped: ' + str(stopped)) - LOGGER.info("Starting isc-dhcp-server") + LOGGER.info('Starting isc-dhcp-server') if self.start(): isc_booted = False # Scan for 5 seconds if not yet ready - for i in range(5): + for _ in range(5): time.sleep(1) isc_booted = self.is_running() if isc_booted: - break; - LOGGER.info("isc-dhcp-server started: " + str(isc_booted)) + break + LOGGER.info('isc-dhcp-server started: ' + str(isc_booted)) - LOGGER.info("Starting RADVD") + LOGGER.info('Starting RADVD') if self.radvd.start(): radvd_booted = False # Scan for 5 seconds if not yet ready - for i in range(5): + for _ in range(5): time.sleep(1) radvd_booted = self.radvd.is_running() if radvd_booted: - break; - LOGGER.info("RADVD started: " + str(radvd_booted)) - - + break + LOGGER.info('RADVD started: ' + str(radvd_booted)) return isc_booted and radvd_booted + def run(): dhcp_server = DHCPServer() booted = dhcp_server.boot() @@ -117,14 +118,15 @@ def run(): config = str(dhcp_server.dhcp_config) while True: - dhcp_server.dhcp_config.resolve_config() - new_config = str(dhcp_server.dhcp_config) - if config != new_config: - LOGGER.info("DHCP Config Changed") - config = new_config - success = dhcp_server.restart() - success = dhcp_server.radvd.restart() - time.sleep(1) + dhcp_server.dhcp_config.resolve_config() + new_config = str(dhcp_server.dhcp_config) + if config != new_config: + LOGGER.info('DHCP Config Changed') + config = new_config + dhcp_server.restart() + 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 a693ac3a1..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""" @@ -47,7 +48,7 @@ def RestartDHCPServer(self, request, context): # pylint: disable=W0613 started = self._dhcp_server.restart() LOGGER.info('DHCP server restarted: ' + (str(started))) return pb2.Response(code=200, message='{}') - except Exception as e: # pylint: disable=W0718 + except Exception as e: # pylint: disable=W0718 fail_message = 'Failed to restart DHCP server: ' + str(e) LOGGER.error(fail_message) LOGGER.error(traceback.format_exc()) @@ -59,7 +60,7 @@ def StartDHCPServer(self, request, context): # pylint: disable=W0613 started = self._dhcp_server.start() LOGGER.info('DHCP server started: ' + (str(started))) return pb2.Response(code=200, message='{}') - except Exception as e: # pylint: disable=W0718 + except Exception as e: # pylint: disable=W0718 fail_message = 'Failed to start DHCP server: ' + str(e) LOGGER.error(fail_message) LOGGER.error(traceback.format_exc()) @@ -71,12 +72,12 @@ def StopDHCPServer(self, request, context): # pylint: disable=W0613 stopped = self._dhcp_server.stop() LOGGER.info('DHCP server stopped: ' + (str(stopped))) return pb2.Response(code=200, message='{}') - except Exception as e: # pylint: disable=W0718 + except Exception as e: # pylint: disable=W0718 fail_message = 'Failed to stop DHCP server: ' + str(e) LOGGER.error(fail_message) LOGGER.error(traceback.format_exc()) return pb2.Response(code=500, message=fail_message) - + def AddReservedLease(self, request, context): # pylint: disable=W0613 LOGGER.info('Add reserved lease called') try: @@ -86,7 +87,7 @@ def AddReservedLease(self, request, context): # pylint: disable=W0613 dhcp_config.write_config() LOGGER.info('Reserved lease added') return pb2.Response(code=200, message='{}') - except Exception as e: # pylint: disable=W0718 + except Exception as e: # pylint: disable=W0718 fail_message = 'Failed to add reserved lease: ' + str(e) LOGGER.error(fail_message) LOGGER.error(traceback.format_exc()) @@ -100,7 +101,7 @@ def DeleteReservedLease(self, request, context): # pylint: disable=W0613 dhcp_config.write_config() LOGGER.info('Reserved lease deleted') return pb2.Response(code=200, message='{}') - except Exception as e: # pylint: disable=W0718 + except Exception as e: # pylint: disable=W0718 fail_message = 'Failed to delete reserved lease: ' + str(e) LOGGER.error(fail_message) LOGGER.error(traceback.format_exc()) @@ -114,7 +115,7 @@ def DisableFailover(self, request, contest): # pylint: disable=W0613 dhcp_config.write_config() LOGGER.info('Failover disabled') return pb2.Response(code=200, message='{}') - except Exception as e: # pylint: disable=W0718 + except Exception as e: # pylint: disable=W0718 fail_message = 'Failed to disable failover: ' + str(e) LOGGER.error(fail_message) LOGGER.error(traceback.format_exc()) @@ -128,7 +129,7 @@ def EnableFailover(self, request, contest): # pylint: disable=W0613 dhcp_config.write_config() LOGGER.info('Failover enabled') return pb2.Response(code=200, message='{}') - except Exception as e: # pylint: disable=W0718 + except Exception as e: # pylint: disable=W0718 fail_message = 'Failed to enable failover: ' + str(e) LOGGER.error(fail_message) LOGGER.error(traceback.format_exc()) @@ -143,7 +144,7 @@ def GetDHCPRange(self, request, context): # pylint: disable=W0613 try: pool = self._get_dhcp_config()._subnets[0].pools[0] return pb2.DHCPRange(code=200, start=pool.range_start, end=pool.range_end) - except Exception as e: # pylint: disable=W0718 + except Exception as e: # pylint: disable=W0718 fail_message = 'Failed to get DHCP range: ' + str(e) LOGGER.error(fail_message) LOGGER.error(traceback.format_exc()) @@ -161,7 +162,7 @@ def GetLease(self, request, context): # pylint: disable=W0613 return pb2.Response(code=200, message=str(lease)) else: return pb2.Response(code=200, message='{}') - except Exception as e: # pylint: disable=W0718 + except Exception as e: # pylint: disable=W0718 fail_message = 'Failed to get lease: ' + str(e) LOGGER.error(fail_message) LOGGER.error(traceback.format_exc()) @@ -179,7 +180,7 @@ def SetDHCPRange(self, request, context): # pylint: disable=W0613 dhcp_config.write_config() LOGGER.info('DHCP range set') return pb2.Response(code=200, message='DHCP Range Set') - except Exception as e: # pylint: disable=W0718 + except Exception as e: # pylint: disable=W0718 fail_message = 'Failed to set DHCP range: ' + str(e) LOGGER.error(fail_message) LOGGER.error(traceback.format_exc()) @@ -191,4 +192,4 @@ def GetStatus(self, request, context): # pylint: disable=W0613 """ dhcp_status = self._dhcp_server.is_running() message = str({'dhcpStatus': dhcp_status}) - return pb2.Response(code=200, message=message) \ No newline at end of file + return pb2.Response(code=200, message=message) diff --git a/modules/network/dhcp-1/python/src/grpc_server/radvd_server.py b/modules/network/dhcp-1/python/src/grpc_server/radvd_server.py index 48e063e61..8bb1d0539 100644 --- a/modules/network/dhcp-1/python/src/grpc_server/radvd_server.py +++ b/modules/network/dhcp-1/python/src/grpc_server/radvd_server.py @@ -13,10 +13,8 @@ # limitations under the License. """Contains all the necessary classes to maintain the DHCP server""" -import time from common import logger from common import util -from dhcp_config import DHCPConfig CONFIG_FILE = '/etc/dhcp/dhcpd.conf' LOG_NAME = 'radvd' @@ -31,25 +29,25 @@ def __init__(self): LOGGER = logger.get_logger(LOG_NAME, 'dhcp-1') def restart(self): - LOGGER.info("Restarting RADVD Server") - response = util.run_command("radvd-service restart", False) - LOGGER.info("RADVD Restarted: " + str(response)) + LOGGER.info('Restarting RADVD Server') + response = util.run_command('radvd-service restart', False) + LOGGER.info('RADVD Restarted: ' + str(response)) return response def start(self): - LOGGER.info("Starting RADVD Server") - response = util.run_command("radvd-service start", False) - LOGGER.info("RADVD Started: " + str(response)) + LOGGER.info('Starting RADVD Server') + response = util.run_command('radvd-service start', False) + LOGGER.info('RADVD Started: ' + str(response)) return response def stop(self): - LOGGER.info("Stopping RADVD Server") - response = util.run_command("radvd-service stop", False) - LOGGER.info("RADVD Stopped: " + str(response)) + LOGGER.info('Stopping RADVD Server') + response = util.run_command('radvd-service stop', False) + LOGGER.info('RADVD Stopped: ' + str(response)) return response def is_running(self): - LOGGER.info("Checking RADVD Status") - response = util.run_command("radvd-service status") - LOGGER.info("RADVD Status: " + str(response)) + LOGGER.info('Checking RADVD Status') + response = util.run_command('radvd-service status') + LOGGER.info('RADVD Status: ' + str(response)) return response[0] == 'radvd service is running.' 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 33cb5938c..5da5e4cf2 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 @@ -186,13 +186,18 @@ def __str__(self): config += '\tprimary;' if self.primary else 'secondary;' config += '\n\t{ADDRESS_KEY} {ADDRESS};' if self.address is not None else '' config += '\n\t{PORT_KEY} {PORT};' if self.port is not None else '' - config += '\n\t{PEER_ADDRESS_KEY} {PEER_ADDRESS};' if self.peer_address is not None else '' - config += '\n\t{PEER_PORT_KEY} {PEER_PORT};' if self.peer_port is not None else '' - config += '\n\t{MAX_RESPONSE_DELAY_KEY} {MAX_RESPONSE_DELAY};' if self.max_response_delay is not None else '' - config += '\n\t{MAX_UNACKED_UPDATES_KEY} {MAX_UNACKED_UPDATES};' if self.max_unacked_updates is not None else '' + config += ('\n\t{PEER_ADDRESS_KEY} {PEER_ADDRESS};' + if self.peer_address is not None else '') + config += ('\n\t{PEER_PORT_KEY} {PEER_PORT};' + if self.peer_port is not None else '') + config += ('\n\t{MAX_RESPONSE_DELAY_KEY} {MAX_RESPONSE_DELAY};' + if self.max_response_delay is not None else '') + config += ('\n\t{MAX_UNACKED_UPDATES_KEY} {MAX_UNACKED_UPDATES};' + if self.max_unacked_updates is not None else '') config += '\n\t{MCLT_KEY} {MCLT};' if self.mclt is not None else '' config += '\n\t{SPLIT_KEY} {SPLIT};' if self.split is not None else '' - config += '\n\t{LOAD_BALANCE_MAX_SECONDS_KEY} {LOAD_BALANCE_MAX_SECONDS};' if self.load_balance_max_seconds is not None else '' + config += ('\n\t{LOAD_BALANCE_MAX_SECONDS_KEY} {LOAD_BALANCE_MAX_SECONDS};' + if self.load_balance_max_seconds is not None else '') config += '\n\r}}' config = config.format( @@ -220,9 +225,9 @@ def __str__(self): if not self.enabled: lines = config.strip().split('\n') - for i in range(len(lines)-1): + for i in range(len(lines) - 1): lines[i] = '#' + lines[i] - lines[-1] = '#' + lines[-1].strip() # Handle the last line separately + lines[-1] = '#' + lines[-1].strip() # Handle the last line separately config = '\n'.join(lines) return config @@ -302,15 +307,20 @@ def __init__(self, subnet): def __str__(self): config = 'subnet {SUBNET_OPTION} netmask {SUBNET_MASK_OPTION} {{' - config += '\n\t{NTP_OPTION_KEY} {NTP_OPTION};' if self._ntp_servers is not None else '' - config += '\n\t{SUBNET_MASK_OPTION_KEY} {SUBNET_MASK_OPTION};' if self._subnet_mask is not None else '' - config += '\n\t{BROADCAST_OPTION_KEY} {BROADCAST_OPTION};' if self._broadcast is not None else '' - config += '\n\t{ROUTER_OPTION_KEY} {ROUTER_OPTION};' if self._routers is not None else '' - config += '\n\t{DNS_OPTION_KEY} {DNS_OPTION};' if self._dns_servers is not None else '' - config += '\n\t{INTERFACE_KEY} {INTERFACE_OPTION};' if self._interface is not None else '' + config += ('\n\t{NTP_OPTION_KEY} {NTP_OPTION};' + if self._ntp_servers is not None else '') + config += ('\n\t{SUBNET_MASK_OPTION_KEY} {SUBNET_MASK_OPTION};' + if self._subnet_mask is not None else '') + config += ('\n\t{BROADCAST_OPTION_KEY} {BROADCAST_OPTION};' + if self._broadcast is not None else '') + config += ('\n\t{ROUTER_OPTION_KEY} {ROUTER_OPTION};' + if self._routers is not None else '') + config += ('\n\t{DNS_OPTION_KEY} {DNS_OPTION};' + if self._dns_servers is not None else '') + config += ('\n\t{INTERFACE_KEY} {INTERFACE_OPTION};' + if self._interface is not None else '') config += '\n\t{AUTHORITATIVE_KEY};' if self._authoritative else '' - config = config.format(length='multi-line', SUBNET_OPTION=self._subnet, NTP_OPTION_KEY=NTP_OPTION_KEY, @@ -407,8 +417,11 @@ def __init__(self, pool): def __str__(self): config = 'pool {{' - config += '\n\t\t{FAILOVER_KEY} "{FAILOVER}";' if self.failover_peer is not None else '' - config += '\n\t\t{RANGE_KEY} {RANGE_START} {RANGE_END};' if self.range_start is not None and self.range_end is not None else '' + config += ('\n\t\t{FAILOVER_KEY} "{FAILOVER}";' + if self.failover_peer is not None else '') + config += ('\n\t\t{RANGE_KEY} {RANGE_START} {RANGE_END};' + if self.range_start is not None and self.range_end is not None + else '') config += '\n\t}}' config = config.format( @@ -490,4 +503,4 @@ def resolve_host(self, reserved_host): self.hw_addr = part.strip().split(HARDWARE_KEY)[1].strip().split(';')[0] elif FIXED_ADDRESS_KEY in part: self.fixed_addr = part.strip().split( - FIXED_ADDRESS_KEY)[1].strip().split(';')[0] \ No newline at end of file + FIXED_ADDRESS_KEY)[1].strip().split(';')[0] 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 2cc78403a..d9a0b0190 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 @@ -20,84 +20,88 @@ DHCP_CONFIG = None + def get_config_file_path(): - dhcp_config = DHCPConfig() - current_dir = os.path.dirname(os.path.abspath(__file__)) - module_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(current_dir)))) - conf_file = os.path.join(module_dir,CONFIG_FILE) - return conf_file + current_dir = os.path.dirname(os.path.abspath(__file__)) + module_dir = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(current_dir)))) + 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 + dhcp_config = DHCPConfig() + dhcp_config.resolve_config(get_config_file_path()) + return dhcp_config + class DHCPConfigTest(unittest.TestCase): - @classmethod - def setUpClass(cls): - # Resolve the config - global DHCP_CONFIG - DHCP_CONFIG = get_config() - - def test_resolve_config(self): - print('Test Resolve Config:\n' + str(DHCP_CONFIG)) - - # Resolve the raw config file - with open(get_config_file_path(),'r') as f: - lines = f.readlines() - - # Get the resolved config as a - conf_parts = str(DHCP_CONFIG).split('\n') - - # dhcpd conf is not picky about spacing so we just - # need to check contents of each line for matching - # to make sure evertying matches - for i in range(len(lines)): - self.assertEqual(lines[i].strip(),conf_parts[i].strip()) - - def test_disable_failover(self): - DHCP_CONFIG.disable_failover() - print('Test Disable Config:\n' + str(DHCP_CONFIG)) - config_lines = str(DHCP_CONFIG._peer).split('\n') - for line in config_lines: - self.assertTrue(line.startswith('#')) - - def test_enable_failover(self): - DHCP_CONFIG.enable_failover() - print('Test Enable Config:\n' + str(DHCP_CONFIG)) - config_lines = str(DHCP_CONFIG._peer).split('\n') - for line in config_lines: - self.assertFalse(line.startswith('#')) - - def test_add_reserved_host(self): - DHCP_CONFIG.add_reserved_host('test','00:11:22:33:44:55','192.168.10.5') - host = DHCP_CONFIG.get_reserved_host('00:11:22:33:44:55') - self.assertIsNotNone(host) - print('AddHostConfig:\n' + str(DHCP_CONFIG)) - - def test_delete_reserved_host(self): - DHCP_CONFIG.delete_reserved_host('00:11:22:33:44:55') - host = DHCP_CONFIG.get_reserved_host('00:11:22:33:44:55') - self.assertIsNone(host) - print('DeleteHostConfig:\n' + str(DHCP_CONFIG)) - - def test_resolve_config_with_hosts(self): - DHCP_CONFIG.add_reserved_host('test','00:11:22:33:44:55','192.168.10.5') - config_with_hosts = DHCPConfig() - config_with_hosts.make(str(DHCP_CONFIG)) - host = config_with_hosts.get_reserved_host('00:11:22:33:44:55') - self.assertIsNotNone(host) - print("ResolveConfigWithHosts:\n" + str(config_with_hosts)) + @classmethod + def setUpClass(cls): + # Resolve the config + global DHCP_CONFIG + DHCP_CONFIG = get_config() + + def test_resolve_config(self): + print('Test Resolve Config:\n' + str(DHCP_CONFIG)) + + # Resolve the raw config file + with open(get_config_file_path(), 'r', encoding='UTF-8') as f: + lines = f.readlines() + + # Get the resolved config as a + conf_parts = str(DHCP_CONFIG).split('\n') + + # dhcpd conf is not picky about spacing so we just + # need to check contents of each line for matching + # to make sure evertying matches + for i in range(len(lines)): + self.assertEqual(lines[i].strip(), conf_parts[i].strip()) + + def test_disable_failover(self): + DHCP_CONFIG.disable_failover() + print('Test Disable Config:\n' + str(DHCP_CONFIG)) + config_lines = str(DHCP_CONFIG._peer).split('\n') + for line in config_lines: + self.assertTrue(line.startswith('#')) + + def test_enable_failover(self): + DHCP_CONFIG.enable_failover() + print('Test Enable Config:\n' + str(DHCP_CONFIG)) + config_lines = str(DHCP_CONFIG._peer).split('\n') + for line in config_lines: + self.assertFalse(line.startswith('#')) + + def test_add_reserved_host(self): + DHCP_CONFIG.add_reserved_host('test', '00:11:22:33:44:55', '192.168.10.5') + host = DHCP_CONFIG.get_reserved_host('00:11:22:33:44:55') + self.assertIsNotNone(host) + print('AddHostConfig:\n' + str(DHCP_CONFIG)) + + def test_delete_reserved_host(self): + DHCP_CONFIG.delete_reserved_host('00:11:22:33:44:55') + host = DHCP_CONFIG.get_reserved_host('00:11:22:33:44:55') + self.assertIsNone(host) + print('DeleteHostConfig:\n' + str(DHCP_CONFIG)) + + def test_resolve_config_with_hosts(self): + DHCP_CONFIG.add_reserved_host('test', '00:11:22:33:44:55', '192.168.10.5') + config_with_hosts = DHCPConfig() + config_with_hosts.make(str(DHCP_CONFIG)) + host = config_with_hosts.get_reserved_host('00:11:22:33:44:55') + self.assertIsNotNone(host) + print('ResolveConfigWithHosts:\n' + str(config_with_hosts)) + if __name__ == '__main__': - suite = unittest.TestSuite() - suite.addTest(DHCPConfigTest('test_resolve_config')) - suite.addTest(DHCPConfigTest('test_disable_failover')) - suite.addTest(DHCPConfigTest('test_enable_failover')) - suite.addTest(DHCPConfigTest('test_add_reserved_host')) - suite.addTest(DHCPConfigTest('test_delete_reserved_host')) - suite.addTest(DHCPConfigTest('test_resolve_config_with_hosts')) - - runner = unittest.TextTestRunner() - runner.run(suite) \ No newline at end of file + suite = unittest.TestSuite() + suite.addTest(DHCPConfigTest('test_resolve_config')) + suite.addTest(DHCPConfigTest('test_disable_failover')) + suite.addTest(DHCPConfigTest('test_enable_failover')) + suite.addTest(DHCPConfigTest('test_add_reserved_host')) + suite.addTest(DHCPConfigTest('test_delete_reserved_host')) + suite.addTest(DHCPConfigTest('test_resolve_config_with_hosts')) + + runner = unittest.TextTestRunner() + runner.run(suite) 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 1431d6ddd..93f88c376 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 @@ -13,6 +13,7 @@ # limitations under the License. """Contains all the necessary classes to maintain the DHCP server""" +import sys import time from common import logger from common import util @@ -35,78 +36,78 @@ def __init__(self): self.dhcp_config.resolve_config() def restart(self): - LOGGER.info("Restarting DHCP Server") - isc_started = util.run_command("service isc-dhcp-server restart", False) + LOGGER.info('Restarting DHCP Server') + isc_started = util.run_command('service isc-dhcp-server restart', False) radvd_started = self.radvd.restart() started = isc_started and radvd_started - LOGGER.info("DHCP Restarted: " + str(started)) + LOGGER.info('DHCP Restarted: ' + str(started)) return started def start(self): - LOGGER.info("Starting DHCP Server") - isc_started = util.run_command("service isc-dhcp-server start", False) + LOGGER.info('Starting DHCP Server') + isc_started = util.run_command('service isc-dhcp-server start', False) radvd_started = self.radvd.start() started = isc_started and radvd_started - LOGGER.info("DHCP Started: " + str(started)) + LOGGER.info('DHCP Started: ' + str(started)) return started def stop(self): - LOGGER.info("Stopping DHCP Server") - isc_stopped = util.run_command("service isc-dhcp-server stop", False) + LOGGER.info('Stopping DHCP Server') + isc_stopped = util.run_command('service isc-dhcp-server stop', False) radvd_stopped = self.radvd.stop() stopped = isc_stopped and radvd_stopped - LOGGER.info("DHCP Stopped: " + str(stopped)) + LOGGER.info('DHCP Stopped: ' + str(stopped)) return stopped 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.' + 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.' radvd_running = self.radvd.is_running() running = isc_running and radvd_running - LOGGER.info("DHCP Status: " + str(running)) + LOGGER.info('DHCP Status: ' + str(running)) return running def boot(self): - LOGGER.info("Booting DHCP Server") + LOGGER.info('Booting DHCP Server') isc_booted = False radvd_booted = False if self.is_running(): - LOGGER.info("Stopping isc-dhcp-server") + LOGGER.info('Stopping isc-dhcp-server') stopped = self.stop() - LOGGER.info("isc-dhcp-server stopped: " + str(stopped)) + LOGGER.info('isc-dhcp-server stopped: ' + str(stopped)) if self.radvd.is_running(): - LOGGER.info("Stopping RADVD") + LOGGER.info('Stopping RADVD') stopped = self.radvd.stop() - LOGGER.info("radvd stopped: " + str(stopped)) + LOGGER.info('radvd stopped: ' + str(stopped)) - LOGGER.info("Starting isc-dhcp-server") + LOGGER.info('Starting isc-dhcp-server') if self.start(): isc_booted = False # Scan for 5 seconds if not yet ready - for i in range(5): + for _ in range(5): time.sleep(1) isc_booted = self.is_running() if isc_booted: - break; - LOGGER.info("isc-dhcp-server started: " + str(isc_booted)) + break + LOGGER.info('isc-dhcp-server started: ' + str(isc_booted)) - LOGGER.info("Starting RADVD") + LOGGER.info('Starting RADVD') if self.radvd.start(): radvd_booted = False # Scan for 5 seconds if not yet ready - for i in range(5): + for _ in range(5): time.sleep(1) radvd_booted = self.radvd.is_running() if radvd_booted: - break; - LOGGER.info("RADVD started: " + str(radvd_booted)) - - + break + LOGGER.info('RADVD started: ' + str(radvd_booted)) return isc_booted and radvd_booted + def run(): dhcp_server = DHCPServer() booted = dhcp_server.boot() @@ -117,14 +118,15 @@ def run(): config = str(dhcp_server.dhcp_config) while True: - dhcp_server.dhcp_config.resolve_config() - new_config = str(dhcp_server.dhcp_config) - if config != new_config: - LOGGER.info("DHCP Config Changed") - config = new_config - success = dhcp_server.restart() - success = dhcp_server.radvd.restart() - time.sleep(1) + dhcp_server.dhcp_config.resolve_config() + new_config = str(dhcp_server.dhcp_config) + if config != new_config: + LOGGER.info('DHCP Config Changed') + config = new_config + dhcp_server.restart() + dhcp_server.radvd.restart() + time.sleep(1) + if __name__ == '__main__': run() diff --git a/modules/network/dhcp-2/python/src/grpc_server/network_service.py b/modules/network/dhcp-2/python/src/grpc_server/network_service.py index 5af9e6c44..f9deba965 100644 --- a/modules/network/dhcp-2/python/src/grpc_server/network_service.py +++ b/modules/network/dhcp-2/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""" @@ -47,7 +48,7 @@ def RestartDHCPServer(self, request, context): # pylint: disable=W0613 started = self._dhcp_server.restart() LOGGER.info('DHCP server restarted: ' + (str(started))) return pb2.Response(code=200, message='{}') - except Exception as e: # pylint: disable=W0718 + except Exception as e: # pylint: disable=W0718 fail_message = 'Failed to restart DHCP server: ' + str(e) LOGGER.error(fail_message) LOGGER.error(traceback.format_exc()) @@ -59,7 +60,7 @@ def StartDHCPServer(self, request, context): # pylint: disable=W0613 started = self._dhcp_server.start() LOGGER.info('DHCP server started: ' + (str(started))) return pb2.Response(code=200, message='{}') - except Exception as e: # pylint: disable=W0718 + except Exception as e: # pylint: disable=W0718 fail_message = 'Failed to start DHCP server: ' + str(e) LOGGER.error(fail_message) LOGGER.error(traceback.format_exc()) @@ -71,12 +72,12 @@ def StopDHCPServer(self, request, context): # pylint: disable=W0613 stopped = self._dhcp_server.stop() LOGGER.info('DHCP server stopped: ' + (str(stopped))) return pb2.Response(code=200, message='{}') - except Exception as e: # pylint: disable=W0718 + except Exception as e: # pylint: disable=W0718 fail_message = 'Failed to stop DHCP server: ' + str(e) LOGGER.error(fail_message) LOGGER.error(traceback.format_exc()) return pb2.Response(code=500, message=fail_message) - + def AddReservedLease(self, request, context): # pylint: disable=W0613 LOGGER.info('Add reserved lease called') try: @@ -86,7 +87,7 @@ def AddReservedLease(self, request, context): # pylint: disable=W0613 dhcp_config.write_config() LOGGER.info('Reserved lease added') return pb2.Response(code=200, message='{}') - except Exception as e: # pylint: disable=W0718 + except Exception as e: # pylint: disable=W0718 fail_message = 'Failed to add reserved lease: ' + str(e) LOGGER.error(fail_message) LOGGER.error(traceback.format_exc()) @@ -100,7 +101,7 @@ def DeleteReservedLease(self, request, context): # pylint: disable=W0613 dhcp_config.write_config() LOGGER.info('Reserved lease deleted') return pb2.Response(code=200, message='{}') - except Exception as e: # pylint: disable=W0718 + except Exception as e: # pylint: disable=W0718 fail_message = 'Failed to delete reserved lease: ' + str(e) LOGGER.error(fail_message) LOGGER.error(traceback.format_exc()) @@ -114,7 +115,7 @@ def DisableFailover(self, request, contest): # pylint: disable=W0613 dhcp_config.write_config() LOGGER.info('Failover disabled') return pb2.Response(code=200, message='{}') - except Exception as e: # pylint: disable=W0718 + except Exception as e: # pylint: disable=W0718 fail_message = 'Failed to disable failover: ' + str(e) LOGGER.error(fail_message) LOGGER.error(traceback.format_exc()) @@ -128,7 +129,7 @@ def EnableFailover(self, request, contest): # pylint: disable=W0613 dhcp_config.write_config() LOGGER.info('Failover enabled') return pb2.Response(code=200, message='{}') - except Exception as e: # pylint: disable=W0718 + except Exception as e: # pylint: disable=W0718 fail_message = 'Failed to enable failover: ' + str(e) LOGGER.error(fail_message) LOGGER.error(traceback.format_exc()) @@ -143,7 +144,7 @@ def GetDHCPRange(self, request, context): # pylint: disable=W0613 try: pool = self._get_dhcp_config()._subnets[0].pools[0] return pb2.DHCPRange(code=200, start=pool.range_start, end=pool.range_end) - except Exception as e: # pylint: disable=W0718 + except Exception as e: # pylint: disable=W0718 fail_message = 'Failed to get DHCP range: ' + str(e) LOGGER.error(fail_message) LOGGER.error(traceback.format_exc()) @@ -161,7 +162,7 @@ def GetLease(self, request, context): # pylint: disable=W0613 return pb2.Response(code=200, message=str(lease)) else: return pb2.Response(code=200, message='{}') - except Exception as e: # pylint: disable=W0718 + except Exception as e: # pylint: disable=W0718 fail_message = 'Failed to get lease: ' + str(e) LOGGER.error(fail_message) LOGGER.error(traceback.format_exc()) @@ -179,7 +180,7 @@ def SetDHCPRange(self, request, context): # pylint: disable=W0613 dhcp_config.write_config() LOGGER.info('DHCP range set') return pb2.Response(code=200, message='DHCP Range Set') - except Exception as e: # pylint: disable=W0718 + except Exception as e: # pylint: disable=W0718 fail_message = 'Failed to set DHCP range: ' + str(e) LOGGER.error(fail_message) LOGGER.error(traceback.format_exc()) @@ -191,4 +192,4 @@ def GetStatus(self, request, context): # pylint: disable=W0613 """ dhcp_status = self._dhcp_server.is_running() message = str({'dhcpStatus': dhcp_status}) - return pb2.Response(code=200, message=message) \ No newline at end of file + return pb2.Response(code=200, message=message) diff --git a/modules/network/dhcp-2/python/src/grpc_server/radvd_server.py b/modules/network/dhcp-2/python/src/grpc_server/radvd_server.py index 0c6ef90d6..bc5d8b55f 100644 --- a/modules/network/dhcp-2/python/src/grpc_server/radvd_server.py +++ b/modules/network/dhcp-2/python/src/grpc_server/radvd_server.py @@ -13,10 +13,8 @@ # limitations under the License. """Contains all the necessary classes to maintain the DHCP server""" -import time from common import logger from common import util -from dhcp_config import DHCPConfig CONFIG_FILE = '/etc/dhcp/dhcpd.conf' LOG_NAME = 'radvd' @@ -31,25 +29,25 @@ def __init__(self): LOGGER = logger.get_logger(LOG_NAME, 'dhcp-2') def restart(self): - LOGGER.info("Restarting RADVD Server") - response = util.run_command("radvd-service restart", False) - LOGGER.info("RADVD Restarted: " + str(response)) + LOGGER.info('Restarting RADVD Server') + response = util.run_command('radvd-service restart', False) + LOGGER.info('RADVD Restarted: ' + str(response)) return response def start(self): - LOGGER.info("Starting RADVD Server") - response = util.run_command("radvd-service start", False) - LOGGER.info("RADVD Started: " + str(response)) + LOGGER.info('Starting RADVD Server') + response = util.run_command('radvd-service start', False) + LOGGER.info('RADVD Started: ' + str(response)) return response def stop(self): - LOGGER.info("Stopping RADVD Server") - response = util.run_command("radvd-service stop", False) - LOGGER.info("RADVD Stopped: " + str(response)) + LOGGER.info('Stopping RADVD Server') + response = util.run_command('radvd-service stop', False) + LOGGER.info('RADVD Stopped: ' + str(response)) return response def is_running(self): - LOGGER.info("Checking RADVD Status") - response = util.run_command("radvd-service status") - LOGGER.info("RADVD Status: " + str(response)) + LOGGER.info('Checking RADVD Status') + response = util.run_command('radvd-service status') + LOGGER.info('RADVD Status: ' + str(response)) return response[0] == 'radvd service is running.' diff --git a/modules/test/conn/python/src/connection_module.py b/modules/test/conn/python/src/connection_module.py index 5b3bf7038..fd60bc122 100644 --- a/modules/test/conn/python/src/connection_module.py +++ b/modules/test/conn/python/src/connection_module.py @@ -11,17 +11,17 @@ # 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. - """Connection test module""" import util import sys -from scapy.all import * +import json +from scapy.all import rdpcap, DHCP, Ether from test_module import TestModule from dhcp1.client import Client as DHCPClient1 -LOG_NAME = "test_connection" +LOG_NAME = 'test_connection' LOGGER = None -OUI_FILE="/usr/local/etc/oui.txt" +OUI_FILE = '/usr/local/etc/oui.txt' DHCP_SERVER_CAPTURE_FILE = '/runtime/network/dhcp-1.pcap' STARTUP_CAPTURE_FILE = '/runtime/device/startup.pcap' MONITOR_CAPTURE_FILE = '/runtime/device/monitor.pcap' @@ -35,10 +35,11 @@ def __init__(self, module): global LOGGER LOGGER = self._get_logger() self.dhcp1_client = DHCPClient1() - - # ToDo: Move this into some level of testing, leave for + + # ToDo: Move this into some level of testing, leave for # reference until tests are implemented with these calls - # response = self.dhcp1_client.add_reserved_lease('test','00:11:22:33:44:55','10.10.10.21') + # response = self.dhcp1_client.add_reserved_lease( + # 'test','00:11:22:33:44:55','10.10.10.21') # print("AddLeaseResp: " + str(response)) # response = self.dhcp1_client.delete_reserved_lease('00:11:22:33:44:55') @@ -63,52 +64,53 @@ def __init__(self, module): # print("Set Range: " + str(response)) def _connection_dhcp_address(self): - LOGGER.info("Running connection.dhcp_address") + LOGGER.info('Running connection.dhcp_address') response = self.dhcp1_client.get_lease(self._device_mac) - LOGGER.info("DHCP Lease resolved:\n" + str(response)) + LOGGER.info('DHCP Lease resolved:\n' + str(response)) if response.code == 200: - lease = eval(response.message) + #lease = eval(response.message) + lease = json.loads(response.message) if 'ip' in lease: ip_addr = lease['ip'] - LOGGER.info("IP Resolved: " + ip_addr) - LOGGER.info("Attempting to ping device..."); + LOGGER.info('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('Ping Success: ' + str(ping_success)) if ping_success: - return True, "Device responded to leased ip address" + return True, 'Device responded to leased ip address' else: - return False, "Device did not respond to leased ip address" + return False, 'Device did not respond to leased ip address' else: - LOGGER.info("No DHCP lease found for: " + self._device_mac) - return False, "No DHCP lease found for: " + self._device_mac + LOGGER.info('No DHCP lease found for: ' + self._device_mac) + return False, 'No DHCP lease found for: ' + self._device_mac def _connection_mac_address(self): - LOGGER.info("Running connection.mac_address") + LOGGER.info('Running connection.mac_address') if self._device_mac is not None: - LOGGER.info("MAC address found: " + self._device_mac) - return True, "MAC address found: " + self._device_mac + LOGGER.info('MAC address found: ' + self._device_mac) + return True, 'MAC address found: ' + self._device_mac else: - LOGGER.info("No MAC address found: " + self._device_mac) - return False, "No MAC address found." + LOGGER.info('No MAC address found: ' + self._device_mac) + return False, 'No MAC address found.' def _connection_mac_oui(self): - LOGGER.info("Running connection.mac_oui") + LOGGER.info('Running connection.mac_oui') manufacturer = self._get_oui_manufacturer(self._device_mac) if manufacturer is not None: - LOGGER.info("OUI Manufacturer found: " + manufacturer) - return True, "OUI Manufacturer found: " + manufacturer + LOGGER.info('OUI Manufacturer found: ' + manufacturer) + return True, 'OUI Manufacturer found: ' + manufacturer else: - LOGGER.info("No OUI Manufacturer found for: " + self._device_mac) - return False, "No OUI Manufacturer found for: " + self._device_mac + LOGGER.info('No OUI Manufacturer found for: ' + self._device_mac) + return False, 'No OUI Manufacturer found for: ' + self._device_mac def _connection_single_ip(self): - LOGGER.info("Running connection.single_ip") + LOGGER.info('Running connection.single_ip') result = None if self._device_mac is None: - LOGGER.info("No MAC address found: ") - return result, "No MAC address found." - + LOGGER.info('No MAC address found: ') + return result, 'No MAC address found.' + # Read all the pcap files containing DHCP packet information packets = rdpcap(DHCP_SERVER_CAPTURE_FILE) packets.append(rdpcap(STARTUP_CAPTURE_FILE)) @@ -116,50 +118,47 @@ def _connection_single_ip(self): # Extract MAC addresses from DHCP packets mac_addresses = set() - LOGGER.info("Inspecting: " + str(len(packets)) + " packets") + LOGGER.info('Inspecting: ' + str(len(packets)) + ' packets') for packet in packets: # Option[1] = message-type, option 3 = DHCPREQUEST - if DHCP in packet and packet[DHCP].options[0][1] == 3: - mac_address = packet[Ether].src - mac_addresses.add(mac_address.upper()) + if DHCP in packet and packet[DHCP].options[0][1] == 3: + mac_address = packet[Ether].src + mac_addresses.add(mac_address.upper()) # Check if the device mac address is in the list of DHCPREQUESTs result = self._device_mac.upper() in mac_addresses - LOGGER.info("DHCPREQUEST detected from device: " + str(result)) + LOGGER.info('DHCPREQUEST detected from device: ' + str(result)) # Check the unique MAC addresses to see if they match the device for mac_address in mac_addresses: - LOGGER.info("DHCPREQUEST from MAC address: " + mac_address) - result &= self._device_mac.upper() == mac_address + LOGGER.info('DHCPREQUEST from MAC address: ' + mac_address) + result &= self._device_mac.upper() == mac_address return result - def _connection_target_ping(self): - LOGGER.info("Running connection.target_ping") + LOGGER.info('Running connection.target_ping') # If the ipv4 address wasn't resolved yet, try again if self._device_ipv4_addr is None: - self._device_ipv4_addr = self._get_device_ipv4(self) - + self._device_ipv4_addr = self._get_device_ipv4(self) if self._device_ipv4_addr is None: - LOGGER.error("No device IP could be resolved") + LOGGER.error('No device IP could be resolved') sys.exit(1) else: return self._ping(self._device_ipv4_addr) - def _get_oui_manufacturer(self,mac_address): + def _get_oui_manufacturer(self, mac_address): # Do some quick fixes on the format of the mac_address # to match the oui file pattern - mac_address = mac_address.replace(":","-").upper() - with open(OUI_FILE, "r") as file: - for line in file: - if mac_address.startswith(line[:8]): - start = line.index("(hex)") + len("(hex)") - return line[start:].strip() # Extract the company name + mac_address = mac_address.replace(':', '-').upper() + with open(OUI_FILE, 'r', encoding='UTF-8') as file: + for line in file: + if mac_address.startswith(line[:8]): + start = line.index('(hex)') + len('(hex)') + return line[start:].strip() # Extract the company name return None def _ping(self, host): cmd = 'ping -c 1 ' + str(host) success = util.run_command(cmd, output=False) return success - \ No newline at end of file