#!/usr/bin/env python3
import logging
-import subprocess
-import time
+from subprocess import run, CalledProcessError
import argparse
import ipaddress
from sys import exit
-from os import chdir, getcwd
-from shutil import copytree
-from json import dumps
-from requests import get
+from os import chdir, getcwd, path, popen, kill, getuid, stat, mkdir, getlogin, chmod
+from shutil import copytree, rmtree, move
+from json import loads, dumps
+from yaml import load, SafeLoader, dump
+from glob import glob
+from time import strftime, tzname, daylight
+from docker import from_env
+from requests import get, codes, post
from requests.exceptions import MissingSchema, InvalidSchema, InvalidURL, ConnectionError, ConnectTimeout
def validate_url(url):
type=validate_ip, metavar='IP', required=True)
parser_bootstrap.add_argument('--typefileserver', help='Type of the file server (SFTP/FTPS) to be included in the VES event',
type=str, choices=['sftp', 'ftps'], required=True)
+ parser_bootstrap.add_argument('--user', help='File server username', type=str, metavar='USERNAME', required=True)
+ parser_bootstrap.add_argument('--password', help='File server password', type=str, metavar='PASSWORD', required=True)
parser_bootstrap.add_argument('--ipstart', help='IP address range beginning', type=validate_ip, metavar='IP', required=True)
# Start command parser
parser_start = subparsers.add_parser('start', help='Start instances')
- parser_start.add_argument('--count', help='Instance count to start', type=int, metavar='INT', default=1)
+ parser_start.add_argument('--count', help='Instance count to start', type=int, metavar='INT', default=0)
# Stop command parser
parser_stop = subparsers.add_parser('stop', help='Stop instances')
- parser_stop.add_argument('--count', help='Instance count to stop', type=int, metavar='INT', default=1)
+ parser_stop.add_argument('--count', help='Instance count to stop', type=int, metavar='INT', default=0)
# Trigger command parser
parser_trigger = subparsers.add_parser('trigger', help='Trigger one single VES event from each simulator')
- parser_trigger.add_argument('--count', help='Instance count to trigger', type=int, metavar='INT', default=1)
+ parser_trigger.add_argument('--count', help='Instance count to trigger', type=int, metavar='INT', default=0)
+ # Stop-simulator command parser
+ parser_stopsimulator = subparsers.add_parser('stop_simulator', help='Stop sending PNF registration messages')
+ parser_stopsimulator.add_argument('--count', help='Instance count to stop', type=int, metavar='INT', default=0)
# Trigger-custom command parser
parser_triggerstart = subparsers.add_parser('trigger_custom', help='Trigger one single VES event from specific simulators')
parser_triggerstart.add_argument('--triggerstart', help='First simulator id to trigger', type=int,
metavar='INT', required=True)
# Status command parser
parser_status = subparsers.add_parser('status', help='Status')
- parser_status.add_argument('--count', help='Instance count to show status for', type=int, metavar='INT', default=1)
+ parser_status.add_argument('--count', help='Instance count to show status for', type=int, metavar='INT', default=0)
# Clean command parser
subparsers.add_parser('clean', help='Clean work-dirs')
# General options parser
type=str, default='info')
return parser
-class MassPnfSim():
+class MassPnfSim:
log_lvl = logging.INFO
+ sim_compose_template = 'docker-compose-template.yml'
+ sim_vsftpd_template = 'config/vsftpd_ssl-TEMPLATE.conf'
+ sim_vsftpd_config = 'config/vsftpd_ssl.conf'
+ sim_sftp_script = 'fix-sftp-perms.sh'
+ sim_sftp_script_template = 'fix-sftp-perms-template.sh'
+ sim_config = 'config/config.yml'
+ sim_msg_config = 'config/config.json'
+ sim_port = 5000
+ sim_base_url = 'http://{}:' + str(sim_port) + '/simulator'
+ sim_start_url = sim_base_url + '/start'
+ sim_status_url = sim_base_url + '/status'
+ sim_stop_url = sim_base_url + '/stop'
+ sim_container_name = 'pnf-simulator'
+ rop_script_name = 'ROP_file_creator.sh'
def __init__(self, args):
self.args = args
self.logger = logging.getLogger(__name__)
self.logger.setLevel(self.log_lvl)
self.sim_dirname_pattern = "pnf-sim-lw-"
+ self.mvn_build_cmd = 'mvn clean package docker:build -Dcheckstyle.skip'
+ self.docker_compose_status_cmd = 'docker-compose ps'
+
+ # Validate 'trigger_custom' subcommand options
+ if self.args.subcommand == 'trigger_custom':
+ if (self.args.triggerend + 1) > self._enum_sim_instances():
+ self.logger.error('--triggerend value greater than existing instance count.')
+ exit(1)
+
+ # Validate --count option for subcommands that support it
+ if self.args.subcommand in ['start', 'stop', 'trigger', 'status', 'stop_simulator']:
+ if self.args.count > self._enum_sim_instances():
+ self.logger.error('--count value greater that existing instance count')
+ exit(1)
+ if not self._enum_sim_instances():
+ self.logger.error('No bootstrapped instance found')
+ exit(1)
+
+ # Validate 'bootstrap' subcommand
+ if (self.args.subcommand == 'bootstrap') and self._enum_sim_instances():
+ self.logger.error('Bootstrapped instances detected, not overwiriting, clean first')
+ exit(1)
def _run_cmd(self, cmd, dir_context='.'):
- if self.args.verbose == 'debug':
- cmd='bash -x ' + cmd
old_pwd = getcwd()
try:
chdir(dir_context)
- subprocess.run(cmd, check=True, shell=True)
+ self.logger.debug(f'_run_cmd: Current direcotry: {getcwd()}')
+ self.logger.debug(f'_run_cmd: Command string: {cmd}')
+ run(cmd, check=True, shell=True)
chdir(old_pwd)
except FileNotFoundError:
self.logger.error(f"Directory {dir_context} not found")
- except subprocess.CalledProcessError as e:
+ except CalledProcessError as e:
exit(e.returncode)
+ def _enum_sim_instances(self):
+ '''Helper method that returns bootstraped simulator instances count'''
+ return len(glob(f"{self.sim_dirname_pattern}[0-9]*"))
+
+ def _get_sim_instance_data(self, instance_id):
+ '''Helper method that returns specific instance data'''
+ oldpwd = getcwd()
+ chdir(f"{self.sim_dirname_pattern}{instance_id}")
+ with open(self.sim_config) as cfg:
+ yml = load(cfg, Loader=SafeLoader)
+ chdir(oldpwd)
+ return yml['ippnfsim']
+
+ def _get_docker_containers(self):
+ '''Returns a list containing 'name' attribute of running docker containers'''
+ dc = from_env()
+ containers = []
+ for container in dc.containers.list():
+ containers.append(container.attrs['Name'][1:])
+ return containers
+
+ def _get_iter_range(self):
+ '''Helper routine to get the iteration range
+ for the lifecycle commands'''
+ if hasattr(self.args, 'count'):
+ if not self.args.count:
+ return [self._enum_sim_instances()]
+ else:
+ return [self.args.count]
+ elif hasattr(self.args, 'triggerstart'):
+ return [self.args.triggerstart, self.args.triggerend + 1]
+ else:
+ return [self._enum_sim_instances()]
+
+ def _archive_logs(self, sim_dir):
+ '''Helper function to archive simulator logs or create the log dir'''
+ old_pwd = getcwd()
+ try:
+ chdir(sim_dir)
+ if path.isdir('logs'):
+ arch_dir = f"logs/archive_{strftime('%Y-%m-%d_%T')}"
+ mkdir(arch_dir)
+ self.logger.debug(f'Created {arch_dir}')
+ # Collect file list to move
+ self.logger.debug('Archiving log files')
+ for fpattern in ['*.log', '*.xml']:
+ for f in glob('logs/' + fpattern):
+ # Move files from list to arch dir
+ move(f, arch_dir)
+ self.logger.debug(f'Moving {f} to {arch_dir}')
+ else:
+ mkdir('logs')
+ self.logger.debug("Logs dir didn't exist, created")
+ chdir(old_pwd)
+ except FileNotFoundError:
+ self.logger.error(f"Directory {sim_dir} not found")
+
+ def _generate_pnf_sim_config(self, i, port_sftp, port_ftps, pnf_sim_ip):
+ '''Writes a yaml formatted configuration file for Java simulator app'''
+ yml = {}
+ yml['urlves'] = self.args.urlves
+ yml['urlsftp'] = f'sftp://{self.args.user}:{self.args.password}@{self.args.ipfileserver}:{port_sftp}'
+ yml['urlftps'] = f'ftps://{self.args.user}:{self.args.password}@{self.args.ipfileserver}:{port_ftps}'
+ yml['ippnfsim'] = pnf_sim_ip
+ yml['typefileserver'] = self.args.typefileserver
+ self.logger.debug(f'Generated simulator config:\n{dump(yml)}')
+ with open(f'{self.sim_dirname_pattern}{i}/{self.sim_config}', 'w') as fout:
+ fout.write(dump(yml))
+
+ def _generate_config_file(self, source, dest, **kwargs):
+ '''Helper private method to generate a file based on a template'''
+ old_pwd = getcwd()
+ chdir(self.sim_dirname_pattern + str(kwargs['I']))
+ # Read the template file
+ with open(source, 'r') as f:
+ template = f.read()
+ # Replace all occurences of env like variable with it's
+ # relevant value from a corresponding key form kwargs
+ for (k,v) in kwargs.items():
+ template = template.replace('${' + k + '}', str(v))
+ with open(dest, 'w') as f:
+ f.write(template)
+ chdir(old_pwd)
+
def bootstrap(self):
self.logger.info("Bootstrapping PNF instances")
start_port += 2
self.logger.info(f'\tCreating {self.sim_dirname_pattern}{i}')
- try:
- copytree('pnf-sim-lightweight', f'{self.sim_dirname_pattern}{i}')
- except FileExistsError:
- self.logger.error(f'Directory {self.sim_dirname_pattern}{i} already exists, cannot overwrite.')
- exit(1)
-
- composercmd = " ".join([
- "./simulator.sh compose",
- ip['gw'],
- ip['subnet'],
- str(i),
- self.args.urlves,
- ip['PnfSim'],
- str(self.args.ipfileserver),
- self.args.typefileserver,
- str(PortSftp),
- str(PortFtps),
- ip['ftps'],
- ip['sftp'],
- str(ftps_pasv_port_start),
- str(ftps_pasv_port_end)
- ])
- self.logger.debug(f"Script cmdline: {composercmd}")
+ copytree('pnf-sim-lightweight', f'{self.sim_dirname_pattern}{i}')
self.logger.info(f"\tCreating instance #{i} configuration ")
- self._run_cmd(composercmd, f"{self.sim_dirname_pattern}{i}")
+ self._generate_pnf_sim_config(i, PortSftp, PortFtps, ip['PnfSim'])
+ # generate docker-compose for the simulator instance
+ self._generate_config_file(self.sim_compose_template, 'docker-compose.yml',
+ IPGW = ip['gw'], IPSUBNET = ip['subnet'],
+ I = i, IPPNFSIM = ip['PnfSim'],
+ PORTSFTP = str(PortSftp),
+ PORTFTPS = str(PortFtps),
+ IPFTPS = ip['ftps'], IPSFTP = ip['sftp'],
+ FTPS_PASV_MIN = str(ftps_pasv_port_start),
+ FTPS_PASV_MAX = str(ftps_pasv_port_end),
+ TIMEZONE = tzname[daylight],
+ FILESERV_USER = self.args.user,
+ FILESERV_PASS = self.args.password)
+ # generate vsftpd config file for the simulator instance
+ self._generate_config_file(self.sim_vsftpd_template, self.sim_vsftpd_config,
+ I = i, USER = getlogin(),
+ FTPS_PASV_MIN = str(ftps_pasv_port_start),
+ FTPS_PASV_MAX = str(ftps_pasv_port_end),
+ IPFILESERVER = str(self.args.ipfileserver))
+ # generate sftp permission fix script
+ self._generate_config_file(self.sim_sftp_script_template, self.sim_sftp_script,
+ I = i, FILESERV_USER = self.args.user)
+ chmod(f'{self.sim_dirname_pattern}{i}/{self.sim_sftp_script}', 0o755)
+ # Run the 3GPP measurements file generator
+ self._run_cmd(f'./ROP_file_creator.sh {i} &', f"{self.sim_dirname_pattern}{i}")
ftps_pasv_port_start += ftps_pasv_port_num_of_ports + 1
ftps_pasv_port_end += ftps_pasv_port_num_of_ports + 1
+ # ugly hack to chown vsftpd config file to root
+ if getuid():
+ self._run_cmd(f'sudo chown root {self.sim_vsftpd_config}', f'{self.sim_dirname_pattern}{i}')
+ self.logger.debug(f"vsftpd config file owner UID: {stat(self.sim_dirname_pattern + str(i) + '/' + self.sim_vsftpd_config).st_uid}")
+
self.logger.info(f'Done setting up instance #{i}')
def build(self):
self.logger.info("Building simulator image")
- completed = subprocess.run('set -x; cd pnf-sim-lightweight; ./simulator.sh build ', shell=True)
- self.logger.info(f"Build docker image: {completed.stdout}")
+ if path.isfile('pnf-sim-lightweight/pom.xml'):
+ self._run_cmd(self.mvn_build_cmd, 'pnf-sim-lightweight')
+ else:
+ self.logger.error('POM file was not found, Maven cannot run')
+ exit(1)
def clean(self):
self.logger.info('Cleaning simulators workdirs')
- self._run_cmd(f"rm -rf {self.sim_dirname_pattern}*")
+ for sim_id in range(self._enum_sim_instances()):
+ rmtree(f"{self.sim_dirname_pattern}{sim_id}")
def start(self):
- for i in range(self.args.count):
- self.logger.info(f'Starting {self.sim_dirname_pattern}{i} instance:')
- self._run_cmd('./simulator.sh start', f"{self.sim_dirname_pattern}{i}")
- time.sleep(5)
+ for i in range(*self._get_iter_range()):
+ # If container is not running
+ if f"{self.sim_container_name}-{i}" not in self._get_docker_containers():
+ self.logger.info(f'Starting {self.sim_dirname_pattern}{i} instance:')
+ self.logger.info(f' PNF-Sim IP: {self._get_sim_instance_data(i)}')
+ #Move logs to archive
+ self._archive_logs(self.sim_dirname_pattern + str(i))
+ self.logger.info(' Starting simulator containers using netconf model specified in config/netconf.env')
+ self._run_cmd('docker-compose up -d', self.sim_dirname_pattern + str(i))
+ else:
+ self.logger.warning(f'Instance {self.sim_dirname_pattern}{i} containers are already up')
def status(self):
- for i in range(self.args.count):
- self.logger.info(f'Getting {self.sim_dirname_pattern}{i} status:')
- self._run_cmd('./simulator.sh status', f"{self.sim_dirname_pattern}{i}")
+ for i in range(*self._get_iter_range()):
+ self.logger.info(f'Getting {self.sim_dirname_pattern}{i} instance status:')
+ if f"{self.sim_container_name}-{i}" in self._get_docker_containers():
+ try:
+ sim_ip = self._get_sim_instance_data(i)
+ self.logger.info(f' PNF-Sim IP: {sim_ip}')
+ self._run_cmd(self.docker_compose_status_cmd, f"{self.sim_dirname_pattern}{i}")
+ sim_response = get('{}'.format(self.sim_status_url).format(sim_ip))
+ if sim_response.status_code == codes.ok:
+ self.logger.info(sim_response.text)
+ else:
+ self.logger.error(f'Simulator request returned http code {sim_response.status_code}')
+ except KeyError:
+ self.logger.error(f'Unable to get sim instance IP from {self.sim_config}')
+ else:
+ self.logger.info(' Simulator containers are down')
def stop(self):
- for i in range(self.args.count):
+ for i in range(*self._get_iter_range()):
self.logger.info(f'Stopping {self.sim_dirname_pattern}{i} instance:')
- self._run_cmd(f'./simulator.sh stop {i}', f"{self.sim_dirname_pattern}{i}")
+ self.logger.info(f' PNF-Sim IP: {self._get_sim_instance_data(i)}')
+ # attempt killing ROP script
+ rop_pid = []
+ for ps_line in iter(popen(f'ps --no-headers -C {self.rop_script_name} -o pid,cmd').readline, ''):
+ # try getting ROP script pid
+ try:
+ ps_line_arr = ps_line.split()
+ assert self.rop_script_name in ps_line_arr[2]
+ assert ps_line_arr[3] == str(i)
+ rop_pid = ps_line_arr[0]
+ except AssertionError:
+ pass
+ else:
+ # get rop script childs, kill ROP script and all childs
+ childs = popen(f'pgrep -P {rop_pid}').read().split()
+ for pid in [rop_pid] + childs:
+ kill(int(pid), 15)
+ self.logger.info(f' ROP_file_creator.sh {i} successfully killed')
+ if not rop_pid:
+ # no process found
+ self.logger.warning(f' ROP_file_creator.sh {i} already not running')
+ # try tearing down docker-compose application
+ if f"{self.sim_container_name}-{i}" in self._get_docker_containers():
+ self._run_cmd('docker-compose down', self.sim_dirname_pattern + str(i))
+ self._run_cmd('docker-compose rm', self.sim_dirname_pattern + str(i))
+ else:
+ self.logger.warning(" Simulator containers are already down")
def trigger(self):
self.logger.info("Triggering VES sending:")
- for i in range(self.args.count):
+ for i in range(*self._get_iter_range()):
+ sim_ip = self._get_sim_instance_data(i)
self.logger.info(f'Triggering {self.sim_dirname_pattern}{i} instance:')
- self._run_cmd(f'./simulator.sh trigger-simulator', f"{self.sim_dirname_pattern}{i}")
+ self.logger.info(f' PNF-Sim IP: {sim_ip}')
+ # setup req headers
+ req_headers = {
+ "Content-Type": "application/json",
+ "X-ONAP-RequestID": "123",
+ "X-InvocationID": "456"
+ }
+ self.logger.debug(f' Request headers: {req_headers}')
+ try:
+ # get payload for the request
+ with open(f'{self.sim_dirname_pattern}{i}/{self.sim_msg_config}') as data:
+ json_data = loads(data.read())
+ self.logger.debug(f' JSON payload for the simulator:\n{json_data}')
+ # make a http request to the simulator
+ sim_response = post('{}'.format(self.sim_start_url).format(sim_ip), headers=req_headers, json=json_data)
+ if sim_response.status_code == codes.ok:
+ self.logger.info(' Simulator response: ' + sim_response.text)
+ else:
+ self.logger.warning(' Simulator response ' + sim_response.text)
+ except TypeError:
+ self.logger.error(f' Could not load JSON data from {self.sim_dirname_pattern}{i}/{self.sim_msg_config}')
- def trigger_custom(self):
- self.logger.info("Triggering VES sending by a range of simulators:")
- for i in range(self.args.triggerstart, self.args.triggerend+1):
- self.logger.info(f'Triggering {self.sim_dirname_pattern}{i} instance:')
- self._run_cmd(f'./simulator.sh trigger-simulator', f"{self.sim_dirname_pattern}{i}")
+ # Make the 'trigger_custom' an alias to the 'trigger' method
+ trigger_custom = trigger
+
+ def stop_simulator(self):
+ self.logger.info("Stopping sending PNF registration messages:")
+ for i in range(*self._get_iter_range()):
+ sim_ip = self._get_sim_instance_data(i)
+ self.logger.info(f'Stopping {self.sim_dirname_pattern}{i} instance:')
+ self.logger.info(f' PNF-Sim IP: {sim_ip}')
+ sim_response = post('{}'.format(self.sim_stop_url).format(sim_ip))
+ if sim_response.status_code == codes.ok:
+ self.logger.info(' Simulator response: ' + sim_response.text)
+ else:
+ self.logger.warning(' Simulator response ' + sim_response.text)