Support setting custom username/password for the file server service
[integration.git] / test / mocks / mass-pnf-sim / MassPnfSim.py
index b69e0fc..4907755 100755 (executable)
@@ -4,12 +4,14 @@ from subprocess import run, CalledProcessError
 import argparse
 import ipaddress
 from sys import exit
-from os import chdir, getcwd, path
-from shutil import copytree, rmtree
-from json import dumps
-from yaml import load, SafeLoader
+from os import chdir, getcwd, path, popen, kill, getuid, stat, mkdir, getlogin, chmod
+from shutil import copytree, rmtree, move
+from json import loads, dumps
+from yaml import load, SafeLoader, dump
 from glob import glob
-from requests import get
+from time import strftime, tzname, daylight
+from docker import from_env
+from requests import get, codes, post
 from requests.exceptions import MissingSchema, InvalidSchema, InvalidURL, ConnectionError, ConnectTimeout
 
 def validate_url(url):
@@ -48,6 +50,8 @@ def get_parser():
                                   type=validate_ip, metavar='IP', required=True)
     parser_bootstrap.add_argument('--typefileserver', help='Type of the file server (SFTP/FTPS) to be included in the VES event',
                                   type=str, choices=['sftp', 'ftps'], required=True)
+    parser_bootstrap.add_argument('--user', help='File server username', type=str, metavar='USERNAME', required=True)
+    parser_bootstrap.add_argument('--password', help='File server password', type=str, metavar='PASSWORD', required=True)
     parser_bootstrap.add_argument('--ipstart', help='IP address range beginning', type=validate_ip, metavar='IP', required=True)
     # Start command parser
     parser_start = subparsers.add_parser('start', help='Start instances')
@@ -58,6 +62,9 @@ def get_parser():
     # Trigger command parser
     parser_trigger = subparsers.add_parser('trigger', help='Trigger one single VES event from each simulator')
     parser_trigger.add_argument('--count', help='Instance count to trigger', type=int, metavar='INT', default=0)
