3 from subprocess import run, CalledProcessError
7 from os import chdir, getcwd, path, popen, kill, getuid, stat, mkdir, getlogin, chmod
8 from shutil import copytree, rmtree, move
9 from json import loads, dumps
10 from yaml import load, SafeLoader, dump
12 from time import strftime, tzname, daylight
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('--user', help='File server username', type=str, metavar='USERNAME', required=True)
54 parser_bootstrap.add_argument('--password', help='File server password', type=str, metavar='PASSWORD', required=True)
55 parser_bootstrap.add_argument('--ipstart', help='IP address range beginning', type=validate_ip, metavar='IP', required=True)
56 # Start command parser
57 parser_start = subparsers.add_parser('start', help='Start instances')
58 parser_start.add_argument('--count', help='Instance count to start', type=int, metavar='INT', default=0)
60 parser_stop = subparsers.add_parser('stop', help='Stop instances')
61 parser_stop.add_argument('--count', help='Instance count to stop', type=int, metavar='INT', default=0)
62 # Trigger command parser
63 parser_trigger = subparsers.add_parser('trigger', help='Trigger one single VES event from each simulator')
64 parser_trigger.add_argument('--count', help='Instance count to trigger', type=int, metavar='INT', default=0)
65 # Stop-simulator command parser
66 parser_stopsimulator = subparsers.add_parser('stop_simulator', help='Stop sending PNF registration messages')
67 parser_stopsimulator.add_argument('--count', help='Instance count to stop', type=int, metavar='INT', default=0)
68 # Trigger-custom command parser
69 parser_triggerstart = subparsers.add_parser('trigger_custom', help='Trigger one single VES event from specific simulators')
70 parser_triggerstart.add_argument('--triggerstart', help='First simulator id to trigger', type=int,
71 metavar='INT', required=True)
72 parser_triggerstart.add_argument('--triggerend', help='Last simulator id to trigger', type=int,
73 metavar='INT', required=True)
74 # Status command parser
75 parser_status = subparsers.add_parser('status', help='Status')
76 parser_status.add_argument('--count', help='Instance count to show status for', type=int, metavar='INT', default=0)
77 # Clean command parser
78 subparsers.add_parser('clean', help='Clean work-dirs')
79 # General options parser
80 parser.add_argument('--verbose', help='Verbosity level', choices=['info', 'debug'],
81 type=str, default='info')
86 # MassPnfSim class actions decorator
87 class _MassPnfSim_Decorators:
89 def validate_subcommand(method):
91 # Validate 'trigger_custom' subcommand options
92 if self.args.subcommand == 'trigger_custom':
93 if (self.args.triggerend + 1) > self._enum_sim_instances():
94 self.logger.error('--triggerend value greater than existing instance count.')
97 # Validate --count option for subcommands that support it
98 if self.args.subcommand in ['start', 'stop', 'trigger', 'status', 'stop_simulator']:
99 if self.args.count > self._enum_sim_instances():
100 self.logger.error('--count value greater that existing instance count')
102 if not self._enum_sim_instances():
103 self.logger.error('No bootstrapped instance found')
106 # Validate 'bootstrap' subcommand
107 if (self.args.subcommand == 'bootstrap') and self._enum_sim_instances():
108 self.logger.error('Bootstrapped instances detected, not overwiriting, clean first')
113 log_lvl = logging.INFO
114 sim_compose_template = 'docker-compose-template.yml'
115 sim_vsftpd_template = 'config/vsftpd_ssl-TEMPLATE.conf'
116 sim_vsftpd_config = 'config/vsftpd_ssl.conf'
117 sim_sftp_script = 'fix-sftp-perms.sh'
118 sim_sftp_script_template = 'fix-sftp-perms-template.sh'
119 sim_config = 'config/config.yml'
120 sim_msg_config = 'config/config.json'
122 sim_base_url = 'http://{}:' + str(sim_port) + '/simulator'
123 sim_start_url = sim_base_url + '/start'
124 sim_status_url = sim_base_url + '/status'
125 sim_stop_url = sim_base_url + '/stop'
126 sim_container_name = 'pnf-simulator'
127 rop_script_name = 'ROP_file_creator.sh'
129 def __init__(self, args):
131 self.logger = logging.getLogger(__name__)
132 self.logger.setLevel(self.log_lvl)
133 self.sim_dirname_pattern = "pnf-sim-lw-"
134 self.mvn_build_cmd = 'mvn clean package docker:build -Dcheckstyle.skip'
135 self.docker_compose_status_cmd = 'docker-compose ps'
137 def _run_cmd(self, cmd, dir_context='.'):
141 self.logger.debug(f'_run_cmd: Current direcotry: {getcwd()}')
142 self.logger.debug(f'_run_cmd: Command string: {cmd}')
143 run(cmd, check=True, shell=True)
145 except FileNotFoundError:
146 self.logger.error(f"Directory {dir_context} not found")
147 except CalledProcessError as e:
150 def _enum_sim_instances(self):
151 '''Helper method that returns bootstraped simulator instances count'''
152 return len(glob(f"{self.sim_dirname_pattern}[0-9]*"))
154 def _get_sim_instance_data(self, instance_id):
155 '''Helper method that returns specific instance data'''
157 chdir(f"{self.sim_dirname_pattern}{instance_id}")
158 with open(self.sim_config) as cfg:
159 yml = load(cfg, Loader=SafeLoader)
161 return yml['ippnfsim']
163 def _get_docker_containers(self):
164 '''Returns a list containing 'name' attribute of running docker containers'''
167 for container in dc.containers.list():
168 containers.append(container.attrs['Name'][1:])
171 def _get_iter_range(self):
172 '''Helper routine to get the iteration range
173 for the lifecycle commands'''
174 if hasattr(self.args, 'count'):
175 if not self.args.count:
176 return [self._enum_sim_instances()]
178 return [self.args.count]
179 elif hasattr(self.args, 'triggerstart'):
180 return [self.args.triggerstart, self.args.triggerend + 1]
182 return [self._enum_sim_instances()]
184 def _archive_logs(self, sim_dir):
185 '''Helper function to archive simulator logs or create the log dir'''
189 if path.isdir('logs'):
190 arch_dir = f"logs/archive_{strftime('%Y-%m-%d_%T')}"
192 self.logger.debug(f'Created {arch_dir}')
193 # Collect file list to move
194 self.logger.debug('Archiving log files')
195 for fpattern in ['*.log', '*.xml']:
196 for f in glob('logs/' + fpattern):
197 # Move files from list to arch dir
199 self.logger.debug(f'Moving {f} to {arch_dir}')
202 self.logger.debug("Logs dir didn't exist, created")
204 except FileNotFoundError:
205 self.logger.error(f"Directory {sim_dir} not found")
207 def _generate_pnf_sim_config(self, i, port_sftp, port_ftps, pnf_sim_ip):
208 '''Writes a yaml formatted configuration file for Java simulator app'''
210 yml['urlves'] = self.args.urlves
211 yml['urlsftp'] = f'sftp://{self.args.user}:{self.args.password}@{self.args.ipfileserver}:{port_sftp}'
212 yml['urlftps'] = f'ftps://{self.args.user}:{self.args.password}@{self.args.ipfileserver}:{port_ftps}'
213 yml['ippnfsim'] = pnf_sim_ip
214 yml['typefileserver'] = self.args.typefileserver
215 self.logger.debug(f'Generated simulator config:\n{dump(yml)}')
216 with open(f'{self.sim_dirname_pattern}{i}/{self.sim_config}', 'w') as fout:
217 fout.write(dump(yml))
219 def _generate_config_file(self, source, dest, **kwargs):
220 '''Helper private method to generate a file based on a template'''
222 chdir(self.sim_dirname_pattern + str(kwargs['I']))
223 # Read the template file
224 with open(source, 'r') as f:
226 # Replace all occurences of env like variable with it's
227 # relevant value from a corresponding key form kwargs
228 for (k,v) in kwargs.items():
229 template = template.replace('${' + k + '}', str(v))
230 with open(dest, 'w') as f:
234 @_MassPnfSim_Decorators.validate_subcommand
236 self.logger.info("Bootstrapping PNF instances")
239 ftps_pasv_port_start = 8000
240 ftps_pasv_port_num_of_ports = 10
242 ftps_pasv_port_end = ftps_pasv_port_start + ftps_pasv_port_num_of_ports
244 for i in range(self.args.count):
245 self.logger.info(f"PNF simulator instance: {i}")
247 # The IP ranges are in distance of 16 compared to each other.
248 # This is matching the /28 subnet mask used in the dockerfile inside.
249 instance_ip_offset = i * 16
260 for prop in ip_properties:
261 ip.update({prop: str(self.args.ipstart + ip_offset + instance_ip_offset)})
264 self.logger.debug(f'Instance #{i} properties:\n {dumps(ip, indent=4)}')
266 PortSftp = start_port + 1
267 PortFtps = start_port + 2
270 self.logger.info(f'\tCreating {self.sim_dirname_pattern}{i}')
271 copytree('pnf-sim-lightweight', f'{self.sim_dirname_pattern}{i}')
272 self.logger.info(f"\tCreating instance #{i} configuration ")
273 self._generate_pnf_sim_config(i, PortSftp, PortFtps, ip['PnfSim'])
274 # generate docker-compose for the simulator instance
275 self._generate_config_file(self.sim_compose_template, 'docker-compose.yml',
276 IPGW = ip['gw'], IPSUBNET = ip['subnet'],
277 I = i, IPPNFSIM = ip['PnfSim'],
278 PORTSFTP = str(PortSftp),
279 PORTFTPS = str(PortFtps),
280 IPFTPS = ip['ftps'], IPSFTP = ip['sftp'],
281 FTPS_PASV_MIN = str(ftps_pasv_port_start),
282 FTPS_PASV_MAX = str(ftps_pasv_port_end),
283 TIMEZONE = tzname[daylight],
284 FILESERV_USER = self.args.user,
285 FILESERV_PASS = self.args.password)
286 # generate vsftpd config file for the simulator instance
287 self._generate_config_file(self.sim_vsftpd_template, self.sim_vsftpd_config,
288 I = i, USER = getlogin(),
289 FTPS_PASV_MIN = str(ftps_pasv_port_start),
290 FTPS_PASV_MAX = str(ftps_pasv_port_end),
291 IPFILESERVER = str(self.args.ipfileserver))
292 # generate sftp permission fix script
293 self._generate_config_file(self.sim_sftp_script_template, self.sim_sftp_script,
294 I = i, FILESERV_USER = self.args.user)
295 chmod(f'{self.sim_dirname_pattern}{i}/{self.sim_sftp_script}', 0o755)
296 # Run the 3GPP measurements file generator
297 self._run_cmd(f'./ROP_file_creator.sh {i} &', f"{self.sim_dirname_pattern}{i}")
299 ftps_pasv_port_start += ftps_pasv_port_num_of_ports + 1
300 ftps_pasv_port_end += ftps_pasv_port_num_of_ports + 1
302 # ugly hack to chown vsftpd config file to root
304 self._run_cmd(f'sudo chown root {self.sim_vsftpd_config}', f'{self.sim_dirname_pattern}{i}')
305 self.logger.debug(f"vsftpd config file owner UID: {stat(self.sim_dirname_pattern + str(i) + '/' + self.sim_vsftpd_config).st_uid}")
307 self.logger.info(f'Done setting up instance #{i}')
310 self.logger.info("Building simulator image")
311 if path.isfile('pnf-sim-lightweight/pom.xml'):
312 self._run_cmd(self.mvn_build_cmd, 'pnf-sim-lightweight')
314 self.logger.error('POM file was not found, Maven cannot run')
318 self.logger.info('Cleaning simulators workdirs')
319 for sim_id in range(self._enum_sim_instances()):
320 rmtree(f"{self.sim_dirname_pattern}{sim_id}")
322 @_MassPnfSim_Decorators.validate_subcommand
324 for i in range(*self._get_iter_range()):
325 # If container is not running
326 if f"{self.sim_container_name}-{i}" not in self._get_docker_containers():
327 self.logger.info(f'Starting {self.sim_dirname_pattern}{i} instance:')
328 self.logger.info(f' PNF-Sim IP: {self._get_sim_instance_data(i)}')
329 #Move logs to archive
330 self._archive_logs(self.sim_dirname_pattern + str(i))
331 self.logger.info(' Starting simulator containers using netconf model specified in config/netconf.env')
332 self._run_cmd('docker-compose up -d', self.sim_dirname_pattern + str(i))
334 self.logger.warning(f'Instance {self.sim_dirname_pattern}{i} containers are already up')
336 @_MassPnfSim_Decorators.validate_subcommand
338 for i in range(*self._get_iter_range()):
339 self.logger.info(f'Getting {self.sim_dirname_pattern}{i} instance status:')
340 if f"{self.sim_container_name}-{i}" in self._get_docker_containers():
342 sim_ip = self._get_sim_instance_data(i)
343 self.logger.info(f' PNF-Sim IP: {sim_ip}')
344 self._run_cmd(self.docker_compose_status_cmd, f"{self.sim_dirname_pattern}{i}")
345 sim_response = get('{}'.format(self.sim_status_url).format(sim_ip))
346 if sim_response.status_code == codes.ok:
347 self.logger.info(sim_response.text)
349 self.logger.error(f'Simulator request returned http code {sim_response.status_code}')
351 self.logger.error(f'Unable to get sim instance IP from {self.sim_config}')
353 self.logger.info(' Simulator containers are down')
355 @_MassPnfSim_Decorators.validate_subcommand
357 for i in range(*self._get_iter_range()):
358 self.logger.info(f'Stopping {self.sim_dirname_pattern}{i} instance:')
359 self.logger.info(f' PNF-Sim IP: {self._get_sim_instance_data(i)}')
360 # attempt killing ROP script
362 for ps_line in iter(popen(f'ps --no-headers -C {self.rop_script_name} -o pid,cmd').readline, ''):
363 # try getting ROP script pid
365 ps_line_arr = ps_line.split()
366 assert self.rop_script_name in ps_line_arr[2]
367 assert ps_line_arr[3] == str(i)
368 rop_pid = ps_line_arr[0]
369 except AssertionError:
372 # get rop script childs, kill ROP script and all childs
373 childs = popen(f'pgrep -P {rop_pid}').read().split()
374 for pid in [rop_pid] + childs:
376 self.logger.info(f' ROP_file_creator.sh {i} successfully killed')
379 self.logger.warning(f' ROP_file_creator.sh {i} already not running')
380 # try tearing down docker-compose application
381 if f"{self.sim_container_name}-{i}" in self._get_docker_containers():
382 self._run_cmd('docker-compose down', self.sim_dirname_pattern + str(i))
383 self._run_cmd('docker-compose rm', self.sim_dirname_pattern + str(i))
385 self.logger.warning(" Simulator containers are already down")
387 @_MassPnfSim_Decorators.validate_subcommand
389 self.logger.info("Triggering VES sending:")
390 for i in range(*self._get_iter_range()):
391 sim_ip = self._get_sim_instance_data(i)
392 self.logger.info(f'Triggering {self.sim_dirname_pattern}{i} instance:')
393 self.logger.info(f' PNF-Sim IP: {sim_ip}')
396 "Content-Type": "application/json",
397 "X-ONAP-RequestID": "123",
398 "X-InvocationID": "456"
400 self.logger.debug(f' Request headers: {req_headers}')
402 # get payload for the request
403 with open(f'{self.sim_dirname_pattern}{i}/{self.sim_msg_config}') as data:
404 json_data = loads(data.read())
405 self.logger.debug(f' JSON payload for the simulator:\n{json_data}')
406 # make a http request to the simulator
407 sim_response = post('{}'.format(self.sim_start_url).format(sim_ip), headers=req_headers, json=json_data)
408 if sim_response.status_code == codes.ok:
409 self.logger.info(' Simulator response: ' + sim_response.text)
411 self.logger.warning(' Simulator response ' + sim_response.text)
413 self.logger.error(f' Could not load JSON data from {self.sim_dirname_pattern}{i}/{self.sim_msg_config}')
415 # Make the 'trigger_custom' an alias to the 'trigger' method
416 trigger_custom = trigger
418 @_MassPnfSim_Decorators.validate_subcommand
419 def stop_simulator(self):
420 self.logger.info("Stopping sending PNF registration messages:")
421 for i in range(*self._get_iter_range()):
422 sim_ip = self._get_sim_instance_data(i)
423 self.logger.info(f'Stopping {self.sim_dirname_pattern}{i} instance:')
424 self.logger.info(f' PNF-Sim IP: {sim_ip}')
425 sim_response = post('{}'.format(self.sim_stop_url).format(sim_ip))
426 if sim_response.status_code == codes.ok:
427 self.logger.info(' Simulator response: ' + sim_response.text)
429 self.logger.warning(' Simulator response ' + sim_response.text)