3 from subprocess import run, CalledProcessError
7 from os import chdir, getcwd, path, popen, kill, getuid, stat, mkdir
8 from shutil import copytree, rmtree, move
9 from json import loads, dumps
10 from yaml import load, SafeLoader
12 from time import strftime
13 from docker import from_env
14 from requests import get, codes, post
15 from requests.exceptions import MissingSchema, InvalidSchema, InvalidURL, ConnectionError, ConnectTimeout
17 def validate_url(url):
18 '''Helper function to perform --urlves input param validation'''
19 logger = logging.getLogger("urllib3")
20 logger.setLevel(logging.WARNING)
22 get(url, timeout=0.001)
23 except (MissingSchema, InvalidSchema, InvalidURL):
24 raise argparse.ArgumentTypeError(f'{url} is not a valid URL')
25 except (ConnectionError, ConnectTimeout):
30 '''Helper function to validate input param is a vaild IP address'''
32 ip_valid = ipaddress.ip_address(ip)
34 raise argparse.ArgumentTypeError(f'{ip} is not a valid IP address')
39 '''Process input arguments'''
41 parser = argparse.ArgumentParser()
42 subparsers = parser.add_subparsers(title='Subcommands', dest='subcommand')
43 # Build command parser
44 subparsers.add_parser('build', help='Build simulator image')
45 # Bootstrap command parser
46 parser_bootstrap = subparsers.add_parser('bootstrap', help='Bootstrap the system')
47 parser_bootstrap.add_argument('--count', help='Instance count to bootstrap', type=int, metavar='INT', default=1)
48 parser_bootstrap.add_argument('--urlves', help='URL of the VES collector', type=validate_url, metavar='URL', required=True)
49 parser_bootstrap.add_argument('--ipfileserver', help='Visible IP of the file server (SFTP/FTPS) to be included in the VES event',
50 type=validate_ip, metavar='IP', required=True)
51 parser_bootstrap.add_argument('--typefileserver', help='Type of the file server (SFTP/FTPS) to be included in the VES event',
52 type=str, choices=['sftp', 'ftps'], required=True)
53 parser_bootstrap.add_argument('--ipstart', help='IP address range beginning', type=validate_ip, metavar='IP', required=True)
54 # Start command parser
55 parser_start = subparsers.add_parser('start', help='Start instances')
56 parser_start.add_argument('--count', help='Instance count to start', type=int, metavar='INT', default=0)
58 parser_stop = subparsers.add_parser('stop', help='Stop instances')
59 parser_stop.add_argument('--count', help='Instance count to stop', type=int, metavar='INT', default=0)
60 # Trigger command parser
61 parser_trigger = subparsers.add_parser('trigger', help='Trigger one single VES event from each simulator')
62 parser_trigger.add_argument('--count', help='Instance count to trigger', type=int, metavar='INT', default=0)
63 # Stop-simulator command parser
64 parser_stopsimulator = subparsers.add_parser('stop_simulator', help='Stop sending PNF registration messages')
65 parser_stopsimulator.add_argument('--count', help='Instance count to stop', type=int, metavar='INT', default=0)
66 # Trigger-custom command parser
67 parser_triggerstart = subparsers.add_parser('trigger_custom', help='Trigger one single VES event from specific simulators')
68 parser_triggerstart.add_argument('--triggerstart', help='First simulator id to trigger', type=int,
69 metavar='INT', required=True)
70 parser_triggerstart.add_argument('--triggerend', help='Last simulator id to trigger', type=int,
71 metavar='INT', required=True)
72 # Status command parser
73 parser_status = subparsers.add_parser('status', help='Status')
74 parser_status.add_argument('--count', help='Instance count to show status for', type=int, metavar='INT', default=0)
75 # Clean command parser
76 subparsers.add_parser('clean', help='Clean work-dirs')
77 # General options parser
78 parser.add_argument('--verbose', help='Verbosity level', choices=['info', 'debug'],
79 type=str, default='info')
84 # MassPnfSim class actions decorator
85 class _MassPnfSim_Decorators:
88 def do_action(action_string, cmd):
89 def action_decorator(method):
90 def action_wrap(self):
91 # Alter looping range if action is 'tigger_custom'
92 if method.__name__ == 'trigger_custom':
93 iter_range = [self.args.triggerstart, self.args.triggerend+1]
95 if not self.args.count:
96 # If no instance count set explicitly via --count
98 iter_range = [self.existing_sim_instances]
100 iter_range = [self.args.count]
102 for i in range(*iter_range):
103 self.logger.info(f'{action_string} {self.sim_dirname_pattern}{i} instance:')
104 self._run_cmd(cmd, f"{self.sim_dirname_pattern}{i}")
106 return action_decorator
108 log_lvl = logging.INFO
109 sim_config = 'config/config.yml'
110 sim_msg_config = 'config/config.json'
112 sim_base_url = 'http://{}:' + str(sim_port) + '/simulator'
113 sim_start_url = sim_base_url + '/start'
114 sim_status_url = sim_base_url + '/status'
115 sim_stop_url = sim_base_url + '/stop'
116 sim_container_name = 'pnf-simulator'
117 rop_script_name = 'ROP_file_creator.sh'
119 def __init__(self, args):
121 self.logger = logging.getLogger(__name__)
122 self.logger.setLevel(self.log_lvl)
123 self.sim_dirname_pattern = "pnf-sim-lw-"
124 self.mvn_build_cmd = 'mvn clean package docker:build -Dcheckstyle.skip'
125 self.docker_compose_status_cmd = 'docker-compose ps'
126 self.existing_sim_instances = self._enum_sim_instances()
128 # Validate 'trigger_custom' subcommand options
129 if self.args.subcommand == 'trigger_custom':
130 if (self.args.triggerend + 1) > self.existing_sim_instances:
131 self.logger.error('--triggerend value greater than existing instance count.')
134 # Validate --count option for subcommands that support it
135 if self.args.subcommand in ['start', 'stop', 'trigger', 'status', 'stop_simulator']:
136 if self.args.count > self.existing_sim_instances:
137 self.logger.error('--count value greater that existing instance count')
139 if not self.existing_sim_instances:
140 self.logger.error('No bootstrapped instance found')
143 # Validate 'bootstrap' subcommand
144 if (self.args.subcommand == 'bootstrap') and self.existing_sim_instances:
145 self.logger.error('Bootstrapped instances detected, not overwiriting, clean first')
148 def _run_cmd(self, cmd, dir_context='.'):
152 self.logger.debug(f'_run_cmd: Current direcotry: {getcwd()}')
153 self.logger.debug(f'_run_cmd: Command string: {cmd}')
154 run(cmd, check=True, shell=True)
156 except FileNotFoundError:
157 self.logger.error(f"Directory {dir_context} not found")
158 except CalledProcessError as e:
161 def _enum_sim_instances(self):
162 '''Helper method that returns bootstraped simulator instances count'''
163 return len(glob(f"{self.sim_dirname_pattern}[0-9]*"))
165 def _get_sim_instance_data(self, instance_id):
166 '''Helper method that returns specific instance data'''
168 chdir(f"{self.sim_dirname_pattern}{instance_id}")
169 with open(self.sim_config) as cfg:
170 yml = load(cfg, Loader=SafeLoader)
172 return yml['ippnfsim']
174 def _get_docker_containers(self):
175 '''Returns a list containing 'name' attribute of running docker containers'''
178 for container in dc.containers.list():
179 containers.append(container.attrs['Name'][1:])
182 def _get_iter_range(self):
183 '''Helper routine to get the iteration range
184 for the lifecycle commands'''
185 if hasattr(self.args, 'count'):
186 if not self.args.count:
187 return [self.existing_sim_instances]
189 return [self.args.count]
190 elif hasattr(self.args, 'triggerstart'):
191 return [self.args.triggerstart, self.args.triggerend + 1]
193 return [self.existing_sim_instances]
195 def _archive_logs(self, sim_dir):
196 '''Helper function to archive simulator logs or create the log dir'''
200 if path.isdir('logs'):
201 arch_dir = f"logs/archive_{strftime('%Y-%m-%d_%T')}"
203 self.logger.debug(f'Created {arch_dir}')
204 # Collect file list to move
205 self.logger.debug('Archiving log files')
206 for fpattern in ['*.log', '*.xml']:
207 for f in glob('logs/' + fpattern):
208 # Move files from list to arch dir
210 self.logger.debug(f'Moving {f} to {arch_dir}')
213 self.logger.debug("Logs dir didn't exist, created")
215 except FileNotFoundError:
216 self.logger.error(f"Directory {sim_dir} not found")
219 self.logger.info("Bootstrapping PNF instances")
222 ftps_pasv_port_start = 8000
223 ftps_pasv_port_num_of_ports = 10
225 ftps_pasv_port_end = ftps_pasv_port_start + ftps_pasv_port_num_of_ports
227 for i in range(self.args.count):
228 self.logger.info(f"PNF simulator instance: {i}")
230 # The IP ranges are in distance of 16 compared to each other.
231 # This is matching the /28 subnet mask used in the dockerfile inside.
232 instance_ip_offset = i * 16
243 for prop in ip_properties:
244 ip.update({prop: str(self.args.ipstart + ip_offset + instance_ip_offset)})
247 self.logger.debug(f'Instance #{i} properties:\n {dumps(ip, indent=4)}')
249 PortSftp = start_port + 1
250 PortFtps = start_port + 2
253 self.logger.info(f'\tCreating {self.sim_dirname_pattern}{i}')
254 copytree('pnf-sim-lightweight', f'{self.sim_dirname_pattern}{i}')
256 composercmd = " ".join([
257 "./simulator.sh compose",
263 str(self.args.ipfileserver),
264 self.args.typefileserver,
269 str(ftps_pasv_port_start),
270 str(ftps_pasv_port_end)
272 self.logger.debug(f"Script cmdline: {composercmd}")
273 self.logger.info(f"\tCreating instance #{i} configuration ")
274 self._run_cmd(composercmd, f"{self.sim_dirname_pattern}{i}")
276 ftps_pasv_port_start += ftps_pasv_port_num_of_ports + 1
277 ftps_pasv_port_end += ftps_pasv_port_num_of_ports + 1
279 # ugly hack to chown vsftpd config file to root
281 self._run_cmd('sudo chown root config/vsftpd_ssl.conf', f'{self.sim_dirname_pattern}{i}')
282 self.logger.debug(f"vsftpd_ssl.conf file owner UID: {stat(self.sim_dirname_pattern + str(i) + '/config/vsftpd_ssl.conf').st_uid}")
284 self.logger.info(f'Done setting up instance #{i}')
287 self.logger.info("Building simulator image")
288 if path.isfile('pnf-sim-lightweight/pom.xml'):
289 self._run_cmd(self.mvn_build_cmd, 'pnf-sim-lightweight')
291 self.logger.error('POM file was not found, Maven cannot run')
295 self.logger.info('Cleaning simulators workdirs')
296 for sim_id in range(self.existing_sim_instances):
297 rmtree(f"{self.sim_dirname_pattern}{sim_id}")
300 for i in range(*self._get_iter_range()):
301 # If container is not running
302 if f"{self.sim_container_name}-{i}" not in self._get_docker_containers():
303 self.logger.info(f'Starting {self.sim_dirname_pattern}{i} instance:')
304 self.logger.info(f' PNF-Sim IP: {self._get_sim_instance_data(i)}')
305 #Move logs to archive
306 self._archive_logs(self.sim_dirname_pattern + str(i))
307 self.logger.info(' Starting simulator containers using netconf model specified in config/netconf.env')
308 self._run_cmd('docker-compose up -d', self.sim_dirname_pattern + str(i))
310 self.logger.warning(f'Instance {self.sim_dirname_pattern}{i} containers are already up')
313 for i in range(*self._get_iter_range()):
314 self.logger.info(f'Getting {self.sim_dirname_pattern}{i} instance status:')
315 if f"{self.sim_container_name}-{i}" in self._get_docker_containers():
317 sim_ip = self._get_sim_instance_data(i)
318 self.logger.info(f' PNF-Sim IP: {sim_ip}')
319 self._run_cmd(self.docker_compose_status_cmd, f"{self.sim_dirname_pattern}{i}")
320 sim_response = get('{}'.format(self.sim_status_url).format(sim_ip))
321 if sim_response.status_code == codes.ok:
322 self.logger.info(sim_response.text)
324 self.logger.error(f'Simulator request returned http code {sim_response.status_code}')
326 self.logger.error(f'Unable to get sim instance IP from {self.sim_config}')
328 self.logger.info(' Simulator containers are down')
331 for i in range(*self._get_iter_range()):
332 self.logger.info(f'Stopping {self.sim_dirname_pattern}{i} instance:')
333 self.logger.info(f' PNF-Sim IP: {self._get_sim_instance_data(i)}')
334 # attempt killing ROP script
336 for ps_line in iter(popen(f'ps --no-headers -C {self.rop_script_name} -o pid,cmd').readline, ''):
337 # try getting ROP script pid
339 ps_line_arr = ps_line.split()
340 assert self.rop_script_name in ps_line_arr[2]
341 assert ps_line_arr[3] == str(i)
342 rop_pid = ps_line_arr[0]
343 except AssertionError:
346 # get rop script childs, kill ROP script and all childs
347 childs = popen(f'pgrep -P {rop_pid}').read().split()
348 for pid in [rop_pid] + childs:
350 self.logger.info(f' ROP_file_creator.sh {i} successfully killed')
353 self.logger.warning(f' ROP_file_creator.sh {i} already not running')
354 # try tearing down docker-compose application
355 if f"{self.sim_container_name}-{i}" in self._get_docker_containers():
356 self._run_cmd('docker-compose down', self.sim_dirname_pattern + str(i))
357 self._run_cmd('docker-compose rm', self.sim_dirname_pattern + str(i))
359 self.logger.warning(" Simulator containers are already down")
362 self.logger.info("Triggering VES sending:")
363 for i in range(*self._get_iter_range()):
364 sim_ip = self._get_sim_instance_data(i)
365 self.logger.info(f'Triggering {self.sim_dirname_pattern}{i} instance:')
366 self.logger.info(f' PNF-Sim IP: {sim_ip}')
369 "Content-Type": "application/json",
370 "X-ONAP-RequestID": "123",
371 "X-InvocationID": "456"
373 self.logger.debug(f' Request headers: {req_headers}')
375 # get payload for the request
376 with open(f'{self.sim_dirname_pattern}{i}/{self.sim_msg_config}') as data:
377 json_data = loads(data.read())
378 self.logger.debug(f' JSON payload for the simulator:\n{json_data}')
379 # make a http request to the simulator
380 sim_response = post('{}'.format(self.sim_start_url).format(sim_ip), headers=req_headers, json=json_data)
381 if sim_response.status_code == codes.ok:
382 self.logger.info(' Simulator response: ' + sim_response.text)
384 self.logger.warning(' Simulator response ' + sim_response.text)
386 self.logger.error(f' Could not load JSON data from {self.sim_dirname_pattern}{i}/{self.sim_msg_config}')
388 # Make the 'trigger_custom' an alias to the 'trigger' method
389 trigger_custom = trigger
391 def stop_simulator(self):
392 self.logger.info("Stopping sending PNF registration messages:")
393 for i in range(*self._get_iter_range()):
394 sim_ip = self._get_sim_instance_data(i)
395 self.logger.info(f'Stopping {self.sim_dirname_pattern}{i} instance:')
396 self.logger.info(f' PNF-Sim IP: {sim_ip}')
397 sim_response = post('{}'.format(self.sim_stop_url).format(sim_ip))
398 if sim_response.status_code == codes.ok:
399 self.logger.info(' Simulator response: ' + sim_response.text)
401 self.logger.warning(' Simulator response ' + sim_response.text)