def test_connect_inventory(self): """Test construct ansible inventory dictionary.""" serializer = SourceSerializer(self.source) source = serializer.data hosts = source['hosts'] connection_port = source['port'] hc_serializer = CredentialSerializer(self.cred) cred = hc_serializer.data inventory_dict = construct_connect_inventory(hosts, cred, connection_port) expected = { 'all': { 'hosts': { '1.2.3.4': None }, 'vars': { 'ansible_become_pass': '******', 'ansible_port': 22, 'ansible_ssh_pass': '******', 'ansible_ssh_private_key_file': 'keyfile', 'ansible_user': '******', 'ansible_become_method': 'sudo', 'ansible_become_user': '******' } } } self.assertEqual(inventory_dict, expected)
def test_scan_inventory(self): """Test construct ansible inventory dictionary.""" serializer = SourceSerializer(self.source) source = serializer.data connection_port = source['port'] hc_serializer = CredentialSerializer(self.cred) cred = hc_serializer.data inventory_dict = construct_scan_inventory([('1.2.3.4', cred)], connection_port, 50) expected = { 'all': { 'children': { 'group_0': { 'hosts': { '1.2.3.4': { 'ansible_user': '******', 'ansible_ssh_pass': '******', 'ansible_host': '1.2.3.4' } } } }, 'vars': { 'ansible_port': 22 } } } self.assertEqual(inventory_dict[1], expected)
def list(self, request): # pylint: disable=unused-argument """List the host credentials.""" queryset = self.filter_queryset(self.get_queryset()) serializer = CredentialSerializer(queryset, many=True) for cred in serializer.data: cred = mask_credential(cred) return Response(serializer.data)
def run_with_result_store(self, result_store): """Run with a given ConnectResultStore.""" serializer = SourceSerializer(self.scan_task.source) source = serializer.data if self.scan_job.options is not None: forks = self.scan_job.options.max_concurrency else: forks = ScanOptions.get_default_forks() if self.scan_task.source.options is not None: use_paramiko = self.scan_task.source.options.use_paramiko else: use_paramiko = False connection_port = source['port'] credentials = source['credentials'] remaining_hosts = result_store.remaining_hosts() for cred_id in credentials: credential = Credential.objects.get(pk=cred_id) if not remaining_hosts: message = 'Skipping credential %s. No remaining hosts.' % \ credential.name self.scan_task.log_message(message) break message = 'Attempting credential %s.' % credential.name self.scan_task.log_message(message) cred_data = CredentialSerializer(credential).data callback = ConnectResultCallback(result_store, credential, self.scan_task.source) try: connect(remaining_hosts, callback, cred_data, connection_port, use_paramiko, forks=forks) except AnsibleError as ansible_error: remaining_hosts_str = ', '.join(result_store.remaining_hosts()) error_message = 'Connect scan task failed with credential %s.'\ ' Error: %s Hosts: %s' %\ (credential.name, ansible_error, remaining_hosts_str) return error_message, ScanTask.FAILED remaining_hosts = result_store.remaining_hosts() logger.debug('Failed systems: %s', remaining_hosts) for host in remaining_hosts: # We haven't connected to these hosts with any # credentials, so they have failed. result_store.record_result(host, self.scan_task.source, None, SystemConnectionResult.FAILED) return None, ScanTask.COMPLETED
def setUp(self): """Create test case setup.""" self.cred = Credential( name='cred1', username='******', password='******', ssh_keyfile=None, become_method=None, become_user=None, become_password=None) self.cred.save() hc_serializer = CredentialSerializer(self.cred) self.cred_data = hc_serializer.data self.source = Source( name='source1', port=22, hosts='["1.2.3.4"]') self.source.save() self.source.credentials.add(self.cred) self.host_list = [('1.2.3.4', self.cred_data)] # setup scan options self.scan_job, self.scan_task = create_scan_job( self.source, ScanTask.SCAN_TYPE_INSPECT) self.connect_scan_task = self.scan_task.prerequisites.first() self.connect_scan_task.update_stats( 'TEST NETWORK CONNECT.', sys_failed=0) success_sys = SystemConnectionResult( name='1.2.3.4', credential=self.cred, status=SystemConnectionResult.SUCCESS) success_sys.save() failed_sys = SystemConnectionResult( name='1.1.1.2', status=SystemConnectionResult.FAILED) failed_sys.save() conn_result = self.connect_scan_task.connection_result conn_result.systems.add(success_sys) conn_result.systems.add(failed_sys) conn_result.save() self.connect_scan_task.update_stats( 'TEST_VC.', sys_count=2, sys_failed=1, sys_scanned=1) self.connect_scan_task.complete() self.scan_task.update_stats( 'TEST NETWORK INSPECT.', sys_failed=0) self.fact_endpoint = 'http://testserver' + reverse('facts-list') self.scan_job.save() scan_data_log.disable_log_for_test()
def setUp(self): """Create test case setup.""" self.cred = Credential(name='cred1', username='******', password='******', ssh_keyfile=None, become_method=None, become_user=None, become_password=None) self.cred.save() hc_serializer = CredentialSerializer(self.cred) self.cred_data = hc_serializer.data # setup source for scan self.source = Source(name='source1', port=22, hosts='["1.2.3.4"]') self.source.save() self.source.credentials.add(self.cred) self.host_list = [('1.2.3.4', self.cred_data)] self.scan_job, self.scan_task = create_scan_job( self.source, ScanTask.SCAN_TYPE_INSPECT) self.connect_scan_task = self.scan_task.prerequisites.first() self.connect_scan_task.update_stats('TEST NETWORK CONNECT.', sys_failed=0) conn_result = self.connect_scan_task.connection_result success_sys = SystemConnectionResult( name='1.2.3.4', credential=self.cred, status=SystemConnectionResult.SUCCESS, task_connection_result=conn_result) success_sys.save() failed_sys = SystemConnectionResult( name='1.1.1.2', status=SystemConnectionResult.FAILED, task_connection_result=conn_result) failed_sys.save() conn_result.save() self.connect_scan_task.update_stats('TEST_VC.', sys_count=2, sys_failed=1, sys_scanned=1) self.connect_scan_task.complete() self.scan_task.update_stats('TEST NETWORK INSPECT.', sys_failed=0) self.fact_endpoint = 'http://testserver' + reverse('reports-list') self.scan_job.save() self.stop_states = [ ScanJob.JOB_TERMINATE_CANCEL, ScanJob.JOB_TERMINATE_PAUSE ] self.interrupt = Mock(value=ScanJob.JOB_RUN)
def test_connect(self, mock_run): """Test connect flow with mocked manager.""" serializer = SourceSerializer(self.source) source = serializer.data hosts = source['hosts'] connection_port = source['port'] hc_serializer = CredentialSerializer(self.cred) cred = hc_serializer.data connect(hosts, Mock(), cred, connection_port) mock_run.assert_called_with(ANY)
def retrieve(self, request, pk=None): # pylint: disable=unused-argument """Get a host credential.""" if not pk or (pk and not is_int(pk)): error = {'id': [_(messages.COMMON_ID_INV)]} raise ValidationError(error) host_cred = get_object_or_404(self.queryset, pk=pk) serializer = CredentialSerializer(host_cred) cred = format_credential(serializer.data) return Response(cred)
def test_connect_failure(self, mock_run, mock_ssh_pass): """Test connect flow with mocked manager and failure.""" serializer = SourceSerializer(self.source) source = serializer.data hosts = source['hosts'] connection_port = source['port'] hc_serializer = CredentialSerializer(self.cred) cred = hc_serializer.data with self.assertRaises(AnsibleError): connect(hosts, Mock(), cred, connection_port) mock_run.assert_called() mock_ssh_pass.assert_called()
def test_construct_vars(self): """Test constructing ansible vars dictionary.""" hc_serializer = CredentialSerializer(self.cred) cred = hc_serializer.data vars_dict = _construct_vars(22, cred) expected = { 'ansible_become_pass': '******', 'ansible_port': 22, 'ansible_ssh_pass': '******', 'ansible_ssh_private_key_file': 'keyfile', 'ansible_user': '******' } self.assertEqual(vars_dict, expected)
def test_store_discovery_success(self): """Test running a discovery scan _store_connect_result.""" scanner = ConnectTaskRunner(self.scan_job, self.scan_task, self.conn_results) hc_serializer = CredentialSerializer(self.cred) cred = hc_serializer.data connected = [('1.2.3.4', cred)] failed = ['1.2.3.5'] expected = {'1.2.3.4': {'name': 'cred1'}, '1.2.3.5': None} # pylint: disable=protected-access result = scanner._store_connect_result(connected, failed) self.assertEqual(len(result), len(expected)) self.assertIn('1.2.3.5', result) self.assertIsNone(result['1.2.3.5'])
def list(self, request): """List the host credentials.""" queryset = self.filter_queryset(self.get_queryset()) page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True) for cred in serializer.data: cred = format_credential(cred) return self.get_paginated_response(serializer.data) serializer = CredentialSerializer(queryset, many=True) for cred in serializer.data: cred = format_credential(cred) return Response(serializer.data)
def run_with_result_store(self, result_store): """Run with a given ConnectResultStore.""" serializer = SourceSerializer(self.scan_task.source) source = serializer.data forks = self.scan_job.options.max_concurrency connection_port = source['port'] credentials = source['credentials'] remaining_hosts = result_store.remaining_hosts() for cred_id in credentials: credential = Credential.objects.get(pk=cred_id) if not remaining_hosts: message = 'Skipping credential %s. No remaining hosts.' % \ credential.name self.scan_task.log_message(message) break message = 'Attempting credential %s.' % credential.name self.scan_task.log_message(message) cred_data = CredentialSerializer(credential).data callback = ConnectResultCallback(result_store, credential) try: connect(remaining_hosts, callback, cred_data, connection_port, forks=forks) except AnsibleError as ansible_error: error_message = 'Connect scan task failed for %s. %s' %\ (self.scan_task, ansible_error) return error_message, ScanTask.FAILED remaining_hosts = result_store.remaining_hosts() logger.debug('Failed systems: %s', remaining_hosts) for host in remaining_hosts: # We haven't connected to these hosts with any # credentials, so they have failed. result_store.record_result(host, None, SystemConnectionResult.FAILED) # Clear cache as results changed self.result = None return None, ScanTask.COMPLETED
def test_process_connect_callback(self): """Test callback processing logic.""" hc_serializer = CredentialSerializer(self.cred) cred = hc_serializer.data callback = ResultCallback() success_result = {'host': '1.2.3.4', 'result': {'rc': 0}} failed_result = {'host': '1.2.3.5', 'result': {'rc': 1}} failed_result_format = {'host': '1.2.3.6'} callback.results.append(success_result) callback.results.append(failed_result) callback.results.append(failed_result_format) success, failed = _process_connect_callback(callback, cred) del cred['password'] self.assertEqual(success, [('1.2.3.4', cred)]) self.assertEqual(failed, ['1.2.3.5', '1.2.3.6'])
def test_connect_inventory(self): """Test construct ansible inventory dictionary.""" serializer = SourceSerializer(self.source) source = serializer.data hosts = source['hosts'] exclude_hosts = source['exclude_hosts'] connection_port = source['port'] hc_serializer = CredentialSerializer(self.cred) cred = hc_serializer.data path = '/path/to/executable.py' ssh_timeout = '0.1s' ssh_args = ['--executable=' + path, '--timeout=' + ssh_timeout, 'ssh'] _, inventory_dict = _construct_connect_inventory(hosts, cred, connection_port, 1, exclude_hosts, ssh_executable=path, ssh_args=ssh_args) # pylint: disable=line-too-long expected = { 'all': { 'children': { 'group_0': { 'hosts': { '1.2.3.4': { 'ansible_host': '1.2.3.4', 'ansible_ssh_executable': '/path/to/executable.py', 'ansible_ssh_common_args': '--executable=/path/to/executable.py --timeout=0.1s ssh' } } } }, # noqa 'vars': { 'ansible_port': 22, 'ansible_user': '******', 'ansible_ssh_pass': '******', 'ansible_ssh_private_key_file': 'keyfile', 'ansible_become_pass': '******', 'ansible_become_method': 'sudo', 'ansible_become_user': '******' } } } self.assertEqual(inventory_dict, expected)
def discovery(self): """Execute the discovery scan with the initialized source. :returns: list of connected hosts credential tuples and list of host that failed connection """ connected = [] serializer = SourceSerializer(self.scan_task.source) source = serializer.data remaining = source['hosts'] credentials = source['credentials'] connection_port = source['port'] forks = self.scan_job.options.max_concurrency for cred_id in credentials: cred_obj = Credential.objects.get(pk=cred_id) hc_serializer = CredentialSerializer(cred_obj) cred = hc_serializer.data connected, remaining = connect(remaining, cred, connection_port, forks=forks) # Update the scan counts if self.scan_task.systems_count is None: self.scan_task.systems_count = len(connected) + len(remaining) self.scan_task.systems_scanned = 0 self.scan_task.systems_failed = 0 self.scan_task.systems_scanned += len(connected) self.scan_task.save() if remaining == []: break logger.info('Connect scan completed for %s.', self.scan_task) logger.info('Successfully connected to %d systems.', len(connected)) if bool(remaining): logger.warning('Failed to connect to %d systems.', len(remaining)) logger.debug('Failed systems: %s', remaining) return connected, remaining
def _connect(manager_interrupt, scan_task, hosts, result_store, credential, connection_port, use_paramiko=False, forks=50, exclude_hosts=None, base_ssh_executable=None, ssh_timeout=None): """Attempt to connect to hosts using the given credential. :param manager_interrupt: Signal used to communicate termination of scan :param scan_task: The scan task for this connection job :param hosts: The collection of hosts to test connections :param result_store: The result store to accept the results. :param credential: The credential used for connections :param connection_port: The connection port :param use_paramiko: use paramiko instead of ssh for connection :param forks: number of forks to run with, default of 50 :param exclude_hosts: Optional. Hosts to exclude from test connections :param base_ssh_executable: ssh executable, or None for 'ssh'. Will be wrapped with a timeout before being passed to Ansible. :param ssh_timeout: string in the format of the 'timeout' command. Timeout for individual tasks. :returns: list of connected hosts credential tuples and list of host that failed connection """ cred_data = CredentialSerializer(credential).data ssh_executable = os.path.abspath( os.path.join(os.path.dirname(__file__), '../../../bin/timeout_ssh')) base_ssh_executable = base_ssh_executable or 'ssh' ssh_timeout = ssh_timeout or settings.QPC_SSH_CONNECT_TIMEOUT # pylint: disable=line-too-long # the ssh arg is required for become-pass because # ansible checks for an exact string match of ssh # anywhere in the command array # See https://github.com/ansible/ansible/blob/stable-2.3/lib/ansible/plugins/connection/ssh.py#L490-L500 # noqa # timeout_ssh will remove the ssh argument before running the command ssh_args = [ '--executable=' + base_ssh_executable, '--timeout=' + ssh_timeout, 'ssh' ] group_names, inventory = _construct_connect_inventory( hosts, cred_data, connection_port, forks, exclude_hosts, ssh_executable, ssh_args) inventory_file = write_inventory(inventory) extra_vars = {} _handle_ssh_passphrase(cred_data) error_msg = '' log_message = 'START CONNECT PROCESSING GROUPS'\ ' with use_paramiko: %s,' \ '%d forks and extra_vars=%s' % (use_paramiko, forks, extra_vars) scan_task.log_message(log_message) for idx, group_name in enumerate(group_names): if manager_interrupt.value == ScanJob.JOB_TERMINATE_CANCEL: raise NetworkCancelException() if manager_interrupt.value == ScanJob.JOB_TERMINATE_PAUSE: raise NetworkPauseException() group_ips = inventory.get('all').get('children').get(group_name).get( 'hosts').keys() group_ips = ["'%s'" % ip for ip in group_ips] group_ip_string = ', '.join(group_ips) log_message = 'START CONNECT PROCESSING GROUP %d of %d. '\ 'About to connect to hosts [%s]' % ( (idx + 1), len(group_names), group_ip_string) scan_task.log_message(log_message) callback = ConnectResultCallback(result_store, credential, scan_task.source) playbook = { 'name': 'attempt connection to systems', 'hosts': group_name, 'gather_facts': False, 'tasks': [{ 'action': { 'module': 'raw', 'args': parse_kv('echo "Hello"') } }] } result = run_playbook(inventory_file, callback, playbook, extra_vars, use_paramiko, forks=forks) if result != TaskQueueManager.RUN_OK: new_error_msg = _construct_error_msg(result) if result not in [ TaskQueueManager.RUN_UNREACHABLE_HOSTS, TaskQueueManager.RUN_FAILED_HOSTS ]: error_msg += '{}\n'.format(new_error_msg) if error_msg != '': raise AnsibleError(error_msg)
def _connect(manager_interrupt, scan_task, hosts, result_store, credential, connection_port, forks, use_paramiko=False, exclude_hosts=None, base_ssh_executable=None, ssh_timeout=None): """Attempt to connect to hosts using the given credential. :param manager_interrupt: Signal used to communicate termination of scan :param scan_task: The scan task for this connection job :param hosts: The collection of hosts to test connections :param result_store: The result store to accept the results. :param credential: The credential used for connections :param connection_port: The connection port :param use_paramiko: use paramiko instead of ssh for connection :param forks: number of forks to run with :param exclude_hosts: Optional. Hosts to exclude from test connections :param base_ssh_executable: ssh executable, or None for 'ssh'. Will be wrapped with a timeout before being passed to Ansible. :param ssh_timeout: string in the format of the 'timeout' command. Timeout for individual tasks. :returns: list of connected hosts credential tuples and list of host that failed connection """ cred_data = CredentialSerializer(credential).data ssh_executable = os.path.abspath( os.path.join(os.path.dirname(__file__), '../../../bin/timeout_ssh')) base_ssh_executable = base_ssh_executable or 'ssh' ssh_timeout = ssh_timeout or settings.QPC_SSH_CONNECT_TIMEOUT # pylint: disable=line-too-long # the ssh arg is required for become-pass because # ansible checks for an exact string match of ssh # anywhere in the command array # See https://github.com/ansible/ansible/blob/stable-2.3/lib/ansible/plugins/connection/ssh.py#L490-L500 # noqa # timeout_ssh will remove the ssh argument before running the command ssh_args = [ '--executable=' + base_ssh_executable, '--timeout=' + ssh_timeout, 'ssh' ] group_names, inventory = _construct_connect_inventory( hosts, cred_data, connection_port, forks, exclude_hosts, ssh_executable, ssh_args) inventory_file = write_to_yaml(inventory) _handle_ssh_passphrase(cred_data) log_message = 'START CONNECT PROCESSING GROUPS'\ ' with use_paramiko: %s and %d forks' % (use_paramiko, forks) scan_task.log_message(log_message) for idx, group_name in enumerate(group_names): check_manager_interrupt(manager_interrupt.value) group_ips = inventory.get('all').get('children').get(group_name).get( 'hosts').keys() group_ips = ["'%s'" % ip for ip in group_ips] group_ip_string = ', '.join(group_ips) log_message = 'START CONNECT PROCESSING GROUP %d of %d. '\ 'About to connect to hosts [%s]' % ( (idx + 1), len(group_names), group_ip_string) scan_task.log_message(log_message) call = ConnectResultCallback(result_store, credential, scan_task.source, manager_interrupt) # Create parameters for ansible runner runner_settings = { 'job_timeout': int(settings.NETWORK_CONNECT_JOB_TIMEOUT) } extra_vars_dict = {'variable_host': group_name} playbook_path = os.path.join(settings.BASE_DIR, 'scanner/network/runner/connect.yml') cmdline_list = [] vault_file_path = '--vault-password-file=%s' % ( settings.DJANGO_SECRET_PATH) cmdline_list.append(vault_file_path) forks_cmd = '--forks=%s' % (forks) cmdline_list.append(forks_cmd) if use_paramiko: cmdline_list.append('--connection=paramiko') # paramiko conn all_commands = ' '.join(cmdline_list) if int(settings.ANSIBLE_LOG_LEVEL) == 0: quiet_bool = True verbosity_lvl = 0 else: quiet_bool = False verbosity_lvl = int(settings.ANSIBLE_LOG_LEVEL) try: runner_obj = ansible_runner.run( quiet=quiet_bool, settings=runner_settings, inventory=inventory_file, extravars=extra_vars_dict, event_handler=call.event_callback, cancel_callback=call.cancel_callback, playbook=playbook_path, cmdline=all_commands, verbosity=verbosity_lvl) except Exception as err_msg: raise AnsibleRunnerException(err_msg) final_status = runner_obj.status if final_status != 'successful': if final_status == 'canceled': if manager_interrupt.value == ScanJob.JOB_TERMINATE_CANCEL: msg = log_messages.NETWORK_PLAYBOOK_STOPPED % ('CONNECT', 'canceled') return msg, scan_task.CANCELED msg = log_messages.NETWORK_PLAYBOOK_STOPPED % ('CONNECT', 'paused') return msg, scan_task.PAUSED if final_status not in ['unreachable', 'failed', 'canceled']: if final_status == 'timeout': error = log_messages.NETWORK_TIMEOUT_ERR else: error = log_messages.NETWORK_UNKNOWN_ERR if scan_task.systems_scanned: msg = log_messages.NETWORK_CONNECT_CONTINUE % ( final_status, str(scan_task.systems_scanned), error) scan_task.log_message(msg, log_level=logging.ERROR) else: msg = log_messages.NETWORK_CONNECT_FAIL % (final_status, error) return msg, scan_task.FAILED return None, scan_task.COMPLETED
def retrieve(self, request, pk=None): # pylint: disable=unused-argument """Get a host credential.""" host_cred = get_object_or_404(self.queryset, pk=pk) serializer = CredentialSerializer(host_cred) cred = mask_credential(serializer.data) return Response(cred)
def setUp(self): """Create test case setup.""" self.cred = Credential( name='cred1', username='******', password='******', ssh_keyfile=None, become_method=None, become_user=None, become_password=None) self.cred.save() hc_serializer = CredentialSerializer(self.cred) self.cred_data = hc_serializer.data self.source = Source( name='source1', port=22, hosts='["1.2.3.4"]') self.source.save() self.source.credentials.add(self.cred) self.host_list = [('1.2.3.4', self.cred_data)] self.connect_scan_task = ScanTask(source=self.source, scan_type=ScanTask.SCAN_TYPE_CONNECT, status=ScanTask.COMPLETED, start_time=datetime.utcnow()) self.connect_scan_task.update_stats( 'TEST NETWORK CONNECT.', sys_failed=0) self.inspect_scan_task = ScanTask(source=self.source, scan_type=ScanTask.SCAN_TYPE_INSPECT, start_time=datetime.utcnow()) self.inspect_scan_task.update_stats( 'TEST NETWORK INSPECT.', sys_failed=0) self.inspect_scan_task.prerequisites.add(self.connect_scan_task) self.inspect_scan_task.save() self.scan_job = ScanJob(scan_type=ScanTask.SCAN_TYPE_INSPECT) self.scan_job.save() self.scan_job.tasks.add(self.connect_scan_task) self.scan_job.tasks.add(self.inspect_scan_task) scan_options = ScanOptions() scan_options.disable_optional_products = {'jboss_eap': False, 'jboss_fuse': False, 'jboss_brms': False} scan_options.save() self.scan_job.options = scan_options self.scan_job.save() self.fact_endpoint = 'http://testserver' + reverse('facts-list') self.conn_results = JobConnectionResult() self.conn_results.save() self.scan_job.connection_results = self.conn_results self.conn_result = TaskConnectionResult( scan_task=self.connect_scan_task, source=self.source) self.conn_result.save() success_sys = SystemConnectionResult( name='1.2.3.4', credential=self.cred, status=SystemConnectionResult.SUCCESS) success_sys.save() failed_sys = SystemConnectionResult( name='1.1.1.2', status=SystemConnectionResult.FAILED) failed_sys.save() self.conn_result.systems.add(success_sys) self.conn_result.systems.add(failed_sys) self.conn_result.save() self.conn_results.results.add(self.conn_result) self.conn_results.save() self.scan_job.connection_results = self.conn_results self.inspect_results = JobInspectionResult() self.inspect_results.save() self.scan_job.inspection_results = self.inspect_results self.scan_job.save()