Add a method to enumerate bootstrapped simulator instances
[integration.git] / test / mocks / mass-pnf-sim / MassPnfSim.py
1 #!/usr/bin/env python3
2 import logging
3 import subprocess
4 import argparse
5 import ipaddress
6 from sys import exit
7 from os import chdir, getcwd, path
8 from shutil import copytree
9 from json import dumps
10 from glob import glob
11 from requests import get
12 from requests.exceptions import MissingSchema, InvalidSchema, InvalidURL, ConnectionError, ConnectTimeout
13
14 def validate_url(url):
15     '''Helper function to perform --urlves input param validation'''
16     logger = logging.getLogger("urllib3")
17     logger.setLevel(logging.WARNING)
18     try:
19         get(url, timeout=0.001)
20     except (MissingSchema, InvalidSchema, InvalidURL):
21         raise argparse.ArgumentTypeError(f'{url} is not a valid URL')
22     except (ConnectionError, ConnectTimeout):
23         pass
24     return url
25
26 def validate_ip(ip):
27     '''Helper function to validate input param is a vaild IP address'''
28     try:
29         ip_valid = ipaddress.ip_address(ip)
30     except ValueError:
31         raise argparse.ArgumentTypeError(f'{ip} is not a valid IP address')
32     else:
33         return ip_valid
34
35 def get_parser():
36     '''Process input arguments'''
37
38     parser = argparse.ArgumentParser()
39     subparsers = parser.add_subparsers(title='Subcommands', dest='subcommand')
40     # Build command parser
41     subparsers.add_parser('build', help='Build simulator image')
42     # Bootstrap command parser
43     parser_bootstrap = subparsers.add_parser('bootstrap', help='Bootstrap the system')
44     parser_bootstrap.add_argument('--count', help='Instance count to bootstrap', type=int, metavar='INT', default=1)
45     parser_bootstrap.add_argument('--urlves', help='URL of the VES collector', type=validate_url, metavar='URL', required=True)
46     parser_bootstrap.add_argument('--ipfileserver', help='Visible IP of the file server (SFTP/FTPS) to be included in the VES event',
47                                   type=validate_ip, metavar='IP', required=True)
48     parser_bootstrap.add_argument('--typefileserver', help='Type of the file server (SFTP/FTPS) to be included in the VES event',
49                                   type=str, choices=['sftp', 'ftps'], required=True)
50     parser_bootstrap.add_argument('--ipstart', help='IP address range beginning', type=validate_ip, metavar='IP', required=True)
51     # Start command parser
52     parser_start = subparsers.add_parser('start', help='Start instances')
53     parser_start.add_argument('--count', help='Instance count to start', type=int, metavar='INT', default=1)
54     # Stop command parser
55     parser_stop = subparsers.add_parser('stop', help='Stop instances')
56     parser_stop.add_argument('--count', help='Instance count to stop', type=int, metavar='INT', default=1)
57     # Trigger command parser
58     parser_trigger = subparsers.add_parser('trigger', help='Trigger one single VES event from each simulator')
59     parser_trigger.add_argument('--count', help='Instance count to trigger', type=int, metavar='INT', default=1)
60     # Trigger-custom command parser
61     parser_triggerstart = subparsers.add_parser('trigger_custom', help='Trigger one single VES event from specific simulators')
62     parser_triggerstart.add_argument('--triggerstart', help='First simulator id to trigger', type=int,
63                                      metavar='INT', required=True)
64     parser_triggerstart.add_argument('--triggerend', help='Last simulator id to trigger', type=int,
65                                      metavar='INT', required=True)
66     # Status command parser
67     parser_status = subparsers.add_parser('status', help='Status')
68     parser_status.add_argument('--count', help='Instance count to show status for', type=int, metavar='INT', default=1)
69     # Clean command parser
70     subparsers.add_parser('clean', help='Clean work-dirs')
71     # General options parser
72     parser.add_argument('--verbose', help='Verbosity level', choices=['info', 'debug'],
73                         type=str, default='info')
74     return parser
75
76 class MassPnfSim:
77
78     # MassPnfSim class actions decorator
79     class _MassPnfSim_Decorators:
80
81         @staticmethod
82         def do_action(action_string, cmd):
83             def action_decorator(method):
84                 def action_wrap(self):
85                     cmd_local = cmd
86                     # Append instance # if action is 'stop'
87                     if method.__name__ == 'stop':
88                         cmd_local += " {}"
89                     # Alter looping range if action is 'tigger_custom'
90                     if method.__name__ == 'trigger_custom':
91                         iter_range = [self.args.triggerstart, self.args.triggerend+1]
92                     else:
93                         iter_range = [self.args.count]
94                     method(self)
95                     for i in range(*iter_range):
96                         self.logger.info(f'{action_string} {self.sim_dirname_pattern}{i} instance:')
97                         self._run_cmd(cmd_local.format(i), f"{self.sim_dirname_pattern}{i}")
98                 return action_wrap
99             return action_decorator
100
101     log_lvl = logging.INFO
102
103     def __init__(self, args):
104         self.args = args
105         self.logger = logging.getLogger(__name__)
106         self.logger.setLevel(self.log_lvl)
107         self.sim_dirname_pattern = "pnf-sim-lw-"
108         self.mvn_build_cmd = 'mvn clean package docker:build -Dcheckstyle.skip'
109         self.existing_sim_instances = self._enum_sim_instances()
110
111     def _run_cmd(self, cmd, dir_context='.'):
112         if self.args.verbose == 'debug':
113             cmd='bash -x ' + cmd
114         old_pwd = getcwd()
115         try:
116             chdir(dir_context)
117             subprocess.run(cmd, check=True, shell=True)
118             chdir(old_pwd)
119         except FileNotFoundError:
120             self.logger.error(f"Directory {dir_context} not found")
121         except subprocess.CalledProcessError as e:
122             exit(e.returncode)
123
124     def _enum_sim_instances(self):
125         '''Helper method that returns bootstraped simulator instances count'''
126         return len(glob(f"{self.sim_dirname_pattern}[0-9]*"))
127
128     def bootstrap(self):
129         self.logger.info("Bootstrapping PNF instances")
130
131         start_port = 2000
132         ftps_pasv_port_start = 8000
133         ftps_pasv_port_num_of_ports = 10
134
135         ftps_pasv_port_end = ftps_pasv_port_start + ftps_pasv_port_num_of_ports
136
137         for i in range(self.args.count):
138             self.logger.info(f"PNF simulator instance: {i}")
139
140             # The IP ranges are in distance of 16 compared to each other.
141             # This is matching the /28 subnet mask used in the dockerfile inside.
142             instance_ip_offset = i * 16
143             ip_properties = [
144                       'subnet',
145                       'gw',
146                       'PnfSim',
147                       'ftps',
148                       'sftp'
149                     ]
150
151             ip_offset = 0
152             ip = {}
153             for prop in ip_properties:
154                 ip.update({prop: str(self.args.ipstart + ip_offset + instance_ip_offset)})
155                 ip_offset += 1
156
157             self.logger.debug(f'Instance #{i} properties:\n {dumps(ip, indent=4)}')
158
159             PortSftp = start_port + 1
160             PortFtps = start_port + 2
161             start_port += 2
162
163             self.logger.info(f'\tCreating {self.sim_dirname_pattern}{i}')
164             try:
165                 copytree('pnf-sim-lightweight', f'{self.sim_dirname_pattern}{i}')
166             except FileExistsError:
167                 self.logger.error(f'Directory {self.sim_dirname_pattern}{i} already exists, cannot overwrite.')
168                 exit(1)
169
170             composercmd = " ".join([
171                     "./simulator.sh compose",
172                     ip['gw'],
173                     ip['subnet'],
174                     str(i),
175                     self.args.urlves,
176                     ip['PnfSim'],
177                     str(self.args.ipfileserver),
178                     self.args.typefileserver,
179                     str(PortSftp),
180                     str(PortFtps),
181                     ip['ftps'],
182                     ip['sftp'],
183                     str(ftps_pasv_port_start),
184                     str(ftps_pasv_port_end)
185                 ])
186             self.logger.debug(f"Script cmdline: {composercmd}")
187             self.logger.info(f"\tCreating instance #{i} configuration ")
188             self._run_cmd(composercmd, f"{self.sim_dirname_pattern}{i}")
189
190             ftps_pasv_port_start += ftps_pasv_port_num_of_ports + 1
191             ftps_pasv_port_end += ftps_pasv_port_num_of_ports + 1
192
193             self.logger.info(f'Done setting up instance #{i}')
194
195     def build(self):
196         self.logger.info("Building simulator image")
197         if path.isfile('pnf-sim-lightweight/pom.xml'):
198             self._run_cmd(self.mvn_build_cmd, 'pnf-sim-lightweight')
199         else:
200             self.logger.error('POM file was not found, Maven cannot run')
201             exit(1)
202
203     def clean(self):
204         self.logger.info('Cleaning simulators workdirs')
205         self._run_cmd(f"rm -rf {self.sim_dirname_pattern}*")
206
207     @_MassPnfSim_Decorators.do_action('Starting', './simulator.sh start')
208     def start(self):
209         pass
210
211     @_MassPnfSim_Decorators.do_action('Getting', './simulator.sh status')
212     def status(self):
213         pass
214
215     @_MassPnfSim_Decorators.do_action('Stopping', './simulator.sh stop')
216     def stop(self):
217         pass
218
219     @_MassPnfSim_Decorators.do_action('Triggering', './simulator.sh trigger-simulator')
220     def trigger(self):
221         self.logger.info("Triggering VES sending:")
222
223     @_MassPnfSim_Decorators.do_action('Triggering', './simulator.sh trigger-simulator')
224     def trigger_custom(self):
225         self.logger.info("Triggering VES sending by a range of simulators:")