class ImageSweeperDeleteImageTestCase(unittest.TestCase):
    DATASTORE_ID = "DS01"
    BASE_TEMP_DIR = "delete_image"
    IMAGE_MARKER_FILENAME = ImageManager.UNUSED_IMAGE_MARKER_FILE_NAME
    IMAGE_TIMESTAMP_FILENAME = ImageManager.IMAGE_TIMESTAMP_FILE_NAME

    def setUp(self):
        self.test_dir = os.path.join(tempfile.mkdtemp(), self.BASE_TEMP_DIR)
        services.register(ServiceName.AGENT_CONFIG, MagicMock())
        self.vim_client = MagicMock()
        self.vim_client.delete_file.side_effect = self.patched_delete
        self.image_manager = ImageManager(self.vim_client, MagicMock())
        self.vm_manager = MagicMock()
        self.image_sweeper = DatastoreImageSweeper(self.image_manager, self.DATASTORE_ID)
        self.deleted = False
        self.marker_unlinked = False

        # Create various image directories and empty vmdks
        image_id_1 = str(uuid.uuid4())
        image_id_2 = str(uuid.uuid4())
        image_id_3 = str(uuid.uuid4())
        image_id_4 = str(uuid.uuid4())

        self.image_id_1 = image_id_1
        self.image_id_2 = image_id_2
        self.image_id_3 = image_id_3
        self.image_id_4 = image_id_4

        dir1 = os.path.join(self.test_dir, "image_" + image_id_1)
        os.makedirs(dir1)
        dir2 = os.path.join(self.test_dir, "image_" + image_id_2)
        os.makedirs(dir2)
        dir3 = os.path.join(self.test_dir, "image_" + image_id_3)
        os.makedirs(dir3)
        dir4 = os.path.join(self.test_dir, "image_" + image_id_4)
        os.makedirs(dir4)

        self.marker_file_content_time = 0
        self.timestamp_file_mod_time = 0

        # Create a good image vmdk under image_id_1, also create a valid image marker file and a valid timestamp file
        vmdk_filename = image_id_1 + ".vmdk"
        open(os.path.join(dir1, vmdk_filename), 'w').close()
        timestamp_filename = os.path.join(dir1, self.IMAGE_TIMESTAMP_FILENAME)
        open(timestamp_filename, 'w').close()
        marker_filename = os.path.join(dir1, self.IMAGE_MARKER_FILENAME)
        open(marker_filename, 'w').close()

        # Create a good image vmdk under image_id_2, create timestamp but no image marker file,
        vmdk_filename = image_id_2 + ".vmdk"
        open(os.path.join(dir2, vmdk_filename), 'w').close()
        timestamp_filename = os.path.join(dir2, self.IMAGE_TIMESTAMP_FILENAME)
        open(timestamp_filename, 'w').close()

        # Create a good image vmdk under image_id_3, create image_marker file but no timestamp file
        vmdk_filename = image_id_3 + ".vmdk"
        open(os.path.join(dir3, vmdk_filename), 'w').close()
        marker_filename = os.path.join(dir3, self.IMAGE_MARKER_FILENAME)
        open(marker_filename, 'w').close()

        # Create a good image vmdk under image_id_4, create image_marker file, renamed timestamp file
        # but no timestamp file
        vmdk_filename = image_id_4 + ".vmdk"
        open(os.path.join(dir4, vmdk_filename), 'w').close()
        marker_filename = os.path.join(dir4, self.IMAGE_MARKER_FILENAME)
        open(marker_filename, 'w').close()

    def tearDown(self):
        shutil.rmtree(self.test_dir, True)

    # The following test plays with the content of marker file (a timestamp) and the mod time of
    # the image timestamp file before and after rename. It should delete the image only if
    # marker - grace period > timestamp AND timestamp == timestamp after rename
    # All the other cases should point to the fact that the image has been used after the image
    # scan started. To avoid problems due to non synchronized clocks on different hosts
    # a grace period of 10 minutes is applied on the timestamp from the marker file
    @parameterized.expand([
        # marker time, mod time, deleted
        (1061, 1000, True),
        (1060, 1000, False),
        (1000, 1000, False),
        (1000, 1001, False)
    ])
    @patch("host.image.image_manager.ImageManager._image_directory")
    @patch("host.image.image_manager.ImageManager._read_marker_file")
    @patch("host.image.image_manager.ImageManager._get_datastore_type")
    @patch("host.image.image_manager.ImageManager._get_mod_time")
    @patch("os.unlink")
    def test_delete_image(self, marker_file_content_time, timestamp_file_mod_time, deleted,
                          unlink, get_mod_time, get_datastore_type, read_marker_file, image_directory):

        self.marker_file_content_time = marker_file_content_time
        self.timestamp_file_mod_time = timestamp_file_mod_time
        marker_unlinked = not deleted

        read_marker_file.side_effect = self.patched_read_marker_file
        get_datastore_type.side_effect = self.patched_get_datastore_type
        get_mod_time.side_effect = self.patched_get_mod_time
        unlink.side_effect = self.patched_unlink
        image_directory.return_value = os.path.join(self.test_dir, "image_" + self.image_id_1)

        ret = self.image_manager.delete_image(self.DATASTORE_ID, self.image_id_1, 60)

        assert_that(deleted is ret)
        assert_that(deleted is self.deleted)
        assert_that(marker_unlinked is self.marker_unlinked)

    @patch("host.image.image_manager.ImageManager._image_directory")
    @patch("host.image.image_manager.ImageManager._get_datastore_type")
    @patch("host.image.image_manager.ImageManager._get_mod_time")
    def test_delete_image_no_marker_file(self, get_mod_time, get_datastore_type, image_directory):

        get_datastore_type.side_effect = self.patched_get_datastore_type
        get_mod_time.side_effect = self.patched_get_mod_time

        image_directory.return_value = os.path.join(self.test_dir, "image_" + self.image_id_2)
        deleted = self.image_manager.delete_image(self.DATASTORE_ID, self.image_id_2, 60)

        assert_that(deleted is False)
        assert_that(self.deleted is False)

    @patch("host.image.image_manager.ImageManager._image_directory")
    @patch("host.image.image_manager.ImageManager._read_marker_file")
    @patch("host.image.image_manager.ImageManager._get_datastore_type")
    def test_delete_image_no_timestamp_file(self, get_datastore_type, read_marker_file, image_directory):

        read_marker_file.side_effect = self.patched_read_marker_file
        get_datastore_type.side_effect = self.patched_get_datastore_type

        self.marker_file_content_time = 1000
        image_directory.return_value = os.path.join(self.test_dir, "image_" + self.image_id_3)
        deleted = self.image_manager.delete_image(self.DATASTORE_ID, self.image_id_3, 60)

        assert_that(deleted is True)
        assert_that(self.deleted is True)

    def patched_read_marker_file(self, filename):
        if not os.path.exists(filename):
            raise OSError
        return self.marker_file_content_time

    def patched_get_datastore_type(self, datastore_id):
        return DatastoreType.EXT3

    def patched_get_mod_time(self, filename):
        try:
            os.path.getmtime(filename)
        except OSError as ex:
            if ex.errno == errno.ENOENT:
                return False, 0
            else:
                raise ex
        # fix mod_time
        mod_time = self.timestamp_file_mod_time
        return True, mod_time

    def patched_unlink(self, target):
        self.marker_unlinked = True

    def patched_delete(self, target):
        self.deleted = True
