Skip to content

Commit

Permalink
Feature/dhcp disconnect (#626)
Browse files Browse the repository at this point in the history
* Initial work on giving host access to test containers

* Change external ip resolving method to use docker0

* Resolve device interface for test containers

* Add gRPC methods to control the interface state
Implement dhcp disconnect test

* Fix host network module option

* Misc cleanup

* Move ip resolving method to IPControl class

* pylint fixes

* refactor func to handle case when network interface not exists

* set test result "Error"

* check device connected

* thread for monitoring device connection

* Minor changes

* check the device connection only before each test

* send testrun status using mqtt

* remove duplicatied line

* refactor setting remaining tests to error

* Pylint fixes

* pylint

* Use mqtt service instead of calling GET /status every 5 seconds. (#644)

* Use mqtt service instead of calling GET /status every 5 seconds.

* Adds tooltip (#638)

Adds tooltip

* Fix focus after profile delete - track by name (#640)

Fix focus after profile delete - track by name

* Update the requests dependency (#643)

* Update requests dependency

* Update requests dependency

* Update dependency in TLS test

* Update docker dependency

---------

Signed-off-by: J Boddey <boddey@google.com>

* remove unused output

* encode mqtt message to json

* Revert "Expired profile (#619)" (#645)

Prevent opening of Expired risk profile

* Improve documentation (#639)

* Improve docs

* Remove paragraph

* Text changes

* Fix text for the BE error

* Change tooltip (#650)

* Change tooltip

* Allows draft profiles to become expired (#636)

* Allow draft profiles to expire

* Move status method into risk profile class

* Use existing method

* Check for expiry in validate method

* Remove unused variable

* Build UI during package instead of install (#621)

* Build UI during package

* Fix local build

* Install npm

* Remove duplicate build message

* Fix ESLint

* Fix script

* Modify scripts

* Improve scripts

* Fix copy command

* Try installing package

* Depend on package job

* Add sudo

* Add sudo

* Troubleshoot

* Fix workflow

* Checkout source for prepare command

* Built ui within a container

* Mount src files for build instead of static copy in build image

* Attempt to fix actions

* Remove manual build container cleanup methods

* undo failed attempts to fix actions

* Fix path

* Remove -it flag

---------

Signed-off-by: J Boddey <boddey@google.com>
Co-authored-by: kurilova <sonnycactus@gmail.com>
Co-authored-by: jhughesbiot <jonathan.hughes@buildingsiot.com>

* Feature/risk in selected (#654)

* Adds risk to selected value

* Adds risk to selected value

---------

Co-authored-by: J Boddey <boddey@google.com>

* Show risk for each question in the Risk profile (#647)

* Show risk for each question in the Risk profile

* set top position to 0

---------

Co-authored-by: J Boddey <boddey@google.com>

* Use mqtt service instead of calling GET /status every 5 seconds.

* Use mqtt service instead of calling GET /status every 5 seconds.

* Use mqtt service instead of calling GET /status every 5 seconds.

* pylint

---------

Signed-off-by: J Boddey <boddey@google.com>
Co-authored-by: J Boddey <boddey@google.com>
Co-authored-by: Aliaksandr Nikitsin <aliaksandrn@google.com>
Co-authored-by: jhughesbiot <jonathan.hughes@buildingsiot.com>

* Allow ICMP response to DHCP messages in DHCP snooping test (#608)

* Allow ICMP response to DHCP messages

* Bug/unit test runtime (#655)

* Change base test module startup to allow setup script to run independent of module startup process
Update connection_module to allow for unit testing
Update unit test run script to use new process

* enable all unit tests
update google cert

* Remove binary fix lines from docker files
pylint updates

* pylint updates

---------

Co-authored-by: jhughesbiot <jonathan.hughes@buildingsiot.com>

* The risk profile saved with old format is shown improperly  while loading based on a new format (#664)

* Fill only fields that are present in profile

* GAR : The alt text for the expired risk profile should be communicated on Enter key (#662)

* Change Expired profile title on Enter; announce Expired profile title on Enter

* Update wording of tls cipher results (#671)

* Show error message if provided; show default message if no (#680)

* Test install on supported operating systems (#675)

* Test install on multiple versions

* Update step names

* Do not give Non-Compliant if error given

---------

Signed-off-by: J Boddey <boddey@google.com>
Co-authored-by: J Boddey <boddey@google.com>
Co-authored-by: Aliaksandr Nikitsin <aliaksandrn@google.com>
Co-authored-by: Sofia Kurilova <kurilova@google.com>
  • Loading branch information
4 people authored Aug 15, 2024
1 parent cbc3f84 commit c919f02
Show file tree
Hide file tree
Showing 16 changed files with 611 additions and 157 deletions.
1 change: 1 addition & 0 deletions framework/python/src/common/mqtt.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class MQTT:
def __init__(self) -> None:
self._host = WEBSOCKETS_HOST
self._client = mqtt_client.Client(mqtt_client.CallbackAPIVersion.VERSION2)
LOGGER.setLevel(logger.logging.INFO)
self._client.enable_logger(LOGGER)

def _connect(self):
Expand Down
9 changes: 9 additions & 0 deletions framework/python/src/net_orc/ip_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from common import logger
from common import util
import re
import socket

LOGGER = logger.get_logger('ip_ctrl')

Expand Down Expand Up @@ -96,6 +97,14 @@ def get_iface_port_stats(self, iface):
else:
return None

def get_ip_address(self, iface):
addrs = psutil.net_if_addrs()
if iface in addrs:
for addr in addrs[iface]:
if addr.family == socket.AF_INET:
return addr.address
return None

def get_namespaces(self):
result = util.run_command('ip netns list')
# Strip ID's from the namespace results
Expand Down
47 changes: 27 additions & 20 deletions framework/python/src/net_orc/network_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ def start_network(self):
# Get network ready (via Network orchestrator)
LOGGER.debug('Network is ready')

def get_ip_address(self, iface):
return self._ip_ctrl.get_ip_address(iface)

def get_listener(self):
return self._listener

Expand Down Expand Up @@ -494,14 +497,15 @@ def _load_network_module(self, module_dir):
# Load network service networking configuration
if net_module.enable_container:

net_module.net_config.enable_wan = net_module_json['config']['network'][
'enable_wan']
net_module.net_config.ip_index = net_module_json['config']['network'][
'ip_index']

net_module.net_config.host = False if not 'host' in net_module_json[
'config']['network'] else net_module_json['config']['network']['host']

if not net_module.net_config.host:
net_module.net_config.enable_wan = net_module_json['config']['network'][
'enable_wan']
net_module.net_config.ip_index = net_module_json['config']['network'][
'ip_index']

net_module.net_config.ipv4_address = self.network_config.ipv4_network[
net_module.net_config.ip_index]
net_module.net_config.ipv4_network = self.network_config.ipv4_network
Expand Down Expand Up @@ -538,26 +542,29 @@ def _get_network_module(self, name):
def _start_network_service(self, net_module):

LOGGER.debug('Starting network service ' + net_module.display_name)
network = 'host' if net_module.net_config.host else PRIVATE_DOCKER_NET
network = 'host' if net_module.net_config.host else 'bridge'
LOGGER.debug(f"""Network: {network}, image name: {net_module.image_name},
container name: {net_module.container_name}""")

try:
client = docker.from_env()
net_module.container = client.containers.run(
net_module.image_name,
auto_remove=True,
cap_add=['NET_ADMIN'],
name=net_module.container_name,
hostname=net_module.container_name,
network_mode='none',
privileged=True,
detach=True,
mounts=net_module.mounts,
environment={
'TZ': self.get_session().get_timezone(),
'HOST_USER': util.get_host_user()
})
net_module.image_name,
auto_remove=True,
cap_add=['NET_ADMIN'],
name=net_module.container_name,
hostname=net_module.container_name,
# Undetermined version of docker seems to have broken
# DNS configuration (/etc/resolv.conf) Re-add when/if
# this network is utilized and DNS issue is resolved
network=network,
privileged=True,
detach=True,
mounts=net_module.mounts,
environment={
'TZ': self.get_session().get_timezone(),
'HOST_USER': util.get_host_user()
})
except docker.errors.ContainerError as error:
LOGGER.error('Container run error')
LOGGER.error(error)
Expand Down Expand Up @@ -793,7 +800,7 @@ def network_adapters_checker(self, mqtt_client: mqtt.MQTT, topic: str):
adapters = self._session.detect_network_adapters_change()
if adapters:
mqtt_client.send_message(topic, adapters)
except Exception:
except Exception: # pylint: disable=W0718
LOGGER.error(traceback.format_exc())

def is_device_connected(self):
Expand Down
80 changes: 42 additions & 38 deletions framework/python/src/test_orc/test_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
LOG_NAME = "test_orc"
LOGGER = logger.get_logger("test_orc")
RUNTIME_DIR = "runtime"
RUNTIME_TEST_DIR = os.path.join(RUNTIME_DIR,"test")
RUNTIME_TEST_DIR = os.path.join(RUNTIME_DIR, "test")
TEST_MODULES_DIR = "modules/test"
MODULE_CONFIG = "conf/module_config.json"
LOG_REGEX = r"^[A-Z][a-z]{2} [0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} test_"
Expand Down Expand Up @@ -182,9 +182,7 @@ def _write_reports(self, test_report):
def _generate_report(self):

report = {}
report["testrun"] = {
"version": self.get_session().get_version()
}
report["testrun"] = {"version": self.get_session().get_version()}

report["mac_addr"] = self.get_session().get_target_device().mac_addr
report["device"] = self.get_session().get_target_device().to_dict()
Expand All @@ -207,7 +205,10 @@ def _calculate_result(self):
for test_result in self._session.get_test_results():
# Check Required tests
if (test_result.required_result.lower() == "required"
and test_result.result.lower() != "compliant"):
and test_result.result.lower() not in [
"compliant",
"error"
]):
result = "Non-Compliant"
# Check Required if Applicable tests
elif (test_result.required_result.lower() == "required if applicable"
Expand Down Expand Up @@ -295,25 +296,20 @@ def _timestamp_results(self, device):

return completed_results_dir

def zip_results(self,
device,
timestamp,
profile):
def zip_results(self, device, timestamp, profile):

try:
LOGGER.debug("Archiving test results")

src_path = os.path.join(LOCAL_DEVICE_REPORTS.replace(
"{device_folder}",
device.device_folder),
timestamp)
src_path = os.path.join(
LOCAL_DEVICE_REPORTS.replace("{device_folder}", device.device_folder),
timestamp)

# Define temp directory to store files before zipping
results_dir = os.path.join(f"/tmp/testrun/{time.time()}")

# Define where to save the zip file
zip_location = os.path.join("/tmp/testrun",
timestamp)
zip_location = os.path.join("/tmp/testrun", timestamp)

# Delete zip_temp if it already exists
if os.path.exists(results_dir):
Expand All @@ -323,16 +319,13 @@ def zip_results(self,
if os.path.exists(zip_location + ".zip"):
os.remove(zip_location + ".zip")

shutil.copytree(src_path,results_dir)
shutil.copytree(src_path, results_dir)

# Include profile if specified
if profile is not None:
LOGGER.debug(
f"Copying profile {profile.name} to results directory")
LOGGER.debug(f"Copying profile {profile.name} to results directory")
shutil.copy(profile.get_file_path(),
os.path.join(
results_dir,
"profile.json"))
os.path.join(results_dir, "profile.json"))

with open(os.path.join(results_dir, "profile.pdf"), "wb") as f:
f.write(profile.to_pdf(device).getvalue())
Expand All @@ -349,10 +342,9 @@ def zip_results(self,
if os.path.exists(zip_file)
else'creation failed'}''')


return zip_file

except Exception as error: # pylint: disable=W0703
except Exception as error: # pylint: disable=W0703
LOGGER.error("Failed to create zip file")
LOGGER.debug(error)
return None
Expand Down Expand Up @@ -426,6 +418,17 @@ def _run_test_module(self, module):
device_monitor_capture = os.path.join(device_test_dir, "monitor.pcap")
util.run_command(f"chown -R {self._host_user} {device_monitor_capture}")

# Resolve the main docker interface (docker0) for host interaction
# Can't use device or internet iface since these are not in a stable
# state for this type of communication during testing but docker0 has
# to exist and should always be available
external_ip = self._net_orc.get_ip_address("docker0")
LOGGER.debug(f"Using external IP: {external_ip}")
extra_hosts = {
"external.localhost": external_ip
} if external_ip is not None else {}

#extra_hosts = {"external.localhost":"172.17.0.1"}
client = docker.from_env()

module.container = client.containers.run(
Expand Down Expand Up @@ -468,8 +471,10 @@ def _run_test_module(self, module):
"IPV4_ADDR": device.ip_addr,
"DEVICE_TEST_MODULES": json.dumps(device.test_modules),
"IPV4_SUBNET": self._net_orc.network_config.ipv4_network,
"IPV6_SUBNET": self._net_orc.network_config.ipv6_network
})
"IPV6_SUBNET": self._net_orc.network_config.ipv6_network,
"DEV_IFACE": self._session.get_device_interface()
},
extra_hosts=extra_hosts)
except (docker.errors.APIError,
docker.errors.ContainerError) as container_error:
LOGGER.error("Test module " + module.name + " has failed to start")
Expand Down Expand Up @@ -526,19 +531,19 @@ def _run_test_module(self, module):

# Convert dict from json into TestCase object
test_case = TestCase(
name=test_result["name"],
description=test_result["description"],
expected_behavior=test_result["expected_behavior"],
required_result=test_result["required_result"],
result=test_result["result"])
name=test_result["name"],
description=test_result["description"],
expected_behavior=test_result["expected_behavior"],
required_result=test_result["required_result"],
result=test_result["result"])

# Any informational test should always report informational
if test_case.required_result == "Informational":
test_case.result = "Informational"

# Add steps to resolve if test is non-compliant
if (test_case.result == "Non-Compliant" and
"recommendations" in test_result):
if (test_case.result == "Non-Compliant"
and "recommendations" in test_result):
test_case.recommendations = test_result["recommendations"]
else:
test_case.recommendations = None
Expand All @@ -548,7 +553,7 @@ def _run_test_module(self, module):
except (FileNotFoundError, PermissionError,
json.JSONDecodeError) as results_error:
LOGGER.error(
f"Error occurred whilst obtaining results for module {module.name}")
f"Error occurred whilst obtaining results for module {module.name}")
LOGGER.error(results_error)

# Get the markdown report from the module if generated
Expand Down Expand Up @@ -666,11 +671,10 @@ def _load_test_module(self, module_dir):
for test_case_json in module_json["config"]["tests"]:
try:
test_case = TestCase(
name=test_case_json["name"],
description=test_case_json["test_description"],
expected_behavior=test_case_json["expected_behavior"],
required_result=test_case_json["required_result"]
)
name=test_case_json["name"],
description=test_case_json["test_description"],
expected_behavior=test_case_json["expected_behavior"],
required_result=test_case_json["required_result"])

if "recommendations" in test_case_json:
test_case.recommendations = test_case_json["recommendations"]
Expand Down
Loading

0 comments on commit c919f02

Please sign in to comment.