Exemple #1
0
 def _manage_disk(self, op, **kwargs):
     try:
         self._logger.debug("Invoking %s(%s)" % (op.info.name, kwargs))
         task = op(self._manager, **kwargs)
         if task:
             self._vim_client.wait_for_task(task)
     except vim.fault.FileFault, e:
         raise DiskFileException(e.msg)
class EsxImageManager(ImageManager):
    NUM_MAKEDIRS_ATTEMPTS = 10
    DEFAULT_TMP_IMAGES_CLEANUP_INTERVAL = 600.0
    IMAGE_TOMBSTONE_FILE_NAME = "image_tombstone.txt"
    IMAGE_MARKER_FILE_NAME = "unused_image_marker.txt"
    IMAGE_TIMESTAMP_FILE_NAME = "image_timestamp.txt"
    IMAGE_TIMESTAMP_FILE_RENAME_SUFFIX = ".renamed"

    def __init__(self, vim_client, ds_manager):
        super(EsxImageManager, self).__init__()
        self._logger = logging.getLogger(__name__)
        self._vim_client = vim_client
        self._ds_manager = ds_manager
        self._image_reaper = None
        self._uwsim_nas_exist = None
        agent_config = services.get(ServiceName.AGENT_CONFIG)
        self._in_uwsim = agent_config.in_uwsim

    def monitor_for_cleanup(self,
                            reap_interval=DEFAULT_TMP_IMAGES_CLEANUP_INTERVAL):
        self._image_reaper = Periodic(self.reap_tmp_images, reap_interval)
        self._image_reaper.daemon = True
        self._image_reaper.start()

    def cleanup(self):
        if self._image_reaper is not None:
            self._image_reaper.stop()

    @log_duration
    def check_image(self, image_id, datastore):
        image_dir = os_vmdk_path(datastore, image_id, IMAGE_FOLDER_NAME)

        try:
            return os.path.exists(image_dir)
        except:
            self._logger.exception("Error looking up %s" % image_dir)
            return False

    """
    The following method is intended
    as a replacement of check_image in
    the vm creation workflow compatible
    with the new image sweeper.
    For an image to be valid both the
    directory and the image timestamp
    file must exists on the datastore.
    """

    def check_and_validate_image(self, image_id, ds_id):
        image_dir = os.path.dirname(
            os_vmdk_path(ds_id, image_id, IMAGE_FOLDER_NAME))

        try:
            if not os.path.exists(image_dir):
                return False
        except:
            self._logger.exception("Error looking up %s" % image_dir)
            return False

        # Check the existence of the timestamp file
        timestamp_pathname = \
            os.path.join(image_dir,
                         self.IMAGE_TIMESTAMP_FILE_NAME)
        try:
            if os.path.exists(timestamp_pathname):
                return True
        except Exception as ex:
            self._logger.exception("Exception looking up %s, %s" %
                                   (timestamp_pathname, ex))
            return False

        return False

    """
    This method is used to update the mod time on the
    image timestamp file. It also checks for the existence
    of a tombstone file for this image. If the tombstone
    file exists it throws an exception.
    """

    def touch_image_timestamp(self, ds_id, image_id):
        """
        :param ds_id:
        :param image_id:
        :return:
        """
        image_path = os.path.dirname(
            os_vmdk_path(ds_id, image_id, IMAGE_FOLDER_NAME))

        # Check the existence of the timestamp file
        tombstone_pathname = \
            os.path.join(image_path,
                         self.IMAGE_TOMBSTONE_FILE_NAME)
        try:
            tombstone = os.path.exists(tombstone_pathname)
        except Exception as ex:
            self._logger.exception("Exception looking up %s, %s" %
                                   (tombstone_pathname, ex))

        if tombstone:
            raise InvalidImageState

        # Touch the timestamp file
        timestamp_pathname = \
            os.path.join(image_path,
                         self.IMAGE_TIMESTAMP_FILE_NAME)
        try:
            os.utime(timestamp_pathname, None)
        except Exception as ex:
            self._logger.exception("Exception looking up %s, %s" %
                                   (timestamp_pathname, ex))
            raise ex

    """
    This method is used to create a tombstone marker
    in the new image management work flow. The tombstone
    marker is a file under the image directory.
    """

    def create_image_tombstone(self, ds_id, image_id):
        """
        :param ds_id:
        :param image_id:
        :return:
        """
        image_path = os.path.dirname(
            os_vmdk_path(ds_id, image_id, IMAGE_FOLDER_NAME))

        # Create tombstone file for the image
        tombstone_pathname = \
            os.path.join(image_path,
                         self.IMAGE_TOMBSTONE_FILE_NAME)
        try:
            open(tombstone_pathname, 'w').close()
        except Exception as ex:
            self._logger.exception("Exception creating %s, %s" %
                                   (tombstone_pathname, ex))
            raise ex

        self._logger.info("Image: %s tombstoned" % tombstone_pathname)

    @log_duration
    def check_image_dir(self, image_id, datastore):
        image_path = os_vmdk_path(datastore, image_id, IMAGE_FOLDER_NAME)
        try:
            return os.path.exists(os.path.dirname(image_path))
        except:
            self._logger.error("Error looking up %s" % image_path,
                               exc_info=True)
            return False

    def get_image_directory_path(self, datastore_id, image_id):
        return image_directory_path(datastore_id, image_id)

    def get_image_path(self, datastore_id, image_id):
        return os_vmdk_path(datastore_id, image_id, IMAGE_FOLDER_NAME)

    def image_size(self, image_id):
        # TODO(mmutsuzaki) We should iterate over all the image datastores
        # until we find one that has the image.
        image_ds = list(self._ds_manager.image_datastores())[0]
        image_path = os_vmdk_flat_path(image_ds, image_id, IMAGE_FOLDER_NAME)

        return os.path.getsize(image_path)

    def _load_json(self, metadata_path):
        if os.path.exists(metadata_path):
            with open(metadata_path) as fh:
                try:
                    data = json.load(fh)
                    return data
                except ValueError:
                    self._logger.error("Error loading metadata file %s" %
                                       metadata_path,
                                       exc_info=True)
        return {}

    def get_image_metadata(self, image_id, datastore):
        metadata_path = os_metadata_path(datastore, image_id,
                                         IMAGE_FOLDER_NAME)
        self._logger.info("Loading metadata %s" % metadata_path)
        return self._load_json(metadata_path)

    def get_image_manifest(self, image_id):
        # This is a shortcut for ttylinux. ttylinux doesn't have manifest file.
        if image_id == "ttylinux":
            return ImageType.CLOUD, ImageReplication.EAGER

        # TODO(mmutsuzaki) We should iterate over all the image datastores
        # until we find one that has the image.
        image_ds = list(self._ds_manager.image_datastores())[0]
        manifest_path = os_image_manifest_path(image_ds, image_id)
        if not os.path.isfile(manifest_path):
            self._logger.info("Manifest file %s not found" % manifest_path)
            return None, None

        self._logger.info("Loading manifest %s" % manifest_path)
        data = self._load_json(manifest_path)
        type = ImageType._NAMES_TO_VALUES[data["imageType"]]
        replication = ImageReplication._NAMES_TO_VALUES[
            data["imageReplication"]]

        return type, replication

    def _get_datastore_type(self, datastore_id):
        datastores = self._ds_manager.get_datastores()
        return [ds.type for ds in datastores if ds.id == datastore_id][0]

    def _prepare_virtual_disk_spec(self, disk_type, adapter_type):
        """
        :param disk_type [vim.VirtualDiskManager.VirtualDiskType]:
        :param adapter_type [vim.VirtualDiskManager.VirtualDiskAdapterType]:
        """
        _vd_spec = vim.VirtualDiskManager.VirtualDiskSpec()
        _vd_spec.diskType = str(disk_type)
        _vd_spec.adapterType = str(adapter_type)

        return _vd_spec

    def _create_tmp_image(self, source_datastore, source_id, dest_datastore,
                          dest_id):
        """ Copy an image into a temp location.
            1. Lock a tmp image destination file with an exclusive lock. This
            is to prevent the GC thread from garbage collecting directories
            that are actively being used.
            The temp directory name contains a random UUID to prevent
            collisions with concurrent copies
            2. Create the temp directory.
            3. Copy the metadata file over.
            4. Copy the vmdk over.

            @return the tmp image directory on success.
        """
        source = vmdk_path(source_datastore, source_id, IMAGE_FOLDER_NAME)
        temp_dest = tmp_image_path(dest_datastore, dest_id)
        ds_type = self._get_datastore_type(dest_datastore)
        tmp_image_dir_path = os.path.dirname(datastore_to_os_path(temp_dest))
        # Try grabbing the lock on the temp directory if it fails
        # (very unlikely) someone else is copying an image just retry
        # later.
        with FileBackedLock(tmp_image_dir_path, ds_type):
            source_meta = os_metadata_path(source_datastore, source_id,
                                           IMAGE_FOLDER_NAME)
            # Create the temp directory
            mkdir_p(tmp_image_dir_path)

            # Copy the metadata file if it exists.
            if os.path.exists(source_meta):
                try:
                    shutil.copy(source_meta, tmp_image_dir_path)
                except:
                    self._logger.exception("Failed to copy metadata file %s",
                                           source_meta)
                    raise

            # Create the timestamp file
            self._create_image_timestamp_file(tmp_image_dir_path)

            _vd_spec = self._prepare_virtual_disk_spec(
                vim.VirtualDiskManager.VirtualDiskType.thin,
                vim.VirtualDiskManager.VirtualDiskAdapterType.lsiLogic)

            self._manage_disk(vim.VirtualDiskManager.CopyVirtualDisk_Task,
                              sourceName=source,
                              destName=temp_dest,
                              destSpec=_vd_spec)
        return tmp_image_dir_path

    def _move_image(self, image_id, datastore, tmp_dir):
        """
        Atomic move of a tmp folder into the image datastore. Handles
        concurrent moves by locking a well know derivative of the image_id
        while doing the atomic move.
        The exclusive file lock ensures that only one move is successful.
        Has the following side effects:
            a - If the destination image already exists, it is assumed that
            someone else successfully copied the image over and the temp
            directory is deleted.
            b - If we fail to acquire the file lock after retrying 3 times,
            or the atomic move fails, the tmp image directory will be left
            behind and needs to be garbage collected later.

        image_id: String.The image id of the image being moved.
        datastore: String. The datastore id of the datastore.
        tmp_dir: String. The absolute path of the temp image directory.

        raises: OsError if the move fails
                AcquireLockFailure, InvalidFile if we fail to lock the
                destination image.
        """
        ds_type = self._get_datastore_type(datastore)
        image_path = os.path.dirname(
            os_vmdk_path(datastore, image_id, IMAGE_FOLDER_NAME))
        parent_path = os.path.dirname(image_path)
        # Create the parent image directory if it doesn't exist.
        try:
            mkdir_p(parent_path)
        except OSError as e:
            if e.errno == errno.EEXIST and os.path.isdir(parent_path):
                # Parent directory exists nothing to do.
                pass
            else:
                raise
        try:
            with FileBackedLock(image_path, ds_type, retry=300,
                                wait_secs=0.01):  # wait lock for 3 seconds
                if self._check_image_repair(image_id, datastore):
                    raise DiskAlreadyExistException("Image already exists")

                shutil.move(tmp_dir, image_path)
        except (AcquireLockFailure, InvalidFile):
            self._logger.info("Unable to lock %s for atomic move" % image_id)
            raise
        except DiskAlreadyExistException:
            self._logger.info("Image %s already copied" % image_id)
            rm_rf(tmp_dir)
            raise

    """
    The following method should be used to check
    and validate the existence of a previously
    created image. With the new image delete path
    the "timestamp" file must exists inside the
    image directory. If the directory exists and
    the file does not, it may mean that an image
    delete operation was aborted mid-way. In this
    case the following method recreate the timestamp
    file. All operations are performed while
    holding the image directory lock (FileBackedLock),
    the caller is required to hold the lock.
    """

    def _check_image_repair(self, image_id, datastore):
        vmdk_pathname = os_vmdk_path(datastore, image_id, IMAGE_FOLDER_NAME)

        image_dirname = os.path.dirname(vmdk_pathname)
        try:
            # Check vmdk file
            if not os.path.exists(vmdk_pathname):
                self._logger.info("Vmdk path doesn't exists: %s" %
                                  vmdk_pathname)
                return False
        except Exception as ex:
            self._logger.exception("Exception validating %s, %s" %
                                   (image_dirname, ex))
            return False

        # Check timestamp file
        timestamp_pathname = \
            os.path.join(image_dirname,
                         self.IMAGE_TIMESTAMP_FILE_NAME)
        try:
            if os.path.exists(timestamp_pathname):
                self._logger.info("Timestamp file exists: %s" %
                                  timestamp_pathname)
                return True
        except Exception as ex:
            self._logger.exception("Exception validating %s, %s" %
                                   (timestamp_pathname, ex))

        # The timestamp file is not accessible,
        # try creating one, if successful try to
        # delete the renamed timestamp file if it
        # exists
        try:
            self._create_image_timestamp_file(image_dirname)
            self._delete_renamed_image_timestamp_file(image_dirname)
        except Exception as ex:
            self._logger.exception("Exception creating %s, %s" %
                                   (timestamp_pathname, ex))
            return False

        self._logger.info("Image repaired: %s" % image_dirname)
        return True

    def copy_image(self, source_datastore, source_id, dest_datastore, dest_id):
        """Copy an image between datastores.

        This method is used to create a "full clone" of a vmdk.
        It does so by copying a disk to a unique directory in a well known
        temporary directory then moving the disk to the destination image
        location. Data in the temporary directory not properly cleaned up
        will be periodically garbage collected by the reaper thread.

        This minimizes the window during which the vmdk path exists with
        incomplete content. It also works around a hostd issue where
        cp -f does not work.

        The current behavior for when the destination disk exists is
        to overwrite said disk.

        source_datastore: id of the source datastore
        source_id: id of the image to copy from
        dest_datastore: id of the destination datastore
        dest_id: id of the new image in the destination datastore

        throws: AcquireLockFailure if timed out waiting to acquire lock on tmp
                image directory
        throws: InvalidFile if unable to lock tmp image directory or some other
                reasons
        """
        if self.check_and_validate_image(dest_id, dest_datastore):
            # The image is copied, presumably via some other concurrent
            # copy, so we move on.
            self._logger.info("Image %s already copied" % dest_id)
            raise DiskAlreadyExistException("Image already exists")

        # Copy image to the tmp directory.
        tmp_dir = self._create_tmp_image(source_datastore, source_id,
                                         dest_datastore, dest_id)

        self._move_image(dest_id, dest_datastore, tmp_dir)

    def reap_tmp_images(self):
        """ Clean up unused directories in the temp image folder. """
        for ds in self._ds_manager.get_datastores():
            images_dir = tmp_image_folder_os_path(ds.id)

            for f in os.listdir(images_dir):
                path = os.path.join(images_dir, f)
                if not os.path.isdir(path):
                    continue
                try:
                    with FileBackedLock(path, ds.type):
                        if (os.path.exists(path)):
                            self._logger.info("Delete folder %s" % path)
                            shutil.rmtree(path, ignore_errors=True)
                except (AcquireLockFailure, InvalidFile):
                    self._logger.info("Already locked: %s, skipping" % path)
                except:
                    self._logger.info("Unable to remove %s" % path,
                                      exc_info=True)

    def delete_image(self, datastore_id, image_id, ds_type, force):
        # Check if the image currently exists
        if not self.check_image_dir(image_id, datastore_id):
            self._logger.info("Image %s on datastore %s not found" %
                              (image_id, datastore_id))
            raise ImageNotFoundException("Image %s not found" % image_id)

        # Mark image as tombstoned
        self.create_image_tombstone(datastore_id, image_id)

        if not force:
            return

        # If force try to actively garbage collect the image here
        if self._lock_data_disk(datastore_id, image_id):
            self._gc_image_dir(datastore_id, image_id)
        else:
            raise ImageInUse("Image %s is currently in use" % image_id)

        # Now attempt GCing the image directory.
        try:
            self._clean_gc_dir(datastore_id)
        except Exception:
            # Swallow the exception the next clean call will clear it all.
            self._logger.exception("Failed to delete gc dir on datastore %s" %
                                   datastore_id)

    def _lock_data_disk(self, datastore_id, image_id):
        """
        Lock the data disks associated with the VMs in the provided ref file.
        Return True if locking was successful false otherwise.
        """
        data_disk = os_vmdk_flat_path(datastore_id, image_id)
        try:
            # Its ok to delete the data disk as a subsequent power on will
            # fail if the data disk is not there.
            os.remove(data_disk)
        except OSError:
            # Remove failed so disk is locked.
            self._logger.debug("Disk %s on datastore %s is already locked" %
                               (data_disk, datastore_id))
            return False
        return True

    def get_images(self, datastore):
        """ Get image list from datastore
        :param datastore: datastore id
        :return: list of string, image id list
        """
        image_ids = []

        # image_folder is /vmfs/volumes/${datastore}/images
        image_folder = os_datastore_path(datastore, IMAGE_FOLDER_NAME)

        if not os.path.exists(image_folder):
            raise DatastoreNotFoundException()

        # prefix is the 2-digit prefix of image id
        for prefix in os.listdir(image_folder):
            # outer path is something like
            # /vmfs/volumes/${datastore}/images/${image_id}[0:2]
            outer_path = os.path.join(image_folder, prefix)
            if not os.path.isdir(outer_path):
                continue

            for image_id in os.listdir(outer_path):
                if self.check_image(image_id, datastore):
                    image_ids.append(image_id)

        return image_ids

    def mark_unused(self, image_scanner):
        images_dir_path = os_datastore_path(image_scanner.datastore_id,
                                            IMAGE_FOLDER_NAME)
        # Log messages with prefix: "IMAGE SCANNER" are for debugging
        # and will be removed after basic testing
        self._logger.info("IMAGE SCANNER: images_dir: %s" % images_dir_path)
        if not os.path.isdir(images_dir_path):
            self._logger.info(
                "images_dir_path: images_dir: %s, doesn't exist" %
                images_dir_path)
            raise DatastoreNotFoundException(
                "Image scanner, cannot find image "
                "directory for datastore: %s" % image_scanner.datastore_id)

        return self._mark_unused_images(image_scanner, images_dir_path)

    def delete_unused(self, image_sweeper):
        images_dir_path = os_datastore_path(image_sweeper.datastore_id,
                                            IMAGE_FOLDER_NAME)
        # Log messages with prefix: "IMAGE SWEEPER" are for debugging
        # and will be removed after basic testing
        self._logger.info("IMAGE SWEEPER: images_dir: %s" % images_dir_path)
        if not os.path.isdir(images_dir_path):
            self._logger.info(
                "images_dir_path: images_dir: %s, doesn't exist" %
                images_dir_path)
            raise DatastoreNotFoundException(
                "Image sweeper, cannot find image "
                "directory for datastore: %s" % image_sweeper.datastore_id)

        return self._delete_unused_images(image_sweeper, images_dir_path)

    def _unzip(self, src, dst):
        self._logger.info("unzip %s -> %s" % (src, dst))

        fsrc = gzip.open(src, "rb")
        fdst = open(dst, "wb")

        try:
            shutil.copyfileobj(fsrc, fdst)
        finally:
            fsrc.close()
            fdst.close()

    def _copy_disk(self, src, dst):
        self._manage_disk(vim.VirtualDiskManager.CopyVirtualDisk_Task,
                          sourceName=src,
                          destName=dst)

    def _manage_disk(self, op, **kwargs):
        if self._in_uwsim:
            self._manage_disk_uwsim(op, **kwargs)
            return

        try:
            self._logger.debug("Invoking %s(%s)" % (op.info.name, kwargs))
            task = op(self._manager, **kwargs)
            self._vim_client.wait_for_task(task)
        except vim.Fault.FileAlreadyExists, e:
            raise DiskAlreadyExistException(e.msg)
        except vim.Fault.FileFault, e:
            raise DiskFileException(e.msg)
