b6a32fe2fdf43e0ff7d78180ba99b815f664a26e
[integration.git] / test / mocks / mass-pnf-sim / MassPnfSim.py
1 #!/usr/bin/env python3
2 import logging
3 from subprocess import run, CalledProcessError
4 import argparse
5 import ipaddress
6 from sys import exit
7 from os import chdir, getcwd, path
8 from shutil import copytree, rmtree
9 from json import dumps
10 from yaml import load, SafeLoader
11 from glob import glob
12 from docker import from_env
13 from requests import get, codes
14 from requests.exceptions import MissingSchema, InvalidSchema, InvalidURL, ConnectionError, ConnectTimeout
15
16 def validate_url(url):
17     '''Helper function to perform --urlves input param validation'''
18     logger = logging.getLogger("urllib3")
19     logger.setLevel(logging.WARNING)
20     try:
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):
25         pass
26     return url
27
28 def validate_ip(ip):
29     '''Helper function to validate input param is a vaild IP address'''
30     try:
31         ip_valid = ipaddress.ip_address(ip)
32     except ValueError:
33         raise argparse.ArgumentTypeError(f'{ip} is not a valid IP address')
34     else:
35         return ip_valid
36
37 def get_parser():
38     '''Process input arguments'''
39
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)
56     # Stop command parser
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     # Trigger-custom command parser
63     parser_triggerstart = subparsers.add_parser('trigger_custom', help='Trigger one single VES event from specific simulators')
64     parser_triggerstart.add_argument('--triggerstart', help='First simulator id to trigger', type=int,
65                                      metavar='INT', required=True)
66     parser_triggerstart.add_argument('--triggerend', help='Last simulator id to trigger', type=int,
67                                      metavar='INT', required=True)
68     # Status command parser
69     parser_status = subparsers.add_parser('status', help='Status')
70     parser_status.add_argument('--count', help='Instance count to show status for', type=int, metavar='INT', default=0)
71     # Clean command parser
72     subparsers.add_parser('clean', help='Clean work-dirs')
73     # General options parser
74     parser.add_argument('--verbose', help='Verbosity level', choices=['info', 'debug'],
75                         type=str, default='info')
76     return parser
77
78 class MassPnfSim:
79
80     # MassPnfSim class actions decorator
81     class _MassPnfSim_Decorators:
82
83         @staticmethod
84         def do_action(action_string, cmd):
85             def action_decorator(method):
86                 def action_wrap(self):
87                     cmd_local = cmd
88                     # Append instance # if action is 'stop'
89                     if method.__name__ == 'stop':
90                         cmd_local += " {}"
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]
94                     else:
95                         if not self.args.count:
96                             # If no instance count set explicitly via --count
97                             # option
98                             iter_range = [self.existing_sim_instances]
99                         else:
100                             iter_range = [self.args.count]
101                     method(self)
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_local.format(i), f"{self.sim_dirname_pattern}{i}")
105                 return action_wrap
106             return action_decorator
107
108     log_lvl = logging.INFO
109     sim_config = 'config/config.yml'
110     sim_port = 5000
111     sim_base_url = 'http://{}:' + str(sim_port) + '/simulator'
112     sim_container_name = 'pnf-simulator'
113
114     def __init__(self, args):
115         self.args = args
116         self.logger = logging.getLogger(__name__)
117         self.logger.setLevel(self.log_lvl)
118         self.sim_dirname_pattern = "pnf-sim-lw-"
119         self.mvn_build_cmd = 'mvn clean package docker:build -Dcheckstyle.skip'
120         self.docker_compose_status_cmd = 'docker-compose ps'
121         self.existing_sim_instances = self._enum_sim_instances()
122
123         # Validate 'trigger_custom' subcommand options
124         if self.args.subcommand == 'trigger_custom':
125             if (self.args.triggerend + 1) > self.existing_sim_instances:
126                 self.logger.error('--triggerend value greater than existing instance count.')
127                 exit(1)
128
129         # Validate --count option for subcommands that support it
130         if self.args.subcommand in ['start', 'stop', 'trigger', 'status']:
131             if self.args.count > self.existing_sim_instances:
132                 self.logger.error('--count value greater that existing instance count')
133                 exit(1)
134             if not self.existing_sim_instances:
135                 self.logger.error('No bootstrapped instance found')
136                 exit(1)
137
138         # Validate 'bootstrap' subcommand
139         if (self.args.subcommand == 'bootstrap') and self.existing_sim_instances:
140             self.logger.error('Bootstrapped instances detected, not overwiriting, clean first')
141             exit(1)
142
143     def _run_cmd(self, cmd, dir_context='.'):
144         if self.args.verbose == 'debug':
145             cmd='bash -x ' + cmd
146         old_pwd = getcwd()
147         try:
148             chdir(dir_context)
149             run(cmd, check=True, shell=True)
150             chdir(old_pwd)
151         except FileNotFoundError:
152             self.logger.error(f"Directory {dir_context} not found")
153         except CalledProcessError as e:
154             exit(e.returncode)
155
156     def _enum_sim_instances(self):
157         '''Helper method that returns bootstraped simulator instances count'''
158         return len(glob(f"{self.sim_dirname_pattern}[0-9]*"))
159
160     def _get_sim_instance_data(self, instance_id):
161         '''Helper method that returns specific instance data'''
162         oldpwd = getcwd()
163         chdir(f"{self.sim_dirname_pattern}{instance_id}")
164         with open(self.sim_config) as cfg:
165             yml = load(cfg, Loader=SafeLoader)
166         chdir(oldpwd)
167         return yml['ippnfsim']
168
169     def _get_docker_containers(self):
170         '''Returns a list containing 'name' attribute of running docker containers'''
171         dc = from_env()
172         containers = []
173         for container in dc.containers.list():
174             containers.append(container.attrs['Name'][1:])
175         return containers
176
177     def _get_iter_range(self):
178         '''Helper routine to get the iteration range
179         for the lifecycle commands'''
180         if not self.args.count:
181             return [self.existing_sim_instances]
182         else:
183             return [self.args.count]
184
185     def bootstrap(self):
186         self.logger.info("Bootstrapping PNF instances")
187
188         start_port = 2000
189         ftps_pasv_port_start = 8000
190         ftps_pasv_port_num_of_ports = 10
191
192         ftps_pasv_port_end = ftps_pasv_port_start + ftps_pasv_port_num_of_ports
193
194         for i in range(self.args.count):
195             self.logger.info(f"PNF simulator instance: {i}")
196
197             # The IP ranges are in distance of 16 compared to each other.
198             # This is matching the /28 subnet mask used in the dockerfile inside.
199             instance_ip_offset = i * 16
200             ip_properties = [
201                       'subnet',
202                       'gw',
203                       'PnfSim',
204                       'ftps',
205                       'sftp'
206                     ]
207
208             ip_offset = 0
209             ip = {}
210             for prop in ip_properties:
211                 ip.update({prop: str(self.args.ipstart + ip_offset + instance_ip_offset)})
212                 ip_offset += 1
213
214             self.logger.debug(f'Instance #{i} properties:\n {dumps(ip, indent=4)}')
215
216             PortSftp = start_port + 1
217             PortFtps = start_port + 2
218             start_port += 2
219
220             self.logger.info(f'\tCreating {self.sim_dirname_pattern}{i}')
221             copytree('pnf-sim-lightweight', f'{self.sim_dirname_pattern}{i}')
222
223             composercmd = " ".join([
224                     "./simulator.sh compose",
225                     ip['gw'],
226                     ip['subnet'],
227                     str(i),
228                     self.args.urlves,
229                     ip['PnfSim'],
230                     str(self.args.ipfileserver),
231                     self.args.typefileserver,
232                     str(PortSftp),
233                     str(PortFtps),
234                     ip['ftps'],
235                     ip['sftp'],
236                     str(ftps_pasv_port_start),
237                     str(ftps_pasv_port_end)
238                 ])
239             self.logger.debug(f"Script cmdline: {composercmd}")
240             self.logger.info(f"\tCreating instance #{i} configuration ")
241             self._run_cmd(composercmd, f"{self.sim_dirname_pattern}{i}")
242
243             ftps_pasv_port_start += ftps_pasv_port_num_of_ports + 1
244             ftps_pasv_port_end += ftps_pasv_port_num_of_ports + 1
245
246             self.logger.info(f'Done setting up instance #{i}')
247
248     def build(self):
249         self.logger.info("Building simulator image")
250         if path.isfile('pnf-sim-lightweight/pom.xml'):
251             self._run_cmd(self.mvn_build_cmd, 'pnf-sim-lightweight')
252         else:
253             self.logger.error('POM file was not found, Maven cannot run')
254             exit(1)
255
256     def clean(self):
257         self.logger.info('Cleaning simulators workdirs')
258         for sim_id in range(self.existing_sim_instances):
259             rmtree(f"{self.sim_dirname_pattern}{sim_id}")
260
261     @_MassPnfSim_Decorators.do_action('Starting', './simulator.sh start')
262     def start(self):
263         pass
264
265     def status(self):
266         for i in range(*self._get_iter_range()):
267             self.logger.info(f'Getting {self.sim_dirname_pattern}{i} instance status:')
268             if f"{self.sim_container_name}-{i}" in self._get_docker_containers():
269                 try:
270                     sim_ip = self._get_sim_instance_data(i)
271                     self.logger.info(f' PNF-Sim IP: {sim_ip}')
272                     self._run_cmd(self.docker_compose_status_cmd, f"{self.sim_dirname_pattern}{i}")
273                     sim_response = get('{}/status'.format(self.sim_base_url).format(sim_ip))
274                     if sim_response.status_code == codes.ok:
275                         self.logger.info(sim_response.text)
276                     else:
277                         self.logger.error(f'Simulator request returned http code {sim_response.status_code}')
278                 except KeyError:
279                     self.logger.error(f'Unable to get sim instance IP from {self.sim_config}')
280             else:
281                 self.logger.info(' Simulator containers are down')
282
283     @_MassPnfSim_Decorators.do_action('Stopping', './simulator.sh stop')
284     def stop(self):
285         pass
286
287     @_MassPnfSim_Decorators.do_action('Triggering', './simulator.sh trigger-simulator')
288     def trigger(self):
289         self.logger.info("Triggering VES sending:")
290
291     @_MassPnfSim_Decorators.do_action('Triggering', './simulator.sh trigger-simulator')
292     def trigger_custom(self):
293         self.logger.info("Triggering VES sending by a range of simulators:")