def get_connection(module):
    global _DEVICE_CONNECTION
    if not _DEVICE_CONNECTION:
        connection_proxy = Connection(module._socket_path)
        cap = json.loads(connection_proxy.get_capabilities())
        if cap['network_api'] == 'cliconf':
            conn = Cli(module)
        elif cap['network_api'] == 'exosapi':
            conn = HttpApi(module)
        else:
            module.fail_json(msg='Invalid connection type %s' %
                             cap['network_api'])
        _DEVICE_CONNECTION = conn
    return _DEVICE_CONNECTION
Exemple #2
0
def get_connection(module):
    global _DEVICE_CONNECTION
    if not _DEVICE_CONNECTION:
        if is_local_eapi(module):
            conn = LocalEapi(module)
        else:
            connection_proxy = Connection(module._socket_path)
            cap = json.loads(connection_proxy.get_capabilities())
            if cap['network_api'] == 'cliconf':
                conn = Cli(module)
            elif cap['network_api'] == 'eapi':
                conn = HttpApi(module)
        _DEVICE_CONNECTION = conn
    return _DEVICE_CONNECTION
Exemple #3
0
class ActionModule(ActionBase):
    def process_playbook_values(self):
        ''' Get playbook values and perform input validation '''
        argument_spec = dict(
            vrf=dict(type='str', default='management'),
            connect_ssh_port=dict(type='int', default=22),
            file_system=dict(type='str', default='bootflash:'),
            file_pull=dict(type='bool', default=False),
            file_pull_timeout=dict(type='int', default=300),
            file_pull_compact=dict(type='bool', default=False),
            file_pull_kstack=dict(type='bool', default=False),
            local_file=dict(type='path'),
            local_file_directory=dict(type='path'),
            remote_file=dict(type='path'),
            remote_scp_server=dict(type='str'),
            remote_scp_server_user=dict(type='str'),
            remote_scp_server_password=dict(no_log=True),
        )

        playvals = {}
        # Process key value pairs from playbook task
        for key in argument_spec.keys():
            playvals[key] = self._task.args.get(
                key, argument_spec[key].get('default'))
            if playvals[key] is None:
                continue

            option_type = argument_spec[key].get('type', 'str')
            try:
                if option_type == 'str':
                    playvals[key] = validation.check_type_str(playvals[key])
                elif option_type == 'int':
                    playvals[key] = validation.check_type_int(playvals[key])
                elif option_type == 'bool':
                    playvals[key] = validation.check_type_bool(playvals[key])
                elif option_type == 'path':
                    playvals[key] = validation.check_type_path(playvals[key])
                else:
                    raise AnsibleError(
                        'Unrecognized type <{0}> for playbook parameter <{1}>'.
                        format(option_type, key))

            except (TypeError, ValueError) as e:
                raise AnsibleError(
                    "argument %s is of type %s and we were unable to convert to %s: %s"
                    % (key, type(playvals[key]), option_type, to_native(e)))

        # Validate playbook dependencies
        if playvals['file_pull']:
            if playvals.get('remote_file') is None:
                raise AnsibleError(
                    'Playbook parameter <remote_file> required when <file_pull> is True'
                )
            if playvals.get('remote_scp_server') is None:
                raise AnsibleError(
                    'Playbook parameter <remote_scp_server> required when <file_pull> is True'
                )

        if playvals['remote_scp_server'] or \
           playvals['remote_scp_server_user']:

            if None in (playvals['remote_scp_server'],
                        playvals['remote_scp_server_user']):
                params = '<remote_scp_server>, <remote_scp_server_user>'
                raise AnsibleError(
                    'Playbook parameters {0} must be set together'.format(
                        params))

        return playvals

    def check_library_dependencies(self, file_pull):
        if file_pull:
            if not HAS_PEXPECT:
                msg = 'library pexpect is required when file_pull is True but does not appear to be '
                msg += 'installed. It can be installed using `pip install pexpect`'
                raise AnsibleError(msg)
        else:
            if paramiko is None:
                msg = 'library paramiko is required when file_pull is False but does not appear to be '
                msg += 'installed. It can be installed using `pip install paramiko`'
                raise AnsibleError(msg)

            if not HAS_SCP:
                msg = 'library scp is required when file_pull is False but does not appear to be '
                msg += 'installed. It can be installed using `pip install scp`'
                raise AnsibleError(msg)

    def md5sum_check(self, dst, file_system):
        command = 'show file {0}{1} md5sum'.format(file_system, dst)
        remote_filehash = self.conn.exec_command(command)
        remote_filehash = to_bytes(remote_filehash,
                                   errors='surrogate_or_strict')

        local_file = self.playvals['local_file']
        try:
            with open(local_file, 'rb') as f:
                filecontent = f.read()
        except (OSError, IOError) as exc:
            raise AnsibleError('Error reading the file: {0}'.format(
                to_text(exc)))

        filecontent = to_bytes(filecontent, errors='surrogate_or_strict')
        local_filehash = hashlib.md5(filecontent).hexdigest()

        if local_filehash == remote_filehash:
            return True
        else:
            return False

    def remote_file_exists(self, remote_file, file_system):
        command = 'dir {0}/{1}'.format(file_system, remote_file)
        body = self.conn.exec_command(command)
        if 'No such file' in body:
            return False
        else:
            return self.md5sum_check(remote_file, file_system)

    def verify_remote_file_exists(self, dst, file_system):
        command = 'dir {0}/{1}'.format(file_system, dst)
        body = self.conn.exec_command(command)
        if 'No such file' in body:
            return 0
        return body.split()[0].strip()

    def local_file_exists(self, file):
        return os.path.isfile(file)

    def get_flash_size(self, file_system):
        command = 'dir {0}'.format(file_system)
        body = self.conn.exec_command(command)

        match = re.search(r'(\d+) bytes free', body)
        if match:
            bytes_free = match.group(1)
            return int(bytes_free)

        match = re.search(r'No such file or directory', body)
        if match:
            raise AnsibleError(
                'Invalid nxos filesystem {0}'.format(file_system))
        else:
            raise AnsibleError(
                'Unable to determine size of filesystem {0}'.format(
                    file_system))

    def enough_space(self, file, file_system):
        flash_size = self.get_flash_size(file_system)
        file_size = os.path.getsize(file)
        if file_size > flash_size:
            return False

        return True

    def transfer_file_to_device(self, remote_file):
        timeout = self.socket_timeout
        local_file = self.playvals['local_file']
        file_system = self.playvals['file_system']

        if not self.enough_space(local_file, file_system):
            raise AnsibleError(
                'Could not transfer file. Not enough space on device.')

        # frp = full_remote_path, flp = full_local_path
        frp = '{0}{1}'.format(file_system, remote_file)
        flp = os.path.join(os.path.abspath(local_file))
        try:
            self.conn.copy_file(source=flp,
                                destination=frp,
                                proto='scp',
                                timeout=timeout)
        except Exception as exc:
            self.results['failed'] = True
            self.results['msg'] = ('Exception received : %s' % exc)

    def file_push(self):
        local_file = self.playvals['local_file']
        remote_file = self.playvals['remote_file'] or os.path.basename(
            local_file)
        file_system = self.playvals['file_system']

        if not self.local_file_exists(local_file):
            raise AnsibleError('Local file {0} not found'.format(local_file))

        remote_file = remote_file or os.path.basename(local_file)
        remote_exists = self.remote_file_exists(remote_file, file_system)

        if not remote_exists:
            self.results['changed'] = True
            file_exists = False
        else:
            self.results[
                'transfer_status'] = 'No Transfer: File already copied to remote device.'
            file_exists = True

        if not self.play_context.check_mode and not file_exists:
            self.transfer_file_to_device(remote_file)
            self.results[
                'transfer_status'] = 'Sent: File copied to remote device.'

        self.results['local_file'] = local_file
        if remote_file is None:
            remote_file = os.path.basename(local_file)
        self.results['remote_file'] = remote_file

    def copy_file_from_remote(self, local, local_file_directory, file_system):
        self.results['failed'] = False
        nxos_hostname = self.play_context.remote_addr
        nxos_username = self.play_context.remote_user
        nxos_password = self.play_context.password
        port = self.playvals['connect_ssh_port']

        # Build copy command components that will be used to initiate copy from the nxos device.
        cmdroot = 'copy scp://'
        ruser = self.playvals['remote_scp_server_user'] + '@'
        rserver = self.playvals['remote_scp_server']
        rfile = self.playvals['remote_file'] + ' '
        vrf = ' vrf ' + self.playvals['vrf']
        local_dir_root = '/'
        if self.playvals['file_pull_compact']:
            compact = ' compact '
        else:
            compact = ''
        if self.playvals['file_pull_kstack']:
            kstack = ' use-kstack '
        else:
            kstack = ''

        def process_outcomes(session, timeout=None):
            if timeout is None:
                timeout = 10
            outcome = {}
            outcome['user_response_required'] = False
            outcome['password_prompt_detected'] = False
            outcome['existing_file_with_same_name'] = False
            outcome['final_prompt_detected'] = False
            outcome['copy_complete'] = False
            outcome['expect_timeout'] = False
            outcome['error'] = False
            outcome['error_data'] = None

            # Possible outcomes key:
            # 0) - Are you sure you want to continue connecting (yes/no)
            # 1) - Password: or @servers's password:
            # 2) - Warning: There is already a file existing with this name. Do you want to overwrite (y/n)?[n]
            # 3) - Timeout conditions
            # 4) - No space on nxos device file_system
            # 5) - Username/Password or file permission issues
            # 6) - File does not exist on remote scp server
            # 7) - invalid nxos command
            # 8) - compact option not supported
            # 9) - compaction attempt failed
            # 10) - other failures like attempting to compact non image file
            # 11) - failure to resolve hostname
            # 12) - Too many authentication failures
            # 13) - Copy to / from this server not permitted
            # 14) - Copy completed without issues
            # 15) - nxos_router_prompt#
            # 16) - pexpect timeout
            possible_outcomes = [
                r'sure you want to continue connecting \(yes/no\)\? ',
                '(?i)Password: '******'file existing with this name', 'timed out',
                '(?i)No space.*#', '(?i)Permission denied.*#',
                '(?i)No such file.*#', '.*Invalid command.*#',
                'Compaction is not supported on this platform.*#',
                'Compact of.*failed.*#', '(?i)Failed.*#',
                '(?i)Could not resolve hostname',
                '(?i)Too many authentication failures',
                r'(?i)Copying to\/from this server name is not permitted',
                '(?i)Copy complete', r'#\s', pexpect.TIMEOUT
            ]
            index = session.expect(possible_outcomes, timeout=timeout)
            # Each index maps to items in possible_outcomes
            if index == 0:
                outcome['user_response_required'] = True
                return outcome
            elif index == 1:
                outcome['password_prompt_detected'] = True
                return outcome
            elif index == 2:
                outcome['existing_file_with_same_name'] = True
                return outcome
            elif index in [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]:
                before = session.before.strip().replace(' \x08', '')
                after = session.after.strip().replace(' \x08', '')
                outcome['error'] = True
                outcome['error_data'] = 'COMMAND {0} ERROR {1}'.format(
                    before, after)
                return outcome
            elif index == 14:
                outcome['copy_complete'] = True
                return outcome
            elif index == 15:
                outcome['final_prompt_detected'] = True
                return outcome
            elif index == 16:
                # The before property will contain all text up to the expected string pattern.
                # The after string will contain the text that was matched by the expected pattern.
                outcome['expect_timeout'] = True
                outcome[
                    'error_data'] = 'Expect Timeout error occurred: BEFORE {0} AFTER {1}'.format(
                        session.before, session.after)
                return outcome
            else:
                outcome['error'] = True
                outcome[
                    'error_data'] = 'Unrecognized error occurred: BEFORE {0} AFTER {1}'.format(
                        session.before, session.after)
                return outcome

            return outcome

        # Spawn pexpect connection to NX-OS device.
        nxos_session = pexpect.spawn('ssh ' + nxos_username + '@' +
                                     nxos_hostname + ' -p' + str(port))
        # There might be multiple user_response_required prompts or intermittent timeouts
        # spawning the expect session so loop up to 24 times during the spawn process.
        max_attempts = 24
        for connect_attempt in range(max_attempts):
            outcome = process_outcomes(nxos_session)
            if outcome['user_response_required']:
                nxos_session.sendline('yes')
                continue
            if outcome['password_prompt_detected']:
                time.sleep(3)
                nxos_session.sendline(nxos_password)
                continue
            if outcome['final_prompt_detected']:
                break
            if outcome['error'] or outcome['expect_timeout']:
                # Error encountered, try to spawn expect session n more times up to max_attempts - 1
                if connect_attempt < max_attempts:
                    outcome['error'] = False
                    outcome['expect_timeout'] = False
                    nxos_session.close()
                    nxos_session = pexpect.spawn('ssh ' + nxos_username + '@' +
                                                 nxos_hostname + ' -p' +
                                                 str(port))
                    continue
                self.results['failed'] = True
                outcome['error_data'] = re.sub(nxos_password, '',
                                               outcome['error_data'])
                self.results[
                    'error_data'] = 'Failed to spawn expect session! ' + outcome[
                        'error_data']
                nxos_session.close()
                return
        else:
            # The before property will contain all text up to the expected string pattern.
            # The after string will contain the text that was matched by the expected pattern.
            msg = 'After {0} attempts, failed to spawn pexpect session to {1}'
            msg += 'BEFORE: {2}, AFTER: {3}'
            error_msg = msg.format(connect_attempt, nxos_hostname,
                                   nxos_session.before, nxos_session.after)
            re.sub(nxos_password, '', error_msg)
            nxos_session.close()
            raise AnsibleError(error_msg)

        # Create local file directory under NX-OS filesystem if
        # local_file_directory playbook parameter is set.
        if local_file_directory:
            dir_array = local_file_directory.split('/')
            for each in dir_array:
                if each:
                    mkdir_cmd = 'mkdir ' + local_dir_root + each
                    nxos_session.sendline(mkdir_cmd)
                    outcome = process_outcomes(nxos_session)
                    if outcome['error'] or outcome['expect_timeout']:
                        self.results['mkdir_cmd'] = mkdir_cmd
                        self.results['failed'] = True
                        outcome['error_data'] = re.sub(nxos_password, '',
                                                       outcome['error_data'])
                        self.results['error_data'] = outcome['error_data']
                        return
                    local_dir_root += each + '/'

        # Initiate file copy
        copy_cmd = (cmdroot + ruser + rserver + rfile + file_system +
                    local_dir_root + local + compact + vrf + kstack)
        self.results['copy_cmd'] = copy_cmd
        nxos_session.sendline(copy_cmd)
        for copy_attempt in range(6):
            outcome = process_outcomes(nxos_session,
                                       self.playvals['file_pull_timeout'])
            if outcome['user_response_required']:
                nxos_session.sendline('yes')
                continue
            if outcome['password_prompt_detected']:
                if self.playvals.get('remote_scp_server_password'):
                    nxos_session.sendline(
                        self.playvals['remote_scp_server_password'])
                else:
                    err_msg = 'Remote scp server {0} requires a password.'.format(
                        rserver)
                    err_msg += ' Set the <remote_scp_server_password> playbook parameter or configure nxos device for passwordless scp'
                    raise AnsibleError(err_msg)
                continue
            if outcome['existing_file_with_same_name']:
                nxos_session.sendline('y')
                continue
            if outcome['copy_complete']:
                self.results[
                    'transfer_status'] = 'Received: File copied/pulled to nxos device from remote scp server.'
                break
            if outcome['error'] or outcome['expect_timeout']:
                self.results['failed'] = True
                outcome['error_data'] = re.sub(nxos_password, '',
                                               outcome['error_data'])
                if self.playvals.get('remote_scp_server_password'):
                    outcome['error_data'] = re.sub(
                        self.playvals['remote_scp_server_password'], '',
                        outcome['error_data'])
                self.results['error_data'] = outcome['error_data']
                nxos_session.close()
                return
        else:
            # The before property will contain all text up to the expected string pattern.
            # The after string will contain the text that was matched by the expected pattern.
            msg = 'After {0} attempts, failed to copy file to {1}'
            msg += 'BEFORE: {2}, AFTER: {3}, CMD: {4}'
            error_msg = msg.format(copy_attempt, nxos_hostname,
                                   nxos_session.before, nxos_session.before,
                                   copy_cmd)
            re.sub(nxos_password, '', error_msg)
            if self.playvals.get('remote_scp_server_password'):
                re.sub(self.playvals['remote_scp_server_password'], '',
                       error_msg)
            nxos_session.close()
            raise AnsibleError(error_msg)

        nxos_session.close()

    def file_pull(self):
        local_file = self.playvals['local_file']
        remote_file = self.playvals['remote_file']
        file_system = self.playvals['file_system']
        # Note: This is the local file directory on the remote nxos device.
        local_file_dir = self.playvals['local_file_directory']

        local_file = local_file or self.playvals['remote_file'].split('/')[-1]

        if not self.play_context.check_mode:
            self.copy_file_from_remote(local_file, local_file_dir, file_system)

        if not self.results['failed']:
            self.results['changed'] = True
            self.results['remote_file'] = remote_file
            if local_file_dir:
                dir = local_file_dir
            else:
                dir = ''
            self.results['local_file'] = file_system + dir + '/' + local_file
            self.results['remote_scp_server'] = self.playvals[
                'remote_scp_server']

    # This is the main run method for the action plugin to copy files
    def run(self, tmp=None, task_vars=None):
        socket_path = None
        self.play_context = copy.deepcopy(self._play_context)
        self.results = super(ActionModule, self).run(task_vars=task_vars)

        if self.play_context.connection.split('.')[-1] != 'network_cli':
            # Plugin is supported only with network_cli
            self.results['failed'] = True
            self.results[
                'msg'] = 'Connection type must be fully qualified name for network_cli connection type, got %s' % self.play_context.connection
            return self.results

        # Get playbook values
        self.playvals = self.process_playbook_values()

        file_pull = self.playvals['file_pull']
        self.check_library_dependencies(file_pull)

        if socket_path is None:
            socket_path = self._connection.socket_path
        self.conn = Connection(socket_path)

        # Call get_capabilities() to start the connection to the device.
        self.conn.get_capabilities()

        self.socket_timeout = self.conn.get_option(
            'persistent_command_timeout')

        # This action plugin support two modes of operation.
        # - file_pull is False - Push files from the ansible controller to nxos switch.
        # - file_pull is True - Initiate copy from the device to pull files to the nxos switch.
        self.results['transfer_status'] = 'No Transfer'
        self.results['file_system'] = self.playvals['file_system']
        if file_pull:
            self.file_pull()
        else:
            self.file_push()

        return self.results