Exemple #3
0
class EsxImageManager(ImageManager):
    NUM_MAKEDIRS_ATTEMPTS = 10
    DEFAULT_TMP_IMAGES_CLEANUP_INTERVAL = 600.0
    REAP_TMP_IMAGES_GRACE_PERIOD = 600.0
    IMAGE_MARKER_FILE_NAME = "unused_image_marker.txt"
    IMAGE_TIMESTAMP_FILE_NAME = "image_timestamp.txt"
    IMAGE_TIMESTAMP_FILE_RENAME_SUFFIX = ".renamed"

    def __init__(self, vim_client, ds_manager):
        super(EsxImageManager, self).__init__()
        self._logger = logging.getLogger(__name__)
        self._vim_client = vim_client
        self._ds_manager = ds_manager
        self._image_reaper = None

    def monitor_for_cleanup(self,
                            reap_interval=DEFAULT_TMP_IMAGES_CLEANUP_INTERVAL):
        self._image_reaper = Periodic(self.reap_tmp_images, reap_interval)
        self._image_reaper.daemon = True
        self._image_reaper.start()

    def cleanup(self):
        if self._image_reaper is not None:
            self._image_reaper.stop()

    @log_duration
    def check_image(self, image_id, datastore):
        image_dir = os_vmdk_path(datastore, image_id, IMAGE_FOLDER_NAME_PREFIX)
        try:
            return os.path.exists(image_dir)
        except:
            self._logger.exception(
                "Error looking up %s" % image_dir)
            return False

    """
    The following method is intended
    as a replacement of check_image in
    the vm creation workflow compatible
    with the new image sweeper.
    For an image to be valid both the
    directory and the image timestamp
    file must exists on the datastore.
    """
    def check_and_validate_image(self, image_id, ds_id):
        image_dir = os.path.dirname(
            os_vmdk_path(ds_id, image_id, IMAGE_FOLDER_NAME_PREFIX))

        try:
            if not os.path.exists(image_dir):
                return False
        except:
            self._logger.exception(
                "Error looking up %s" % image_dir)
            return False

        # Check the existence of the timestamp file
        timestamp_pathname = \
            os.path.join(image_dir,
                         self.IMAGE_TIMESTAMP_FILE_NAME)
        try:
            if os.path.exists(timestamp_pathname):
                return True
        except Exception as ex:
            self._logger.exception(
                "Exception looking up %s, %s" % (timestamp_pathname, ex))
            return False

        return False

    """
    This method is used to update the mod time on the
    image timestamp file.
    """
    def touch_image_timestamp(self, ds_id, image_id):
        """
        :param ds_id:
        :param image_id:
        :return:
        """
        image_path = os.path.dirname(
            os_vmdk_path(ds_id, image_id, IMAGE_FOLDER_NAME_PREFIX))

        # Touch the timestamp file
        timestamp_pathname = os.path.join(image_path, self.IMAGE_TIMESTAMP_FILE_NAME)
        try:
            os.utime(timestamp_pathname, None)
        except Exception as ex:
            self._logger.exception(
                "Exception looking up %s, %s" % (timestamp_pathname, ex))
            raise ex

    @log_duration
    def check_image_dir(self, image_id, datastore):
        image_path = os_vmdk_path(datastore, image_id, IMAGE_FOLDER_NAME_PREFIX)
        try:
            return os.path.exists(os.path.dirname(image_path))
        except:
            self._logger.error(
                "Error looking up %s" % image_path, exc_info=True)
            return False

    def get_image_directory_path(self, datastore_id, image_id):
        return image_directory_path(datastore_id, image_id)

    def get_image_path(self, datastore_id, image_id):
        return os_vmdk_path(datastore_id, image_id, IMAGE_FOLDER_NAME_PREFIX)

    def image_size(self, image_id):
        for image_ds in self._ds_manager.image_datastores():
            try:
                image_path = os_vmdk_flat_path(image_ds, image_id,
                                               IMAGE_FOLDER_NAME_PREFIX)
                return os.path.getsize(image_path)
            except os.error:
                self._logger.info("Image %s not found in DataStore %s" %
                                  (image_id, image_ds))

        self._logger.warning("Failed to get image size:",
                             exc_info=True)
        # Failed to access shared image.
        raise NoSuchResourceException(
            ResourceType.IMAGE,
            "Image does not exist.")

    def _load_json(self, metadata_path):
        if os.path.exists(metadata_path):
            with open(metadata_path) as fh:
                try:
                    data = json.load(fh)
                    return data
                except ValueError:
                    self._logger.error(
                        "Error loading metadata file %s" % metadata_path,
                        exc_info=True)
        return {}

    def get_image_metadata(self, image_id, datastore):
        metadata_path = os_metadata_path(datastore,
                                         image_id,
                                         IMAGE_FOLDER_NAME_PREFIX)
        self._logger.info("Loading metadata %s" % metadata_path)
        return self._load_json(metadata_path)

    def _get_datastore_type(self, datastore_id):
        datastores = self._ds_manager.get_datastores()
        return [ds.type for ds in datastores if ds.id == datastore_id][0]

    def _prepare_virtual_disk_spec(self, disk_type, adapter_type):
        """
        :param disk_type [vim.VirtualDiskManager.VirtualDiskType]:
        :param adapter_type [vim.VirtualDiskManager.VirtualDiskAdapterType]:
        """
        _vd_spec = vim.VirtualDiskManager.VirtualDiskSpec()
        _vd_spec.diskType = str(disk_type)
        _vd_spec.adapterType = str(adapter_type)

        return _vd_spec

    def _copy_to_tmp_image(self, source_datastore, source_id, dest_datastore, dest_id):
        """ Copy an image into a temp location.
            1. Lock a tmp image destination file with an exclusive lock. This
            is to prevent the GC thread from garbage collecting directories
            that are actively being used.
            The temp directory name contains a random UUID to prevent
            collisions with concurrent copies
            2. Create the temp directory.
            3. Copy the metadata file over.
            4. Copy the vmdk over.

            @return the tmp image directory on success.
        """
        ds_type = self._get_datastore_type(dest_datastore)
        if ds_type == DatastoreType.VSAN:
            tmp_image_dir = os_datastore_path(dest_datastore,
                                              compond_path_join(IMAGE_FOLDER_NAME_PREFIX, dest_id),
                                              compond_path_join(TMP_IMAGE_FOLDER_NAME_PREFIX, str(uuid.uuid4())))
        else:
            tmp_image_dir = os_datastore_path(dest_datastore,
                                              compond_path_join(TMP_IMAGE_FOLDER_NAME_PREFIX, str(uuid.uuid4())))

        # Create the temp directory
        self._vim_client.make_directory(tmp_image_dir)

        # Copy the metadata file if it exists.
        source_meta = os_metadata_path(source_datastore, source_id, IMAGE_FOLDER_NAME_PREFIX)
        if os.path.exists(source_meta):
            try:
                dest_meta = os.path.join(tmp_image_dir, metadata_filename(dest_id))
                shutil.copy(source_meta, dest_meta)
            except:
                self._logger.exception("Failed to copy metadata file %s", source_meta)
                raise

        # Create the timestamp file
        self._create_image_timestamp_file(tmp_image_dir)

        _vd_spec = self._prepare_virtual_disk_spec(
            vim.VirtualDiskManager.VirtualDiskType.thin,
            vim.VirtualDiskManager.VirtualDiskAdapterType.lsiLogic)

        self._manage_disk(vim.VirtualDiskManager.CopyVirtualDisk_Task,
                          sourceName=vmdk_path(source_datastore, source_id, IMAGE_FOLDER_NAME_PREFIX),
                          destName=os_to_datastore_path(os.path.join(tmp_image_dir, "%s.vmdk" % dest_id)),
                          destSpec=_vd_spec)
        return tmp_image_dir

    def _move_image(self, image_id, datastore, tmp_dir):
        """
        Atomic move of a tmp folder into the image datastore. Handles
        concurrent moves by locking a well know derivative of the image_id
        while doing the atomic move.
        The exclusive file lock ensures that only one move is successful.
        Has the following side effects:
            a - If the destination image already exists, it is assumed that
            someone else successfully copied the image over and the temp
            directory is deleted.
            b - If we fail to acquire the file lock after retrying 3 times,
            or the atomic move fails, the tmp image directory will be left
            behind and needs to be garbage collected later.

        image_id: String.The image id of the image being moved.
        datastore: String. The datastore id of the datastore.
        tmp_dir: String. The absolute path of the temp image directory.

        raises: OsError if the move fails
                AcquireLockFailure, InvalidFile if we fail to lock the
                destination image.
        """
        ds_type = self._get_datastore_type(datastore)
        image_path = os_datastore_path(datastore, compond_path_join(IMAGE_FOLDER_NAME_PREFIX, image_id))
        self._logger.info("_move_image: %s => %s, ds_type: %s" % (tmp_dir, image_path, ds_type))

        if not os.path.exists(tmp_dir):
            raise ImageNotFoundException("Temp image %s not found" % tmp_dir)

        try:
            with FileBackedLock(image_path, ds_type, retry=300, wait_secs=0.01):  # wait lock for 3 seconds
                if self._check_image_repair(image_id, datastore):
                    raise DiskAlreadyExistException("Image already exists")

                if ds_type == DatastoreType.VSAN:
                    # on VSAN, move all files under [datastore]/image_[image_id]/tmp_image_[uuid]/* to
                    # [datastore]/image_[image_id]/*.
                    # Also we do not delete tmp_image folder in success case, because VSAN accesses it
                    # when creating linked VM, even the folder is now empty.
                    for entry in os.listdir(tmp_dir):
                        shutil.move(os.path.join(tmp_dir, entry), os.path.join(image_path, entry))
                else:
                    # on VMFS/NFS/etc, rename [datastore]/tmp_image_[uuid] to [datastore]/tmp_image_[image_id]
                    self._vim_client.move_file(tmp_dir, image_path)
        except:
            self._logger.exception("Move image %s to %s failed" % (image_id, image_path))
            self._vim_client.delete_file(tmp_dir)
            raise

    """
    The following method should be used to check
    and validate the existence of a previously
    created image. With the new image delete path
    the "timestamp" file must exists inside the
    image directory. If the directory exists and
    the file does not, it may mean that an image
    delete operation was aborted mid-way. In this
    case the following method recreate the timestamp
    file. All operations are performed while
    holding the image directory lock (FileBackedLock),
    the caller is required to hold the lock.
    """
    def _check_image_repair(self, image_id, datastore):
        vmdk_pathname = os_vmdk_path(datastore,
                                     image_id,
                                     IMAGE_FOLDER_NAME_PREFIX)

        image_dirname = os.path.dirname(vmdk_pathname)
        try:
            # Check vmdk file
            if not os.path.exists(vmdk_pathname):
                self._logger.info("Vmdk path doesn't exists: %s" %
                                  vmdk_pathname)
                return False
        except Exception as ex:
            self._logger.exception(
                "Exception validating %s, %s" % (image_dirname, ex))
            return False

        # Check timestamp file
        timestamp_pathname = \
            os.path.join(image_dirname,
                         self.IMAGE_TIMESTAMP_FILE_NAME)
        try:
            if os.path.exists(timestamp_pathname):
                self._logger.info("Timestamp file exists: %s" %
                                  timestamp_pathname)
                return True
        except Exception as ex:
            self._logger.exception(
                "Exception validating %s, %s" % (timestamp_pathname, ex))

        # The timestamp file is not accessible,
        # try creating one, if successful try to
        # delete the renamed timestamp file if it
        # exists
        try:
            self._create_image_timestamp_file(image_dirname)
            self._delete_renamed_image_timestamp_file(image_dirname)
        except Exception as ex:
            self._logger.exception(
                "Exception creating %s, %s" % (timestamp_pathname, ex))
            return False

        self._logger.info("Image repaired: %s" %
                          image_dirname)
        return True

    def copy_image(self, source_datastore, source_id, dest_datastore, dest_id):
        """Copy an image between datastores.

        This method is used to create a "full clone" of a vmdk.
        It does so by copying a disk to a unique directory in a well known
        temporary directory then moving the disk to the destination image
        location. Data in the temporary directory not properly cleaned up
        will be periodically garbage collected by the reaper thread.

        This minimizes the window during which the vmdk path exists with
        incomplete content. It also works around a hostd issue where
        cp -f does not work.

        The current behavior for when the destination disk exists is
        to overwrite said disk.

        source_datastore: id of the source datastore
        source_id: id of the image to copy from
        dest_datastore: id of the destination datastore
        dest_id: id of the new image in the destination datastore

        throws: AcquireLockFailure if timed out waiting to acquire lock on tmp
                image directory
        throws: InvalidFile if unable to lock tmp image directory or some other
                reasons
        """
        if self.check_and_validate_image(dest_id, dest_datastore):
            # The image is copied, presumably via some other concurrent
            # copy, so we move on.
            self._logger.info("Image %s already copied" % dest_id)
            raise DiskAlreadyExistException("Image already exists")

        # Copy image to the tmp directory.
        tmp_dir = self._copy_to_tmp_image(source_datastore, source_id,
                                          dest_datastore, dest_id)

        self._move_image(dest_id, dest_datastore, tmp_dir)

    def reap_tmp_images(self):
        """ Clean up unused directories in the temp image folder. """
        for ds in self._ds_manager.get_datastores():
            tmp_image_pattern = os_datastore_path_pattern(ds.id, TMP_IMAGE_FOLDER_NAME_PREFIX)
            for image_dir in glob.glob(tmp_image_pattern):
                if not os.path.isdir(image_dir):
                    continue

                create_time = os.stat(image_dir).st_ctime
                current_time = time.time()
                if current_time - self.REAP_TMP_IMAGES_GRACE_PERIOD < create_time:
                    # Skip folders that are newly created in past x minutes
                    # For example, during host-to-host transfer, hostd on
                    # receiving end stores the uploaded file in temp images
                    # folder but does not lock it with FileBackedLock, so we
                    # need to allow a grace period before reaping it.
                    self._logger.info(
                        "Skip folder: %s, created: %s, now: %s" %
                        (image_dir, create_time, current_time))
                    continue

                try:
                    with FileBackedLock(image_dir, ds.type):
                        if os.path.exists(image_dir):
                            self._logger.info("Delete folder %s" % image_dir)
                            shutil.rmtree(image_dir, ignore_errors=True)
                except (AcquireLockFailure, InvalidFile):
                    self._logger.info("Already locked: %s, skipping" % image_dir)
                except:
                    self._logger.info("Unable to remove %s" % image_dir, exc_info=True)

    def get_images(self, datastore):
        """ Get image list from datastore
        :param datastore: datastore id
        :return: list of string, image id list
        """
        image_ids = []

        if not os.path.exists(os_datastore_root(datastore)):
            raise DatastoreNotFoundException()

        # image_folder is /vmfs/volumes/${datastore}/images_*
        image_folder_pattern = os_datastore_path_pattern(datastore,
                                                         IMAGE_FOLDER_NAME_PREFIX)
        for dir in glob.glob(image_folder_pattern):
            image_id = dir.split(COMPOND_PATH_SEPARATOR)[1]
            if self.check_image(image_id, datastore):
                image_ids.append(image_id)

        return image_ids

    def _unzip(self, src, dst):
        self._logger.info("unzip %s -> %s" % (src, dst))

        fsrc = gzip.open(src, "rb")
        fdst = open(dst, "wb")

        try:
            shutil.copyfileobj(fsrc, fdst)
        finally:
            fsrc.close()
            fdst.close()

    def _copy_disk(self, src, dst):
        self._manage_disk(vim.VirtualDiskManager.CopyVirtualDisk_Task,
                          sourceName=src, destName=dst)

    def _manage_disk(self, op, **kwargs):
        try:
            self._logger.debug("Invoking %s(%s)" % (op.info.name, kwargs))
            task = op(self._manager, **kwargs)
            self._vim_client.wait_for_task(task)
        except vim.Fault.FileAlreadyExists, e:
            raise DiskAlreadyExistException(e.msg)
        except vim.Fault.FileFault, e:
            raise DiskFileException(e.msg)