class ImageSweeperDeleteImageTestCase(unittest.TestCase):
    DATASTORE_ID = "DS01"
    BASE_TEMP_DIR = "delete_image"
    IMAGE_MARKER_FILENAME = ImageManager.UNUSED_IMAGE_MARKER_FILE_NAME
    IMAGE_TIMESTAMP_FILENAME = ImageManager.IMAGE_TIMESTAMP_FILE_NAME

    def setUp(self):
        self.test_dir = os.path.join(tempfile.mkdtemp(), self.BASE_TEMP_DIR)
        services.register(ServiceName.AGENT_CONFIG, MagicMock())
        self.vim_client = MagicMock()
        self.vim_client.delete_file.side_effect = self.patched_delete
        self.image_manager = ImageManager(self.vim_client, MagicMock())
        self.vm_manager = MagicMock()
        self.image_sweeper = DatastoreImageSweeper(self.image_manager, self.DATASTORE_ID)
        self.deleted = False
        self.marker_unlinked = False

        # Create various image directories and empty vmdks
        image_id_1 = str(uuid.uuid4())
        image_id_2 = str(uuid.uuid4())
        image_id_3 = str(uuid.uuid4())
        image_id_4 = str(uuid.uuid4())

        self.image_id_1 = image_id_1
        self.image_id_2 = image_id_2
        self.image_id_3 = image_id_3
        self.image_id_4 = image_id_4

        dir1 = os.path.join(self.test_dir, "image_" + image_id_1)
        os.makedirs(dir1)
        dir2 = os.path.join(self.test_dir, "image_" + image_id_2)
        os.makedirs(dir2)
        dir3 = os.path.join(self.test_dir, "image_" + image_id_3)
        os.makedirs(dir3)
        dir4 = os.path.join(self.test_dir, "image_" + image_id_4)
        os.makedirs(dir4)

        self.marker_file_content_time = 0
        self.timestamp_file_mod_time = 0

        # Create a good image vmdk under image_id_1, also create a valid image marker file and a valid timestamp file
        vmdk_filename = image_id_1 + ".vmdk"
        open(os.path.join(dir1, vmdk_filename), 'w').close()
        timestamp_filename = os.path.join(dir1, self.IMAGE_TIMESTAMP_FILENAME)
        open(timestamp_filename, 'w').close()
        marker_filename = os.path.join(dir1, self.IMAGE_MARKER_FILENAME)
        open(marker_filename, 'w').close()

        # Create a good image vmdk under image_id_2, create timestamp but no image marker file,
        vmdk_filename = image_id_2 + ".vmdk"
        open(os.path.join(dir2, vmdk_filename), 'w').close()
        timestamp_filename = os.path.join(dir2, self.IMAGE_TIMESTAMP_FILENAME)
        open(timestamp_filename, 'w').close()

        # Create a good image vmdk under image_id_3, create image_marker file but no timestamp file
        vmdk_filename = image_id_3 + ".vmdk"
        open(os.path.join(dir3, vmdk_filename), 'w').close()
        marker_filename = os.path.join(dir3, self.IMAGE_MARKER_FILENAME)
        open(marker_filename, 'w').close()

        # Create a good image vmdk under image_id_4, create image_marker file, renamed timestamp file
        # but no timestamp file
        vmdk_filename = image_id_4 + ".vmdk"
        open(os.path.join(dir4, vmdk_filename), 'w').close()
        marker_filename = os.path.join(dir4, self.IMAGE_MARKER_FILENAME)
        open(marker_filename, 'w').close()

    def tearDown(self):
        shutil.rmtree(self.test_dir, True)

    # The following test plays with the content of marker file (a timestamp) and the mod time of
    # the image timestamp file before and after rename. It should delete the image only if
    # marker - grace period > timestamp AND timestamp == timestamp after rename
    # All the other cases should point to the fact that the image has been used after the image
    # scan started. To avoid problems due to non synchronized clocks on different hosts
    # a grace period of 10 minutes is applied on the timestamp from the marker file
    @parameterized.expand([
        # marker time, mod time, deleted
        (1061, 1000, True),
        (1060, 1000, False),
        (1000, 1000, False),
        (1000, 1001, False)
    ])
    @patch("host.image.image_manager.ImageManager._image_directory")
    @patch("host.image.image_manager.ImageManager._read_marker_file")
    @patch("host.image.image_manager.ImageManager._get_datastore_type")
    @patch("host.image.image_manager.ImageManager._get_mod_time")
    @patch("os.unlink")
    def test_delete_image(self, marker_file_content_time, timestamp_file_mod_time, deleted,
                          unlink, get_mod_time, get_datastore_type, read_marker_file, image_directory):

        self.marker_file_content_time = marker_file_content_time
        self.timestamp_file_mod_time = timestamp_file_mod_time
        marker_unlinked = not deleted

        read_marker_file.side_effect = self.patched_read_marker_file
        get_datastore_type.side_effect = self.patched_get_datastore_type
        get_mod_time.side_effect = self.patched_get_mod_time
        unlink.side_effect = self.patched_unlink
        image_directory.return_value = os.path.join(self.test_dir, "image_" + self.image_id_1)

        ret = self.image_manager.delete_image(self.DATASTORE_ID, self.image_id_1, 60)

        assert_that(deleted is ret)
        assert_that(deleted is self.deleted)
        assert_that(marker_unlinked is self.marker_unlinked)

    @patch("host.image.image_manager.ImageManager._image_directory")
    @patch("host.image.image_manager.ImageManager._get_datastore_type")
    @patch("host.image.image_manager.ImageManager._get_mod_time")
    def test_delete_image_no_marker_file(self, get_mod_time, get_datastore_type, image_directory):

        get_datastore_type.side_effect = self.patched_get_datastore_type
        get_mod_time.side_effect = self.patched_get_mod_time

        image_directory.return_value = os.path.join(self.test_dir, "image_" + self.image_id_2)
        deleted = self.image_manager.delete_image(self.DATASTORE_ID, self.image_id_2, 60)

        assert_that(deleted is False)
        assert_that(self.deleted is False)

    @patch("host.image.image_manager.ImageManager._image_directory")
    @patch("host.image.image_manager.ImageManager._read_marker_file")
    @patch("host.image.image_manager.ImageManager._get_datastore_type")
    def test_delete_image_no_timestamp_file(self, get_datastore_type, read_marker_file, image_directory):

        read_marker_file.side_effect = self.patched_read_marker_file
        get_datastore_type.side_effect = self.patched_get_datastore_type

        self.marker_file_content_time = 1000
        image_directory.return_value = os.path.join(self.test_dir, "image_" + self.image_id_3)
        deleted = self.image_manager.delete_image(self.DATASTORE_ID, self.image_id_3, 60)

        assert_that(deleted is True)
        assert_that(self.deleted is True)

    def patched_read_marker_file(self, filename):
        if not os.path.exists(filename):
            raise OSError
        return self.marker_file_content_time

    def patched_get_datastore_type(self, datastore_id):
        return DatastoreType.EXT3

    def patched_get_mod_time(self, filename):
        try:
            os.path.getmtime(filename)
        except OSError as ex:
            if ex.errno == errno.ENOENT:
                return False, 0
            else:
                raise ex
        # fix mod_time
        mod_time = self.timestamp_file_mod_time
        return True, mod_time

    def patched_unlink(self, target):
        self.marker_unlinked = True

    def patched_delete(self, target):
        self.deleted = True