def main():
    """main entry point for execution
    """
    backup_spec = dict(
        filename=dict(),
        dir_path=dict(type='path')
    )
    argument_spec = dict(
        backup=dict(default=False, type='bool'),
        backup_options=dict(type='dict', options=backup_spec),
        config=dict(type='str'),
        commit=dict(type='bool'),
        replace=dict(type='str'),
        rollback=dict(type='int'),
        commit_comment=dict(type='str'),
        defaults=dict(default=False, type='bool'),
        multiline_delimiter=dict(type='str'),
        diff_replace=dict(choices=['line', 'block', 'config']),
        diff_match=dict(choices=['line', 'strict', 'exact', 'none']),
        diff_ignore_lines=dict(type='list')
    )

    mutually_exclusive = [('config', 'rollback')]
    required_one_of = [['backup', 'config', 'rollback']]

    module = AnsibleModule(argument_spec=argument_spec,
                           mutually_exclusive=mutually_exclusive,
                           required_one_of=required_one_of,
                           supports_check_mode=True)

    result = {'changed': False}

    connection = Connection(module._socket_path)
    capabilities = module.from_json(connection.get_capabilities())

    if capabilities:
        device_operations = capabilities.get('device_operations', dict())
        validate_args(module, device_operations)
    else:
        device_operations = dict()

    if module.params['defaults']:
        if 'get_default_flag' in capabilities.get('rpc'):
            flags = connection.get_default_flag()
        else:
            flags = 'all'
    else:
        flags = []

    candidate = module.params['config']
    candidate = to_text(candidate, errors='surrogate_then_replace') if candidate else None
    running = connection.get_config(flags=flags)
    rollback_id = module.params['rollback']

    if module.params['backup']:
        result['__backup__'] = running

    if candidate or rollback_id or module.params['replace']:
        try:
            result.update(run(module, device_operations, connection, candidate, running, rollback_id))
        except Exception as exc:
            module.fail_json(msg=to_text(exc))

    module.exit_json(**result)