+    # Stop-simulator command parser
+    parser_stopsimulator = subparsers.add_parser('stop_simulator', help='Stop sending PNF registration messages')
+    parser_stopsimulator.add_argument('--count', help='Instance count to stop', type=int, metavar='INT', default=0)
     # Trigger-custom command parser
     parser_triggerstart = subparsers.add_parser('trigger_custom', help='Trigger one single VES event from specific simulators')
     parser_triggerstart.add_argument('--triggerstart', help='First simulator id to trigger', type=int,
@@ -76,36 +83,21 @@ def get_parser():
 
 class MassPnfSim:
 
-    # MassPnfSim class actions decorator
-    class _MassPnfSim_Decorators:
-
-        @staticmethod
-        def do_action(action_string, cmd):
-            def action_decorator(method):
-                def action_wrap(self):
-                    cmd_local = cmd
-                    # Append instance # if action is 'stop'
-                    if method.__name__ == 'stop':
-                        cmd_local += " {}"
-                    # Alter looping range if action is 'tigger_custom'
-                    if method.__name__ == 'trigger_custom':
-                        iter_range = [self.args.triggerstart, self.args.triggerend+1]
-                    else:
-                        if not self.args.count:
-                            # If no instance count set explicitly via --count
-                            # option
-                            iter_range = [self.existing_sim_instances]
-                        else:
-                            iter_range = [self.args.count]
-                    method(self)
-                    for i in range(*iter_range):
-                        self.logger.info(f'{action_string} {self.sim_dirname_pattern}{i} instance:')
-                        self._run_cmd(cmd_local.format(i), f"{self.sim_dirname_pattern}{i}")
-                return action_wrap
-            return action_decorator
-
     log_lvl = logging.INFO
+    sim_compose_template = 'docker-compose-template.yml'
+    sim_vsftpd_template = 'config/vsftpd_ssl-TEMPLATE.conf'
+    sim_vsftpd_config = 'config/vsftpd_ssl.conf'
+    sim_sftp_script = 'fix-sftp-perms.sh'
+    sim_sftp_script_template = 'fix-sftp-perms-template.sh'
     sim_config = 'config/config.yml'
+    sim_msg_config = 'config/config.json'
+    sim_port = 5000
+    sim_base_url = 'http://{}:' + str(sim_port) + '/simulator'
+    sim_start_url = sim_base_url + '/start'
+    sim_status_url = sim_base_url + '/status'
+    sim_stop_url = sim_base_url + '/stop'
+    sim_container_name = 'pnf-simulator'
+    rop_script_name = 'ROP_file_creator.sh'
 
     def __init__(self, args):
         self.args = args
@@ -113,6 +105,7 @@ class MassPnfSim:
         self.logger.setLevel(self.log_lvl)
         self.sim_dirname_pattern = "pnf-sim-lw-"
         self.mvn_build_cmd = 'mvn clean package docker:build -Dcheckstyle.skip'
+        self.docker_compose_status_cmd = 'docker-compose ps'
         self.existing_sim_instances = self._enum_sim_instances()
 
         # Validate 'trigger_custom' subcommand options
@@ -122,7 +115,7 @@ class MassPnfSim:
                 exit(1)
 
         # Validate --count option for subcommands that support it
-        if self.args.subcommand in ['start', 'stop', 'trigger', 'status']:
+        if self.args.subcommand in ['start', 'stop', 'trigger', 'status', 'stop_simulator']:
             if self.args.count > self.existing_sim_instances:
                 self.logger.error('--count value greater that existing instance count')
                 exit(1)
@@ -136,11 +129,11 @@ class MassPnfSim:
             exit(1)
 
     def _run_cmd(self, cmd, dir_context='.'):
-        if self.args.verbose == 'debug':
-            cmd='bash -x ' + cmd
         old_pwd = getcwd()
         try:
             chdir(dir_context)
+            self.logger.debug(f'_run_cmd: Current direcotry: {getcwd()}')
+            self.logger.debug(f'_run_cmd: Command string: {cmd}')
             run(cmd, check=True, shell=True)
             chdir(old_pwd)
         except FileNotFoundError:
@@ -161,6 +154,77 @@ class MassPnfSim:
         chdir(oldpwd)
         return yml['ippnfsim']
 
+    def _get_docker_containers(self):
+        '''Returns a list containing 'name' attribute of running docker containers'''
+        dc = from_env()
+        containers = []
+        for container in dc.containers.list():
+            containers.append(container.attrs['Name'][1:])
+        return containers
+
+    def _get_iter_range(self):
+        '''Helper routine to get the iteration range
+        for the lifecycle commands'''
+        if hasattr(self.args, 'count'):
+            if not self.args.count:
+                return [self.existing_sim_instances]
+            else:
+                return [self.args.count]
+        elif hasattr(self.args, 'triggerstart'):
+            return [self.args.triggerstart, self.args.triggerend + 1]
+        else:
+            return [self.existing_sim_instances]
+
+    def _archive_logs(self, sim_dir):
+        '''Helper function to archive simulator logs or create the log dir'''
+        old_pwd = getcwd()
+        try:
+            chdir(sim_dir)
+            if path.isdir('logs'):
+                arch_dir = f"logs/archive_{strftime('%Y-%m-%d_%T')}"
+                mkdir(arch_dir)
+                self.logger.debug(f'Created {arch_dir}')
+                # Collect file list to move
+                self.logger.debug('Archiving log files')
+                for fpattern in ['*.log', '*.xml']:
+                    for f in glob('logs/' + fpattern):
+                        # Move files from list to arch dir
+                        move(f, arch_dir)
+                        self.logger.debug(f'Moving {f} to {arch_dir}')
+            else:
+                mkdir('logs')
+                self.logger.debug("Logs dir didn't exist, created")
+            chdir(old_pwd)
+        except FileNotFoundError:
+            self.logger.error(f"Directory {sim_dir} not found")
+
+    def _generate_pnf_sim_config(self, i, port_sftp, port_ftps, pnf_sim_ip):
+        '''Writes a yaml formatted configuration file for Java simulator app'''
+        yml = {}
+        yml['urlves'] = self.args.urlves
+        yml['urlsftp'] = f'sftp://{self.args.user}:{self.args.password}@{self.args.ipfileserver}:{port_sftp}'
+        yml['urlftps'] = f'ftps://{self.args.user}:{self.args.password}@{self.args.ipfileserver}:{port_ftps}'
+        yml['ippnfsim'] = pnf_sim_ip
+        yml['typefileserver'] = self.args.typefileserver
+        self.logger.debug(f'Generated simulator config:\n{dump(yml)}')
+        with open(f'{self.sim_dirname_pattern}{i}/{self.sim_config}', 'w') as fout:
+            fout.write(dump(yml))
+
+    def _generate_config_file(self, source, dest, **kwargs):
+        '''Helper private method to generate a file based on a template'''
+        old_pwd = getcwd()
+        chdir(self.sim_dirname_pattern + str(kwargs['I']))
+        # Read the template file
+        with open(source, 'r') as f:
+            template = f.read()
+        # Replace all occurences of env like variable with it's
+        # relevant value from a corresponding key form kwargs
+        for (k,v) in kwargs.items():
+            template = template.replace('${' + k + '}', str(v))
+        with open(dest, 'w') as f:
+            f.write(template)
+        chdir(old_pwd)
+
     def bootstrap(self):
         self.logger.info("Bootstrapping PNF instances")
 
@@ -198,30 +262,41 @@ class MassPnfSim:
 
             self.logger.info(f'\tCreating {self.sim_dirname_pattern}{i}')
             copytree('pnf-sim-lightweight', f'{self.sim_dirname_pattern}{i}')
-
-            composercmd = " ".join([
-                    "./simulator.sh compose",
-                    ip['gw'],
-                    ip['subnet'],
-                    str(i),
-                    self.args.urlves,
-                    ip['PnfSim'],
-                    str(self.args.ipfileserver),
-                    self.args.typefileserver,
-                    str(PortSftp),
-                    str(PortFtps),
-                    ip['ftps'],
-                    ip['sftp'],
-                    str(ftps_pasv_port_start),
-                    str(ftps_pasv_port_end)
-                ])
-            self.logger.debug(f"Script cmdline: {composercmd}")
             self.logger.info(f"\tCreating instance #{i} configuration ")
-            self._run_cmd(composercmd, f"{self.sim_dirname_pattern}{i}")
+            self._generate_pnf_sim_config(i, PortSftp, PortFtps, ip['PnfSim'])
+            # generate docker-compose for the simulator instance
+            self._generate_config_file(self.sim_compose_template, 'docker-compose.yml',
+                                       IPGW = ip['gw'], IPSUBNET = ip['subnet'],
+                                       I = i, IPPNFSIM = ip['PnfSim'],
+                                       PORTSFTP = str(PortSftp),
+                                       PORTFTPS = str(PortFtps),
+                                       IPFTPS = ip['ftps'], IPSFTP = ip['sftp'],
+                                       FTPS_PASV_MIN = str(ftps_pasv_port_start),
+                                       FTPS_PASV_MAX = str(ftps_pasv_port_end),
+                                       TIMEZONE = tzname[daylight],
+                                       FILESERV_USER = self.args.user,
+                                       FILESERV_PASS = self.args.password)
+            # generate vsftpd config file for the simulator instance
+            self._generate_config_file(self.sim_vsftpd_template, self.sim_vsftpd_config,
+                                       I = i, USER = getlogin(),
+                                       FTPS_PASV_MIN = str(ftps_pasv_port_start),
+                                       FTPS_PASV_MAX = str(ftps_pasv_port_end),
+                                       IPFILESERVER = str(self.args.ipfileserver))
+            # generate sftp permission fix script
+            self._generate_config_file(self.sim_sftp_script_template, self.sim_sftp_script,
+                                       I = i, FILESERV_USER = self.args.user)
+            chmod(f'{self.sim_dirname_pattern}{i}/{self.sim_sftp_script}', 0o755)
+            # Run the 3GPP measurements file generator
+            self._run_cmd(f'./ROP_file_creator.sh {i} &', f"{self.sim_dirname_pattern}{i}")
 
             ftps_pasv_port_start += ftps_pasv_port_num_of_ports + 1
             ftps_pasv_port_end += ftps_pasv_port_num_of_ports + 1
 
+            # ugly hack to chown vsftpd config file to root
+            if getuid():
+                self._run_cmd(f'sudo chown root {self.sim_vsftpd_config}', f'{self.sim_dirname_pattern}{i}')
+                self.logger.debug(f"vsftpd config file owner UID: {stat(self.sim_dirname_pattern + str(i) + '/' + self.sim_vsftpd_config).st_uid}")
+
             self.logger.info(f'Done setting up instance #{i}')
 
     def build(self):
@@ -237,22 +312,106 @@ class MassPnfSim:
         for sim_id in range(self.existing_sim_instances):
             rmtree(f"{self.sim_dirname_pattern}{sim_id}")
 
-    @_MassPnfSim_Decorators.do_action('Starting', './simulator.sh start')
     def start(self):
-        pass
+        for i in range(*self._get_iter_range()):
+            # If container is not running
+            if f"{self.sim_container_name}-{i}" not in self._get_docker_containers():
+                self.logger.info(f'Starting {self.sim_dirname_pattern}{i} instance:')
+                self.logger.info(f' PNF-Sim IP: {self._get_sim_instance_data(i)}')
+                #Move logs to archive
+                self._archive_logs(self.sim_dirname_pattern + str(i))
+                self.logger.info(' Starting simulator containers using netconf model specified in config/netconf.env')
+                self._run_cmd('docker-compose up -d', self.sim_dirname_pattern + str(i))
+            else:
+                self.logger.warning(f'Instance {self.sim_dirname_pattern}{i} containers are already up')
 
-    @_MassPnfSim_Decorators.do_action('Getting', './simulator.sh status')
     def status(self):
-        pass
+        for i in range(*self._get_iter_range()):
+            self.logger.info(f'Getting {self.sim_dirname_pattern}{i} instance status:')
+            if f"{self.sim_container_name}-{i}" in self._get_docker_containers():
+                try:
+                    sim_ip = self._get_sim_instance_data(i)
+                    self.logger.info(f' PNF-Sim IP: {sim_ip}')
+                    self._run_cmd(self.docker_compose_status_cmd, f"{self.sim_dirname_pattern}{i}")
+                    sim_response = get('{}'.format(self.sim_status_url).format(sim_ip))
+                    if sim_response.status_code == codes.ok:
+                        self.logger.info(sim_response.text)
+                    else:
+                        self.logger.error(f'Simulator request returned http code {sim_response.status_code}')
+                except KeyError:
+                    self.logger.error(f'Unable to get sim instance IP from {self.sim_config}')
+            else:
+                self.logger.info(' Simulator containers are down')
 
-    @_MassPnfSim_Decorators.do_action('Stopping', './simulator.sh stop')
     def stop(self):
-        pass
+        for i in range(*self._get_iter_range()):
+            self.logger.info(f'Stopping {self.sim_dirname_pattern}{i} instance:')
+            self.logger.info(f' PNF-Sim IP: {self._get_sim_instance_data(i)}')
+            # attempt killing ROP script
+            rop_pid = []
+            for ps_line in iter(popen(f'ps --no-headers -C {self.rop_script_name} -o pid,cmd').readline, ''):
+                # try getting ROP script pid
+                try:
+                    ps_line_arr = ps_line.split()
+                    assert self.rop_script_name in ps_line_arr[2]
+                    assert ps_line_arr[3] == str(i)
+                    rop_pid = ps_line_arr[0]
+                except AssertionError:
+                    pass
+                else:
+                    # get rop script childs, kill ROP script and all childs
+                    childs = popen(f'pgrep -P {rop_pid}').read().split()
+                    for pid in [rop_pid] + childs:
+                        kill(int(pid), 15)
+                    self.logger.info(f' ROP_file_creator.sh {i} successfully killed')
+            if not rop_pid:
+                # no process found
+                self.logger.warning(f' ROP_file_creator.sh {i} already not running')
+            # try tearing down docker-compose application
+            if f"{self.sim_container_name}-{i}" in self._get_docker_containers():
+                self._run_cmd('docker-compose down', self.sim_dirname_pattern + str(i))
+                self._run_cmd('docker-compose rm', self.sim_dirname_pattern + str(i))
+            else:
+                self.logger.warning(" Simulator containers are already down")
 
-    @_MassPnfSim_Decorators.do_action('Triggering', './simulator.sh trigger-simulator')
     def trigger(self):
         self.logger.info("Triggering VES sending:")
-
-    @_MassPnfSim_Decorators.do_action('Triggering', './simulator.sh trigger-simulator')
-    def trigger_custom(self):
-        self.logger.info("Triggering VES sending by a range of simulators:")
+        for i in range(*self._get_iter_range()):
+            sim_ip = self._get_sim_instance_data(i)
+            self.logger.info(f'Triggering {self.sim_dirname_pattern}{i} instance:')
+            self.logger.info(f' PNF-Sim IP: {sim_ip}')
+            # setup req headers
+            req_headers = {
+                    "Content-Type": "application/json",
+                    "X-ONAP-RequestID": "123",
+                    "X-InvocationID": "456"
+                }
+            self.logger.debug(f' Request headers: {req_headers}')
+            try:
+                # get payload for the request
+                with open(f'{self.sim_dirname_pattern}{i}/{self.sim_msg_config}') as data:
+                    json_data = loads(data.read())
+                    self.logger.debug(f' JSON payload for the simulator:\n{json_data}')
+                    # make a http request to the simulator
+                    sim_response = post('{}'.format(self.sim_start_url).format(sim_ip), headers=req_headers, json=json_data)
+                    if sim_response.status_code == codes.ok:
+                        self.logger.info(' Simulator response: ' + sim_response.text)
+                    else:
+                        self.logger.warning(' Simulator response ' + sim_response.text)
+            except TypeError:
+                self.logger.error(f' Could not load JSON data from {self.sim_dirname_pattern}{i}/{self.sim_msg_config}')
+
+    # Make the 'trigger_custom' an alias to the 'trigger' method
+    trigger_custom = trigger
+
+    def stop_simulator(self):
+        self.logger.info("Stopping sending PNF registration messages:")
+        for i in range(*self._get_iter_range()):
+            sim_ip = self._get_sim_instance_data(i)
+            self.logger.info(f'Stopping {self.sim_dirname_pattern}{i} instance:')
+            self.logger.info(f' PNF-Sim IP: {sim_ip}')
+            sim_response = post('{}'.format(self.sim_stop_url).format(sim_ip))
+            if sim_response.status_code == codes.ok:
+                self.logger.info(' Simulator response: ' + sim_response.text)
+            else:
+                self.logger.warning(' Simulator response ' + sim_response.text)