Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
125f08f
Start API
jboddey Jul 4, 2023
f47c1d0
Write interfaces
jboddey Jul 7, 2023
ceaac38
Get current configuration
jboddey Jul 7, 2023
6e5cc2e
Set versions
jboddey Jul 7, 2023
aaed0e4
Add more API methods
jboddey Jul 10, 2023
3d1c1ae
Correct no-ui flag
jboddey Jul 10, 2023
2f17fce
Do not launch API on baseline test
jboddey Jul 10, 2023
7c4fcda
Move loading devices back to Test Run core
jboddey Jul 12, 2023
3d244af
Merge dev into api (#74)
jboddey Jul 14, 2023
009d124
Merge branch 'dev' into api
jboddey Jul 18, 2023
2724ed8
Fix testing command
jboddey Jul 18, 2023
768ac10
Disable API on testing
jboddey Jul 18, 2023
192bae8
Add API session
jboddey Jul 18, 2023
c176bcf
Remove old method
jboddey Jul 18, 2023
664706e
Remove local vars
jboddey Jul 18, 2023
73d5a18
Replace old var
jboddey Jul 18, 2023
f0e1350
Add device config
jboddey Jul 19, 2023
6d9b5e5
Add device configs
jboddey Jul 19, 2023
ec04823
Fix paths
jboddey Jul 19, 2023
9169adf
Change MAC address
jboddey Jul 19, 2023
b9fad2c
Revert mac
jboddey Jul 19, 2023
215b55c
Fix copy path
jboddey Jul 19, 2023
b09514c
Debug loading devices
jboddey Jul 19, 2023
9d6eaaf
Remove reference
jboddey Jul 19, 2023
ef3dd3c
Changes
jboddey Jul 20, 2023
6c38faf
Re-add checks to prevent null values
jboddey Jul 20, 2023
f14ffeb
Fix variable
jboddey Jul 20, 2023
975a894
Fix
jboddey Jul 20, 2023
bd99c5e
Use dict instead of string
jboddey Jul 20, 2023
3908cde
Try without json conversion
jboddey Jul 20, 2023
1b286a7
Container output to log
jboddey Jul 20, 2023
aa9f03f
Undo changes to nmap module
jboddey Jul 20, 2023
cd855da
Add post devices route
jboddey Jul 21, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
uses: actions/checkout@v2.3.4
- name: Run tests
shell: bash {0}
run: testing/test_baseline
run: testing/baseline/test_baseline

testrun_tests:
name: Tests
Expand All @@ -27,7 +27,7 @@ jobs:
uses: actions/checkout@v2.3.4
- name: Run tests
shell: bash {0}
run: testing/test_tests
run: testing/tests/test_tests
pylint:
name: Pylint
runs-on: ubuntu-22.04
Expand All @@ -37,4 +37,4 @@ jobs:
uses: actions/checkout@v2.3.4
- name: Run tests
shell: bash {0}
run: testing/test_pylint
run: testing/pylint/test_pylint
17 changes: 10 additions & 7 deletions cmd/start → bin/testrun
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,26 @@
# limitations under the License.

if [[ "$EUID" -ne 0 ]]; then
echo "Must run as root. Use sudo cmd/start"
echo "Must run as root. Use sudo testrun"
exit 1
fi

# TODO: Obtain TESTRUNPATH from user environment variables
# TESTRUNPATH="/home/boddey/Desktop/test-run"
# cd $TESTRUNPATH

# Ensure that /var/run/netns folder exists
mkdir -p /var/run/netns
sudo mkdir -p /var/run/netns

# Clear up existing runtime files
rm -rf runtime
# Create device folder if it doesn't exist
mkdir -p local/devices

# Check if python modules exist. Install if not
[ ! -d "venv" ] && cmd/install
# Check if Python modules exist. Install if not
[ ! -d "venv" ] && sudo cmd/install

# Activate Python virtual environment
source venv/bin/activate

# TODO: Execute python code
# Set the PYTHONPATH to include the "src" directory
export PYTHONPATH="$PWD/framework/python/src"
python -u framework/python/src/core/test_runner.py $@
Expand Down
197 changes: 197 additions & 0 deletions framework/python/src/api/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# 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.

from fastapi import FastAPI, APIRouter, Response, Request, status
import json
from json import JSONDecodeError
import psutil
import threading
import uvicorn

from common import logger
from common.device import Device

LOGGER = logger.get_logger("api")

DEVICE_MAC_ADDR_KEY = "mac_addr"
DEVICE_MANUFACTURER_KEY = "manufacturer"
DEVICE_MODEL_KEY = "model"

class Api:
"""Provide REST endpoints to manage Test Run"""

def __init__(self, test_run):

self._test_run = test_run
self._name = "TestRun API"
self._router = APIRouter()

self._session = self._test_run.get_session()

self._router.add_api_route("/system/interfaces", self.get_sys_interfaces)
self._router.add_api_route("/system/config", self.post_sys_config,
methods=["POST"])
self._router.add_api_route("/system/config", self.get_sys_config)
self._router.add_api_route("/system/start", self.start_test_run,
methods=["POST"])
self._router.add_api_route("/system/stop", self.stop_test_run,
methods=["POST"])
self._router.add_api_route("/system/status", self.get_status)

self._router.add_api_route("/devices", self.get_devices)
self._router.add_api_route("/device", self.save_device, methods=["POST"])

self._app = FastAPI()
self._app.include_router(self._router)

self._api_thread = threading.Thread(target=self._start,
name="Test Run API",
daemon=True)

def start(self):
LOGGER.info("Starting API")
self._api_thread.start()
LOGGER.info("API waiting for requests")

def _start(self):
uvicorn.run(self._app, log_config=None)

def stop(self):
LOGGER.info("Stopping API")

async def get_sys_interfaces(self):
addrs = psutil.net_if_addrs()
ifaces = []
for iface in addrs:
ifaces.append(iface)
return ifaces

async def post_sys_config(self, request: Request, response: Response):
try:
config = (await request.body()).decode("UTF-8")
config_json = json.loads(config)
self._session.set_config(config_json)
# Catch JSON Decode error etc
except JSONDecodeError:
response.status_code = status.HTTP_400_BAD_REQUEST
return self._generate_msg(False, "Invalid JSON received")
return self._session.get_config()

async def get_sys_config(self):
return self._session.get_config()

async def get_devices(self):
return self._session.get_device_repository()

async def start_test_run(self, request: Request, response: Response):

LOGGER.debug("Received start command")

# Check request is valid
body = (await request.body()).decode("UTF-8")
body_json = None

try:
body_json = json.loads(body)
except JSONDecodeError:
response.status_code = status.HTTP_400_BAD_REQUEST
return self._generate_msg(False, "Invalid JSON received")

if "device" not in body_json or "mac_addr" not in body_json["device"]:
response.status_code = status.HTTP_400_BAD_REQUEST
return self._generate_msg(False, "Invalid request received")

device = self._session.get_device(body_json["device"]["mac_addr"])

# Check Test Run is not already running
if self._test_run.get_session().get_status() != "Idle":
LOGGER.debug("Test Run is already running. Cannot start another instance")
response.status_code = status.HTTP_409_CONFLICT
return self._generate_msg(False, "Test Run is already running")

# Check if requested device is known in the device repository
if device is None:
response.status_code = status.HTTP_404_NOT_FOUND
return self._generate_msg(False, "A device with that MAC address could not be found")

# Check Test Run is able to start
if self._test_run.get_net_orc().check_config() is False:
response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
return self._generate_msg(False, "Configured interfaces are not ready for use. Ensure both interfaces are connected.")

self._test_run.get_session().set_target_device(device)
LOGGER.info(f"Starting Test Run with device target {device.manufacturer} {device.model} with MAC address {device.mac_addr}")

thread = threading.Thread(target=self._start_test_run,
name="Test Run")
thread.start()
return self._test_run.get_session().to_json()

def _generate_msg(self, success, message):
msg_type = "success"
if not success:
msg_type = "error"
return json.loads('{"' + msg_type + '": "' + message + '"}')

def _start_test_run(self):
self._test_run.start()

async def stop_test_run(self):
LOGGER.debug("Received stop command. Stopping Test Run")
self._test_run.stop()
return self._generate_msg(True, "Test Run stopped")

async def get_status(self):
return self._test_run.get_session().to_json()

async def get_history(self):
LOGGER.info("Returning previous Test Runs to UI")

async def save_device(self, request: Request, response: Response):
LOGGER.debug("Received device post request")

try:
device_raw = (await request.body()).decode("UTF-8")
device_json = json.loads(device_raw)

if not self._validate_device_json(device_json):
response.status_code = status.HTTP_400_BAD_REQUEST
return self._generate_msg(False, "Invalid request received")

device = self._session.get_device(device_json.get(DEVICE_MAC_ADDR_KEY))
if device is None:
# Create new device
device = Device()
device.mac_addr = device_json.get(DEVICE_MAC_ADDR_KEY)
response.status_code = status.HTTP_201_CREATED

device.manufacturer = device_json.get(DEVICE_MANUFACTURER_KEY)
device.model = device_json.get(DEVICE_MODEL_KEY)

self._session.save_device(device)

return device

# Catch JSON Decode error etc
except JSONDecodeError:
response.status_code = status.HTTP_400_BAD_REQUEST
return self._generate_msg(False, "Invalid JSON received")

def _validate_device_json(self, json_obj):
if not (DEVICE_MAC_ADDR_KEY in json_obj and
DEVICE_MANUFACTURER_KEY in json_obj and
DEVICE_MODEL_KEY in json_obj
):
return False
return True
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@

"""Track device object information."""

from net_orc.network_device import NetworkDevice
from dataclasses import dataclass


@dataclass
class Device(NetworkDevice):
class Device():
"""Represents a physical device and it's configuration."""

mac_addr: str = None
manufacturer: str = None
model: str = None
test_modules: str = None
ip_addr: str = None
Loading