def copy_to_device(self,
                       steps,
                       device,
                       origin,
                       destination,
                       protocol,
                       verify_num_images=VERIFY_NUM_IMAGES,
                       expected_num_images=EXPECTED_NUM_IMAGES,
                       vrf=VRF,
                       timeout=TIMEOUT,
                       compact=COMPACT,
                       use_kstack=USE_KSTACK,
                       protected_files=PROTECTED_FILES,
                       overwrite=OVERWRITE,
                       skip_deletion=SKIP_DELETION,
                       copy_attempts=COPY_ATTEMPTS,
                       copy_attempts_sleep=COPY_ATTEMPTS_SLEEP,
                       check_file_stability=CHECK_FILE_STABILITY,
                       stability_check_tries=STABILITY_CHECK_TRIES,
                       stability_check_delay=STABILITY_CHECK_DELAY,
                       min_free_space_percent=MIN_FREE_SPACE_PERCENT,
                       **kwargs):
        switch_image = None
        controller_image = None

        # ['aci-apic-dk9.5.1.2e.iso', 'aci-n9000-dk9.15.1.2e.bin']
        with steps.start('Checking image file versions') as step:
            controller_image_version = 'unknown'
            switch_image_version = 'unknown'
            image_files = origin.get('files')
            for fn in image_files:
                if 'apic' in fn:
                    controller_image = fn
                    controller_image_version = apic_get_firmware_version_from_image_name(
                        device, controller_image)
                elif 'n9000' in fn:
                    switch_image = fn
                    switch_image_version = nxos_aci_get_firmware_version_from_image_name(
                        device, switch_image)

            log.info(f'Controller image version: {controller_image_version}')
            log.info(f'Switch image version: {switch_image_version}')

        with steps.start('Checking fabric versions') as step:
            if not device.connected:
                device.connect()
            version_info = device.parse('show version')
            try:
                controller_version = version_info['pod'][1]['node'][1][
                    'version']
            except KeyError:
                log.debug('Could not get info from show version',
                          exc_info=True)
                controller_version = 'unknown'

            if controller_image_version:
                with step.start(
                        "Checking controller image version") as substep:
                    if controller_version == controller_image_version:
                        upgrade_msg = ', skipping upgrade'
                        image_files.remove(controller_image)
                    else:
                        upgrade_msg = ', upgrade needed'

                    log.info(
                        'Controller {} version: {}, controller image version: {}{}'
                        .format(device.name, controller_version,
                                controller_image_version, upgrade_msg))

            download_switch_image = False
            with step.start("Checking switch image versions") as substep:
                for node_idx in version_info['pod'][1]['node']:
                    if node_idx > 100:
                        node_version = version_info['pod'][1]['node'][
                            node_idx]['version']
                        if node_version != switch_image_version:
                            log.info(
                                'Switch {}, version {}, upgrade needed'.format(
                                    node_idx, node_version))
                            download_switch_image = True
                            break
                        else:
                            log.info('Switch {}, version {}'.format(
                                node_idx, node_version))

            if download_switch_image is False and switch_image in image_files:
                image_files.remove(switch_image)

        if image_files:
            log.info(f'Images to download: {image_files}')

        log.info(
            "Section steps:\n1- Verify correct number of images provided"
            "\n2- Get filesize of image files on remote server"
            "\n3- Check if image files already exist on device"
            "\n4- (Optional) Verify stability of image files"
            "\n5- Verify free space on device else delete unprotected files"
            "\n6- Copy image files to device"
            "\n7- Verify copied image files are present on device")

        # list of destination directories
        destinations = []

        # Get args
        server = origin['hostname']

        # Establish FileUtils session for all FileUtils operations
        file_utils = FileUtils(testbed=device.testbed)

        string_to_remove = file_utils.get_server_block(server).get('path', '')
        image_files = remove_string_from_image(images=origin['files'],
                                               string=string_to_remove)

        # Set active node destination directory
        destination_act = destination['directory']

        # Set standby node destination directory
        if 'standby_directory' in destination:
            destination_stby = destination['standby_directory']
            destinations = [destination_stby, destination_act]
        else:
            destination_stby = None
            destinations = [destination_act]

        # Check remote server info present in testbed YAML
        if not file_utils.get_server_block(server):
            self.failed("Server '{}' was provided in the clean yaml file but "
                        "doesn't exist in the testbed file.\n".format(server))

        # Check image files provided
        if verify_num_images:
            # Verify correct number of images provided
            with steps.start(
                    "Verify correct number of images provided") as step:
                if not verify_num_images_provided(
                        image_list=image_files,
                        expected_images=expected_num_images):
                    step.failed(
                        "Incorrect number of images provided. Please "
                        "provide {} expected image(s) under destination"
                        ".path in clean yaml file.\n".format(
                            expected_num_images))
                else:
                    step.passed("Correct number of images provided")

        # Loop over all image files provided by user
        for index, file in enumerate(image_files):
            # Init
            files_to_copy = {}
            unknown_size = False

            # Get filesize of image files on remote server
            with steps.start("Get filesize of '{}' on remote server '{}'".\
                             format(file, server)) as step:
                try:
                    file_size = device.api.get_file_size_from_server(
                        server=file_utils.get_hostname(server),
                        path=file,
                        protocol=protocol,
                        timeout=timeout,
                        fu_session=file_utils)
                except FileNotFoundError:
                    step.failed(
                        "Can not find file {} on server {}. Terminating clean".
                        format(file, server))
                except Exception as e:
                    log.warning(str(e))
                    # Something went wrong, set file_size to -1
                    file_size = -1
                    unknown_size = True
                    err_msg = "\nUnable to get filesize for file '{}' on "\
                              "remote server {}".format(file, server)
                    if overwrite:
                        err_msg += " - will copy file to device"
                    step.passx(err_msg)
                else:
                    step.passed("Verified filesize of file '{}' to be "
                                "{} bytes".format(file, file_size))
            for dest in destinations:

                # Execute 'dir' before copying image files
                dir_before = device.execute('ls -l {}'.format(dest))

                # Check if file with same name and size exists on device
                dest_file_path = os.path.join(dest, os.path.basename(file))
                image_mapping = self.history[
                    'CopyToDevice'].parameters.setdefault('image_mapping', {})
                image_mapping.update({origin['files'][index]: dest_file_path})
                with steps.start("Check if file '{}' exists on device {} {}".\
                                format(dest_file_path, device.name, dest)) as step:
                    # Check if file exists
                    try:
                        exist = device.api.verify_file_exists(
                            file=dest_file_path,
                            size=file_size,
                            dir_output=dir_before)
                    except Exception as e:
                        exist = False
                        log.warning(
                            "Unable to check if image '{}' exists on device {} {}."
                            "Error: {}".format(dest_file_path, device.name,
                                               dest, str(e)))

                    if (not exist) or (exist and overwrite):
                        # Update list of files to copy
                        file_copy_info = {
                            file: {
                                'size': file_size,
                                'dest_path': dest_file_path,
                                'exist': exist
                            }
                        }
                        files_to_copy.update(file_copy_info)
                        # Print message to user
                        step.passed("Proceeding with copying image {} to device {}".\
                                    format(dest_file_path, device.name))
                    else:
                        step.passed(
                            "Image '{}' already exists on device {} {}, "
                            "skipping copy".format(file, device.name, dest))

                # Check if any file copy is in progress
                if check_file_stability:
                    for file in files_to_copy:
                        with steps.start("Verify stability of file '{}'".\
                                         format(file)) as step:
                            # Check file stability
                            try:
                                stable = device.api.verify_file_size_stable_on_server(
                                    file=file,
                                    server=file_utils.get_hostname(server),
                                    protocol=protocol,
                                    fu_session=file_utils,
                                    delay=stability_check_delay,
                                    max_tries=stability_check_tries)

                                if not stable:
                                    step.failed(
                                        "The size of file '{}' on server is not "
                                        "stable\n".format(file), )
                                else:
                                    step.passed(
                                        "Size of file '{}' is stable".format(
                                            file))
                            except NotImplementedError:
                                # cannot check using tftp
                                step.passx(
                                    "Unable to check file stability over {protocol}"
                                    .format(protocol=protocol))
                            except Exception as e:
                                log.error(str(e))
                                step.failed(
                                    "Error while verifying file stability on "
                                    "server\n")

                # Verify available space on the device is sufficient for image copy, delete
                # unprotected files if needed, copy file to the device
                # unless overwrite: False
                if files_to_copy:
                    with steps.start(
                            "Verify sufficient free space on device '{}' '{}' or delete"
                            " unprotected files".format(device.name,
                                                        dest)) as step:

                        if unknown_size:
                            total_size = -1
                            log.warning(
                                "Amount of space required cannot be confirmed, "
                                "copying the files on the device '{}' '{}' may fail"
                                .format(device.name, dest))

                        if not protected_files:
                            protected_files = [r'^.+$(?<!\.bin)(?<!\.iso)']

                        # Try to free up disk space if skip_deletion is not set to True
                        if not skip_deletion:
                            # TODO: add golden images, config to protected files once we have golden section
                            golden_config = find_clean_variable(
                                self, 'golden_config')
                            golden_image = find_clean_variable(
                                self, 'golden_image')

                            if golden_config:
                                protected_files.extend(golden_config)
                            if golden_image:
                                protected_files.extend(golden_image)

                            # Only calculate size of file being copied
                            total_size = sum(
                                0 if file_data['exist'] else file_data['size']
                                for file_data in files_to_copy.values())

                            try:
                                free_space = device.api.free_up_disk_space(
                                    destination=dest,
                                    required_size=total_size,
                                    skip_deletion=skip_deletion,
                                    protected_files=protected_files,
                                    min_free_space_percent=
                                    min_free_space_percent)
                                if not free_space:
                                    step.failed(
                                        "Unable to create enough space for "
                                        "image on device {} {}".format(
                                            device.name, dest))
                                else:
                                    step.passed(
                                        "Device {} {} has sufficient space to "
                                        "copy images".format(
                                            device.name, dest))
                            except Exception as e:
                                log.error(str(e))
                                step.failed(
                                    "Error while creating free space for "
                                    "image on device {} {}".format(
                                        device.name, dest))

                # Copy the file to the devices
                for file, file_data in files_to_copy.items():
                    with steps.start("Copying image file {} to device {} {}".\
                                     format(file, device.name, dest)) as step:

                        # Copy file unless overwrite is False
                        if not overwrite and file_data['exist']:
                            step.skipped(
                                "File with the same name size exists on "
                                "the device {} {}, skipped copying".format(
                                    device.name, dest))

                        for i in range(1, copy_attempts + 1):
                            try:
                                device.api.\
                                    copy_to_device(protocol=protocol,
                                                   server=file_utils.get_hostname(server),
                                                   remote_path=file,
                                                   local_path=file_data['dest_path'],
                                                   vrf=vrf,
                                                   timeout=timeout,
                                                   compact=compact,
                                                   use_kstack=use_kstack,
                                                   **kwargs)
                            except Exception as e:
                                # if user wants to retry
                                if i < copy_attempts:
                                    log.warning(
                                        "Could not copy file '{file}' to '{d}', {e}\n"
                                        "attempt #{iteration}".format(
                                            file=file,
                                            d=device.name,
                                            e=e,
                                            iteration=i + 1))
                                    log.info(
                                        "Sleeping for {} seconds before retrying"
                                        .format(copy_attempts_sleep))
                                    time.sleep(copy_attempts_sleep)
                                else:
                                    substep.failed("Could not copy '{file}' to '{d}'\n{e}"\
                                                   .format(file=file, d=device.name, e=e))
                            else:
                                log.info(
                                    "File {} has been copied to {} on device {}"
                                    " successfully".format(
                                        file, dest, device.name))
                                break
                        # Save the file copied path and size info for future use
                        history = self.history['CopyToDevice'].parameters.\
                                            setdefault('files_copied', {})
                        history.update({file: file_data})

                with steps.start("Verify images successfully copied") as step:
                    # If nothing copied don't need to verify, skip
                    if 'files_copied' not in self.history[
                            'CopyToDevice'].parameters:
                        step.skipped(
                            "Image files were not copied for {} {} in previous steps, "
                            "skipping verification steps".format(
                                device.name, dest))

                    # Execute 'dir' after copying image files
                    dir_after = device.execute('dir {}'.format(dest))

                    for name, image_data in self.history['CopyToDevice'].\
                                                    parameters['files_copied'].items():
                        with step.start("Verify image '{}' copied to {} on device {}".\
                                        format(image_data['dest_path'], dest, device.name)) as substep:
                            # if size is -1 it means it failed to get the size
                            if image_data['size'] != -1:
                                if not device.api.verify_file_exists(
                                        file=image_data['dest_path'],
                                        size=image_data['size'],
                                        dir_output=dir_after):
                                    substep.failed("Size of image file copied to device {} is "
                                                   "not the same as remote server filesize".\
                                                   format(device.name))
                                else:
                                    substep.passed("Size of image file copied to device {} is "
                                                   "the same as image filesize on remote server".\
                                                   format(device.name))
                            else:
                                substep.skipped(
                                    "Image file has been copied to device {} correctly"
                                    " but cannot verify file size".format(
                                        device.name))

        self.passed("Copy to device completed")
    def update_install_repository(self,
                                  steps,
                                  device,
                                  image,
                                  tftp_server=TFTP_SERVER,
                                  packages=None,
                                  install_timeout=INSTALL_TIMEOUT,
                                  source_directory=SOURCE_DIRECTORY):
        if packages is None:
            packages = []

        with steps.start("Adding image and any provided packages to the "
                         "install repository") as step:

            # Get Ip address of tftp server if tftp_server is provided
            if tftp_server:
                if not hasattr(device.testbed, 'servers'):
                    step.failed("Cannot find any servers in the testbed")
                fu = FileUtils(testbed=device.testbed)
                tftp_server = fu.get_hostname(tftp_server)

            # Get Image Files from install_image_and_packages

            # Get Image Files from TFTP path if tftp_server is provided
            if tftp_server:
                # Separate directory and image
                directory, image = self._getFileNameFromPath(image[0])
                pkgs = ''
                for pkg in packages:
                    _, pkg = self._getFileNameFromPath(pkg)
                    pkgs += ' ' + pkg

                directory = '{p}://{s}/{f}'.format(p='tftp',
                                                   s=tftp_server,
                                                   f=directory)

            # Get Image Files in device location
            elif ':' in image[0]:
                # Separate directory and image
                directory, image = self._getFileNameFromPath(image[0])
                # Get packages and remove directories
                # pkgs = ' pkg1 pkg2 pkg3 ...'
                pkgs = ''
                for pkg in packages:
                    _, pkg = self._getFileNameFromPath(pkg)
                    pkgs += ' ' + pkg

            else:
                directory = source_directory
                _, image = self._getFileNameFromPath(image[0])
                # Get packages and remove directories
                # pkgs = ' pkg1 pkg2 pkg3 ...'
                pkgs = ''
                for pkg in packages:
                    _, pkg = self._getFileNameFromPath(pkg)
                    pkgs += ' ' + pkg

            # install add tftp://tftp_server_ip/tftp_path/ <image> <pkg1> <pkg2> or
            # install add source flash: <image> <pkg1> <pkg2>
            cmd = 'install add source {dir} {image} {packages}'.format(
                dir=directory, image=image, packages=pkgs)

            try:
                device.execute(cmd,
                               timeout=install_timeout,
                               error_pattern=self.error_patterns)

                out = device.expect([self.successful_operation_string],
                                    trim_buffer=False,
                                    timeout=install_timeout)
            except Exception as e:
                step.failed("The command '{cmd}' failed. Error: {e}".format(
                    cmd=cmd, e=str(e)))

            out = out.match_output

            # If code execution reaches here the regex has already been matched
            # via the expect. So we know it will match again here. We just need
            # to retrieve the operation id for the next steps.
            p1 = re.compile(self.successful_operation_string)
            for line in out.splitlines():
                m = p1.match(line)
                if m:
                    self.operation_id = m.groupdict()['id']
                    break

            step.passed("The command '{cmd}' succeeded. The "
                        "operation ID is '{operation_id}'".format(
                            cmd=cmd, operation_id=self.operation_id))