3 from subprocess import run, CalledProcessError
7 from os import chdir, getcwd, path, popen, kill
8 from shutil import copytree, rmtree
9 from json import loads, dumps
10 from yaml import load, SafeLoader
12 from docker import from_env
13 from requests import get, codes, post
14 from requests.exceptions import MissingSchema, InvalidSchema, InvalidURL, ConnectionError, ConnectTimeout
16 def validate_url(url):
17 '''Helper function to perform --urlves input param validation'''
18 logger = logging.getLogger("urllib3")
19 logger.setLevel(logging.WARNING)
21 get(url, timeout=0.001)
22 except (MissingSchema, InvalidSchema, InvalidURL):
23 raise argparse.ArgumentTypeError(f'{url} is not a valid URL')
24 except (ConnectionError, ConnectTimeout):
29 '''Helper function to validate input param is a vaild IP address'''
31 ip_valid = ipaddress.ip_address(ip)
33 raise argparse.ArgumentTypeError(f'{ip} is not a valid IP address')
38 '''Process input arguments'''
40 parser = argparse.ArgumentParser()
41 subparsers = parser.add_subparsers(title='Subcommands', dest='subcommand')
42 # Build command parser
43 subparsers.add_parser('build', help='Build simulator image')
44 # Bootstrap command parser
45 parser_bootstrap = subparsers.add_parser('bootstrap', help='Bootstrap the system')
46 parser_bootstrap.add_argument('--count', help='Instance count to bootstrap', type=int, metavar='INT', default=1)
47 parser_bootstrap.add_argument('--urlves', help='URL of the VES collector', type=validate_url, metavar='URL', required=True)
48 parser_bootstrap.add_argument('--ipfileserver', help='Visible IP of the file server (SFTP/FTPS) to be included in the VES event',
49 type=validate_ip, metavar='IP', required=True)
50 parser_bootstrap.add_argument('--typefileserver', help='Type of the file server (SFTP/FTPS) to be included in the VES event',
51 type=str, choices=['sftp', 'ftps'], required=True)
52 parser_bootstrap.add_argument('--ipstart', help='IP address range beginning', type=validate_ip, metavar='IP', required=True)
53 # Start command parser
54 parser_start = subparsers.add_parser('start', help='Start instances')
55 parser_start.add_argument('--count', help='Instance count to start', type=int, metavar='INT', default=0)
57 parser_stop = subparsers.add_parser('stop', help='Stop instances')
58 parser_stop.add_argument('--count', help='Instance count to stop', type=int, metavar='INT', default=0)
59 # Trigger command parser
60 parser_trigger = subparsers.add_parser('trigger', help='Trigger one single VES event from each simulator')
61 parser_trigger.add_argument('--count', help='Instance count to trigger', type=int, metavar='INT', default=0)
62 # Stop-simulator command parser
63 parser_stopsimulator = subparsers.add_parser('stop_simulator', help='Stop sending PNF registration messages')
64 parser_stopsimulator.add_argument('--count', help='Instance count to stop', type=int, metavar='INT', default=0)
65 # Trigger-custom command parser
66 parser_triggerstart = subparsers.add_parser('trigger_custom', help='Trigger one single VES event from specific simulators')
67 parser_triggerstart.add_argument('--triggerstart', help='First simulator id to trigger', type=int,
68 metavar='INT', required=True)
69 parser_triggerstart.add_argument('--triggerend', help='Last simulator id to trigger', type=int,
70 metavar='INT', required=True)
71 # Status command parser
72 parser_status = subparsers.add_parser('status', help='Status')
73 parser_status.add_argument('--count', help='Instance count to show status for', type=int, metavar='INT', default=0)
74 # Clean command parser
75 subparsers.add_parser('clean', help='Clean work-dirs')
76 # General options parser
77 parser.add_argument('--verbose', help='Verbosity level', choices=['info', 'debug'],
78 type=str, default='info')
83 # MassPnfSim class actions decorator
84 class _MassPnfSim_Decorators:
87 def do_action(action_string, cmd):
88 def action_decorator(method):
89 def action_wrap(self):
90 # Alter looping range if action is 'tigger_custom'
91 if method.__name__ == 'trigger_custom':
92 iter_range = [self.args.triggerstart, self.args.triggerend+1]
94 if not self.args.count:
95 # If no instance count set explicitly via --count
97 iter_range = [self.existing_sim_instances]
99 iter_range = [self.args.count]
101 for i in range(*iter_range):
102 self.logger.info(f'{action_string} {self.sim_dirname_pattern}{i} instance:')
103 self._run_cmd(cmd, f"{self.sim_dirname_pattern}{i}")
105 return action_decorator
107 log_lvl = logging.INFO
108 sim_config = 'config/config.yml'
109 sim_msg_config = 'config/config.json'
111 sim_base_url = 'http://{}:' + str(sim_port) + '/simulator'
112 sim_start_url = sim_base_url + '/start'
113 sim_status_url = sim_base_url + '/status'
114 sim_stop_url = sim_base_url + '/stop'
115 sim_container_name = 'pnf-simulator'
116 rop_script_name = 'ROP_file_creator.sh'
118 def __init__(self, args):
120 self.logger = logging.getLogger(__name__)
121 self.logger.setLevel(self.log_lvl)
122 self.sim_dirname_pattern = "pnf-sim-lw-"
123 self.mvn_build_cmd = 'mvn clean package docker:build -Dcheckstyle.skip'
124 self.docker_compose_status_cmd = 'docker-compose ps'
125 self.existing_sim_instances = self._enum_sim_instances()
127 # Validate 'trigger_custom' subcommand options
128 if self.args.subcommand == 'trigger_custom':
129 if (self.args.triggerend + 1) > self.existing_sim_instances:
130 self.logger.error('--triggerend value greater than existing instance count.')
133 # Validate --count option for subcommands that support it
134 if self.args.subcommand in ['start', 'stop', 'trigger', 'status', 'stop_simulator']:
135 if self.args.count > self.existing_sim_instances:
136 self.logger.error('--count value greater that existing instance count')
138 if not self.existing_sim_instances:
139 self.logger.error('No bootstrapped instance found')
142 # Validate 'bootstrap' subcommand
143 if (self.args.subcommand == 'bootstrap') and self.existing_sim_instances:
144 self.logger.error('Bootstrapped instances detected, not overwiriting, clean first')
147 def _run_cmd(self, cmd, dir_context='.'):
151 self.logger.debug(f'_run_cmd: Current direcotry: {getcwd()}')
152 self.logger.debug(f'_run_cmd: Command string: {cmd}')
153 run(cmd, check=True, shell=True)
155 except FileNotFoundError:
156 self.logger.error(f"Directory {dir_context} not found")
157 except CalledProcessError as e:
160 def _enum_sim_instances(self):
161 '''Helper method that returns bootstraped simulator instances count'''
162 return len(glob(f"{self.sim_dirname_pattern}[0-9]*"))
164 def _get_sim_instance_data(self, instance_id):
165 '''Helper method that returns specific instance data'''
167 chdir(f"{self.sim_dirname_pattern}{instance_id}")
168 with open(self.sim_config) as cfg:
169 yml = load(cfg, Loader=SafeLoader)
171 return yml['ippnfsim']
173 def _get_docker_containers(self):
174 '''Returns a list containing 'name' attribute of running docker containers'''
177 for container in dc.containers.list():
178 containers.append(container.attrs['Name'][1:])
181 def _get_iter_range(self):
182 '''Helper routine to get the iteration range
183 for the lifecycle commands'''
184 if hasattr(self.args, 'count'):
185 if not self.args.count:
186 return [self.existing_sim_instances]
188 return [self.args.count]
189 elif hasattr(self.args, 'triggerstart'):
190 return [self.args.triggerstart, self.args.triggerend + 1]
192 return [self.existing_sim_instances]
195 self.logger.info("Bootstrapping PNF instances")
198 ftps_pasv_port_start = 8000
199 ftps_pasv_port_num_of_ports = 10
201 ftps_pasv_port_end = ftps_pasv_port_start + ftps_pasv_port_num_of_ports
203 for i in range(self.args.count):
204 self.logger.info(f"PNF simulator instance: {i}")
206 # The IP ranges are in distance of 16 compared to each other.
207 # This is matching the /28 subnet mask used in the dockerfile inside.
208 instance_ip_offset = i * 16
219 for prop in ip_properties:
220 ip.update({prop: str(self.args.ipstart + ip_offset + instance_ip_offset)})
223 self.logger.debug(f'Instance #{i} properties:\n {dumps(ip, indent=4)}')
225 PortSftp = start_port + 1
226 PortFtps = start_port + 2
229 self.logger.info(f'\tCreating {self.sim_dirname_pattern}{i}')
230 copytree('pnf-sim-lightweight', f'{self.sim_dirname_pattern}{i}')
232 composercmd = " ".join([
233 "./simulator.sh compose",
239 str(self.args.ipfileserver),
240 self.args.typefileserver,
245 str(ftps_pasv_port_start),
246 str(ftps_pasv_port_end)
248 self.logger.debug(f"Script cmdline: {composercmd}")
249 self.logger.info(f"\tCreating instance #{i} configuration ")
250 self._run_cmd(composercmd, f"{self.sim_dirname_pattern}{i}")
252 ftps_pasv_port_start += ftps_pasv_port_num_of_ports + 1
253 ftps_pasv_port_end += ftps_pasv_port_num_of_ports + 1
255 self.logger.info(f'Done setting up instance #{i}')
258 self.logger.info("Building simulator image")
259 if path.isfile('pnf-sim-lightweight/pom.xml'):
260 self._run_cmd(self.mvn_build_cmd, 'pnf-sim-lightweight')
262 self.logger.error('POM file was not found, Maven cannot run')
266 self.logger.info('Cleaning simulators workdirs')
267 for sim_id in range(self.existing_sim_instances):
268 rmtree(f"{self.sim_dirname_pattern}{sim_id}")
270 @_MassPnfSim_Decorators.do_action('Starting', './simulator.sh start')
275 for i in range(*self._get_iter_range()):
276 self.logger.info(f'Getting {self.sim_dirname_pattern}{i} instance status:')
277 if f"{self.sim_container_name}-{i}" in self._get_docker_containers():
279 sim_ip = self._get_sim_instance_data(i)
280 self.logger.info(f' PNF-Sim IP: {sim_ip}')
281 self._run_cmd(self.docker_compose_status_cmd, f"{self.sim_dirname_pattern}{i}")
282 sim_response = get('{}'.format(self.sim_status_url).format(sim_ip))
283 if sim_response.status_code == codes.ok:
284 self.logger.info(sim_response.text)
286 self.logger.error(f'Simulator request returned http code {sim_response.status_code}')
288 self.logger.error(f'Unable to get sim instance IP from {self.sim_config}')
290 self.logger.info(' Simulator containers are down')
293 for i in range(*self._get_iter_range()):
294 self.logger.info(f'Stopping {self.sim_dirname_pattern}{i} instance:')
295 self.logger.info(f' PNF-Sim IP: {self._get_sim_instance_data(i)}')
296 # attempt killing ROP script
298 for ps_line in iter(popen(f'ps --no-headers -C {self.rop_script_name} -o pid,cmd').readline, ''):
299 # try getting ROP script pid
301 ps_line_arr = ps_line.split()
302 assert self.rop_script_name in ps_line_arr[2]
303 assert ps_line_arr[3] == str(i)
304 rop_pid = ps_line_arr[0]
305 except AssertionError:
308 # get rop script childs, kill ROP script and all childs
309 childs = popen(f'pgrep -P {rop_pid}').read().split()
310 for pid in [rop_pid] + childs:
312 self.logger.info(f' ROP_file_creator.sh {i} successfully killed')
315 self.logger.warning(f' ROP_file_creator.sh {i} already not running')
316 # try tearing down docker-compose application
317 if f"{self.sim_container_name}-{i}" in self._get_docker_containers():
318 self._run_cmd('docker-compose down', self.sim_dirname_pattern + str(i))
319 self._run_cmd('docker-compose rm', self.sim_dirname_pattern + str(i))
321 self.logger.warning(" Simulator containers are already down")
324 self.logger.info("Triggering VES sending:")
325 for i in range(*self._get_iter_range()):
326 sim_ip = self._get_sim_instance_data(i)
327 self.logger.info(f'Triggering {self.sim_dirname_pattern}{i} instance:')
328 self.logger.info(f' PNF-Sim IP: {sim_ip}')
331 "Content-Type": "application/json",
332 "X-ONAP-RequestID": "123",
333 "X-InvocationID": "456"
335 self.logger.debug(f' Request headers: {req_headers}')
337 # get payload for the request
338 with open(f'{self.sim_dirname_pattern}{i}/{self.sim_msg_config}') as data:
339 json_data = loads(data.read())
340 self.logger.debug(f' JSON payload for the simulator:\n{json_data}')
341 # make a http request to the simulator
342 sim_response = post('{}'.format(self.sim_start_url).format(sim_ip), headers=req_headers, json=json_data)
343 if sim_response.status_code == codes.ok:
344 self.logger.info(' Simulator response: ' + sim_response.text)
346 self.logger.warning(' Simulator response ' + sim_response.text)
348 self.logger.error(f' Could not load JSON data from {self.sim_dirname_pattern}{i}/{self.sim_msg_config}')
350 # Make the 'trigger_custom' an alias to the 'trigger' method
351 trigger_custom = trigger
353 def stop_simulator(self):
354 self.logger.info("Stopping sending PNF registration messages:")
355 for i in range(*self._get_iter_range()):
356 sim_ip = self._get_sim_instance_data(i)
357 self.logger.info(f'Stopping {self.sim_dirname_pattern}{i} instance:')
358 self.logger.info(f' PNF-Sim IP: {sim_ip}')
359 sim_response = post('{}'.format(self.sim_stop_url).format(sim_ip))
360 if sim_response.status_code == codes.ok:
361 self.logger.info(' Simulator response: ' + sim_response.text)
363 self.logger.warning(' Simulator response ' + sim_response.text)