Skip to content

Commit

Permalink
modularizing docker methods (#672)
Browse files Browse the repository at this point in the history
* Initial work on modularizing docker methods
Implement test modules
New vars to session for better access across services

* Add network docker module

* Update docker files

* Update faux device docker file

* Fix validator

---------

Signed-off-by: J Boddey <boddey@google.com>
Co-authored-by: J Boddey <boddey@google.com>
  • Loading branch information
jhughesbiot and jboddey authored Aug 15, 2024
1 parent a099599 commit 6bbb803
Show file tree
Hide file tree
Showing 26 changed files with 572 additions and 458 deletions.
6 changes: 3 additions & 3 deletions cmd/build
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ mkdir -p build/network
for dir in modules/network/* ; do
module=$(basename $dir)
echo Building network module $module...
if docker build -f modules/network/$module/$module.Dockerfile -t test-run/$module . ; then
if docker build -f modules/network/$module/$module.Dockerfile -t testrun/$module . ; then
echo Successfully built container for network $module
else
echo An error occured whilst building container for network module $module
Expand All @@ -78,7 +78,7 @@ mkdir -p build/devices
for dir in modules/devices/* ; do
module=$(basename $dir)
echo Building validator module $module...
if docker build -f modules/devices/$module/$module.Dockerfile -t test-run/$module . ; then
if docker build -f modules/devices/$module/$module.Dockerfile -t testrun/$module . ; then
echo Successfully built container for device module $module
else
echo An error occured whilst building container for device module $module
Expand All @@ -92,7 +92,7 @@ mkdir -p build/test
for dir in modules/test/* ; do
module=$(basename $dir)
echo Building test module $module...
if docker build -f modules/test/$module/$module.Dockerfile -t test-run/$module-test . ; then
if docker build -f modules/test/$module/$module.Dockerfile -t testrun/$module-test . ; then
echo Successfully built container for test module $module
else
echo An error occured whilst building container for test module $module
Expand Down
35 changes: 35 additions & 0 deletions framework/python/src/common/docker_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# 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.
"""Utility for common docker methods"""
import docker

def create_private_net(network_name):
client = docker.from_env()
try:
network = client.networks.get(network_name)
network.remove()
except docker.errors.NotFound:
pass

# TODO: These should be made into variables
ipam_pool = docker.types.IPAMPool(subnet='100.100.0.0/16',
iprange='100.100.100.0/24')

ipam_config = docker.types.IPAMConfig(pool_configs=[ipam_pool])

client.networks.create(network_name,
ipam=ipam_config,
internal=True,
check_duplicate=True,
driver='macvlan')
50 changes: 33 additions & 17 deletions framework/python/src/common/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,13 @@ def __init__(self, root_dir):
self._load_config()
self._load_profiles()

# Network information
self._ipv4_subnet = None
self._ipv6_subnet = None

# Store host user for permissions use
self._host_user = util.get_host_user()

self._certs = []
self.load_certs()

Expand Down Expand Up @@ -196,7 +203,7 @@ def _load_config(self):
# Network interfaces
if (NETWORK_KEY in config_file_json
and DEVICE_INTF_KEY in config_file_json.get(NETWORK_KEY)
and INTERNET_INTF_KEY in config_file_json.get(NETWORK_KEY)):
and INTERNET_INTF_KEY in config_file_json.get(NETWORK_KEY)):
self._config[NETWORK_KEY][DEVICE_INTF_KEY] = config_file_json.get(
NETWORK_KEY, {}).get(DEVICE_INTF_KEY)
self._config[NETWORK_KEY][INTERNET_INTF_KEY] = config_file_json.get(
Expand Down Expand Up @@ -240,11 +247,14 @@ def _load_version(self):
try:
version = util.run_command(
'$(grep -R "Version: " $MAKE_CONTROL_DIR | awk "{print $2}"')
except Exception as e:
except Exception as e: # pylint: disable=W0703
LOGGER.debug('Failed getting the version from make control file')
LOGGER.error(e)
self._version = 'Unknown'

def get_host_user(self):
return self._host_user

def get_version(self):
return self._version

Expand Down Expand Up @@ -326,6 +336,12 @@ def get_device(self, mac_addr):
def remove_device(self, device):
self._device_repository.remove(device)

def get_ipv4_subnet(self):
return self._ipv4_subnet

def get_ipv6_subnet(self):
return self._ipv6_subnet

def get_status(self):
return self._status

Expand Down Expand Up @@ -398,15 +414,18 @@ def get_report_url(self):
def set_report_url(self, url):
self._report_url = url

def set_subnets(self, ipv4_subnet, ipv6_subnet):
self._ipv4_subnet = ipv4_subnet
self._ipv6_subnet = ipv6_subnet

def _load_profiles(self):

# Load format of questionnaire
LOGGER.debug('Loading risk assessment format')

try:
with open(os.path.join(
self._root_dir, PROFILE_FORMAT_PATH
), encoding='utf-8') as profile_format_file:
with open(os.path.join(self._root_dir, PROFILE_FORMAT_PATH),
encoding='utf-8') as profile_format_file:
format_json = json.load(profile_format_file)
# Save original profile format for internal validation
self._profile_format = format_json
Expand Down Expand Up @@ -439,7 +458,7 @@ def _load_profiles(self):

try:
for risk_profile_file in os.listdir(
os.path.join(self._root_dir, PROFILES_DIR)):
os.path.join(self._root_dir, PROFILES_DIR)):

LOGGER.debug(f'Discovered profile {risk_profile_file}')

Expand All @@ -459,15 +478,13 @@ def _load_profiles(self):
risk_profile = RiskProfile()

# Pass JSON to populate risk profile
risk_profile.load(
profile_json=json_data,
profile_format=self._profile_format
)
risk_profile.load(profile_json=json_data,
profile_format=self._profile_format)

# Add risk profile to session
self._profiles.append(risk_profile)

except Exception as e:
except Exception as e: # pylint: disable=W0703
LOGGER.error('An error occurred whilst loading risk profiles')
LOGGER.debug(e)

Expand Down Expand Up @@ -511,9 +528,8 @@ def update_profile(self, profile_json):
if risk_profile is None:

# Create a new risk profile
risk_profile = RiskProfile(
profile_json=profile_json,
profile_format=self._profile_format)
risk_profile = RiskProfile(profile_json=profile_json,
profile_format=self._profile_format)
self._profiles.append(risk_profile)

else:
Expand Down Expand Up @@ -649,7 +665,7 @@ def delete_profile(self, profile):

return True

except Exception as e:
except Exception as e: # pylint: disable=W0703
LOGGER.error('An error occurred whilst deleting a profile')
LOGGER.debug(e)
return False
Expand Down Expand Up @@ -789,7 +805,7 @@ def load_certs(self):
self._certs.append(cert_obj)

LOGGER.debug(f'Successfully loaded {cert_file}')
except Exception as e:
except Exception as e: # pylint: disable=W0703
LOGGER.error(f'An error occurred whilst loading {cert_file}')
LOGGER.debug(e)

Expand All @@ -809,7 +825,7 @@ def delete_cert(self, filename):
self._certs.remove(cert)
return True

except Exception as e:
except Exception as e: # pylint: disable=W0703
LOGGER.error('An error occurred whilst deleting the certificate')
LOGGER.debug(e)
return False
Expand Down
155 changes: 155 additions & 0 deletions framework/python/src/core/docker/module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# 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.
"""Represents the base module."""
import docker
from docker.models.containers import Container
import os
from common import logger
import json

IMAGE_PREFIX = 'testrun/'
CONTAINER_PREFIX = 'tr-ct'


class Module:
"""Represents the base module."""

def __init__(self, module_config_file, session):
self._session = session

# Read the config file into a json object
with open(module_config_file, encoding='UTF-8') as config_file:
module_json = json.load(config_file)

# General module information
self.name = module_json['config']['meta']['name']
self.display_name = module_json['config']['meta']['display_name']
self.description = module_json['config']['meta']['description']
self.enabled = module_json['config'].get('enabled', True)
self.depends_on = module_json['config']['docker'].get('depends_on', None)

# Absolute path
# Store the root directory of Testrun based on the expected locatoin
# Testrun/modules/<network or test>/<module>/conf -> 5 levels
self.root_path = os.path.abspath(
os.path.join(module_config_file, '../../../../..'))
self.dir = os.path.dirname(os.path.dirname(module_config_file))
self.dir_name = os.path.basename(self.dir)

# Docker settings
self.build_file = self.dir_name + '.Dockerfile'
self.image_name = f'{IMAGE_PREFIX}{self.dir_name}'
self.container_name = f'{CONTAINER_PREFIX}-{self.dir_name}'
if 'tests' in module_json['config']:
# Append Test module
self.image_name += '-test'
self.container_name += '-test'
self.enable_container = module_json['config']['docker'].get(
'enable_container', True)
self.container: Container = None

self._add_logger(log_name=self.name, module_name=self.name)
self.setup_module(module_json)

def _add_logger(self, log_name, module_name, log_dir=None):
self.logger = logger.get_logger(
name=f'{log_name}_module', # pylint: disable=E1123
log_file=f'{module_name}_module',
log_dir=log_dir)

def build(self):
self.logger.debug('Building module ' + self.dir_name)
client = docker.from_env()
client.images.build(
dockerfile=os.path.join(self.dir, self.build_file),
path=self._path,
forcerm=True, # Cleans up intermediate containers during build
tag=self.image_name)

def get_container(self):
container = None
try:
client = docker.from_env()
container = client.containers.get(self.container_name)
except docker.errors.NotFound:
self.logger.debug('Container ' + self.container_name + ' not found')
except docker.errors.APIError as error:
self.logger.error('Failed to resolve container')
self.logger.error(error)
return container

def get_session(self):
return self._session

def get_status(self):
self.container = self.get_container()
if self.container is not None:
return self.container.status
return None

def get_network(self):
return 'bridge'

def get_mounts(self):
return []

def get_environment(self, device=None): # pylint: disable=W0613
return {}

def setup_module(self, module_json):
pass

def _setup_runtime(self, device=None):
pass

def start(self, device=None):
self._setup_runtime(device)

self.logger.debug('Starting module ' + self.display_name)
network = self.get_network()
self.logger.debug(f"""Network: {network}, image name: {self.image_name},
container name: {self.container_name}""")

try:
client = docker.from_env()
self.container = client.containers.run(
self.image_name,
auto_remove=True,
cap_add=['NET_ADMIN'],
name=self.container_name,
hostname=self.container_name,
network_mode=network,
privileged=True,
detach=True,
mounts=self.get_mounts(),
environment=self.get_environment(device))
except docker.errors.ContainerError as error:
self.logger.error('Container run error')
self.logger.error(error)

def stop(self, kill=False):
self.logger.debug('Stopping module ' + self.container_name)
try:
container = self.get_container()
if container is not None:
if kill:
self.logger.debug('Killing container: ' + self.container_name)
container.kill()
else:
self.logger.debug('Stopping container: ' + self.container_name)
container.stop()
self.logger.debug('Container stopped: ' + self.container_name)
except Exception as error: # pylint: disable=W0703
self.logger.error('Container stop error')
self.logger.error(error)
Loading

0 comments on commit 6bbb803

Please sign in to comment.