# Copyright 2012 Michael Still and Canonical Inc
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

"""Image cache manager.

The cache manager implements the specification at
http://wiki.openstack.org/nova-image-cache-management.

"""

import hashlib
import os
import re
import time

from oslo.config import cfg
from oslo.serialization import jsonutils
from oslo_concurrency import processutils

from nova.i18n import _LE
from nova.i18n import _LI
from nova.i18n import _LW
from nova.openstack.common import fileutils
from nova.openstack.common import log as logging
from nova import utils
from nova.virt import imagecache
from nova.virt.libvirt import utils as libvirt_utils

LOG = logging.getLogger(__name__)

imagecache_opts = [
    cfg.StrOpt('image_info_filename_pattern',
               default='$instances_path/$image_cache_subdirectory_name/'
                       '%(image)s.info',
               help='Allows image information files to be stored in '
                    'non-standard locations'),
    cfg.BoolOpt('remove_unused_kernels',
                default=False,
                help='Should unused kernel images be removed? This is only '
                     'safe to enable if all compute nodes have been updated '
                     'to support this option. This will be enabled by default '
                     'in future.'),
    cfg.IntOpt('remove_unused_resized_minimum_age_seconds',
               default=3600,
               help='Unused resized base images younger than this will not be '
                    'removed'),
    cfg.BoolOpt('checksum_base_images',
                default=False,
                help='Write a checksum for files in _base to disk'),
    cfg.IntOpt('checksum_interval_seconds',
               default=3600,
               help='How frequently to checksum base images'),
    ]

CONF = cfg.CONF
CONF.register_opts(imagecache_opts, 'libvirt')
CONF.import_opt('instances_path', 'nova.compute.manager')
CONF.import_opt('image_cache_subdirectory_name', 'nova.virt.imagecache')


def get_cache_fname(images, key):
    """Return a filename based on the SHA1 hash of a given image ID.

    Image files stored in the _base directory that match this pattern
    are considered for cleanup by the image cache manager. The cache
    manager considers the file to be in use if it matches an instance's
    image_ref, kernel_id or ramdisk_id property.

    However, in grizzly-3 and before, only the image_ref property was
    considered. This means that it's unsafe to store kernel and ramdisk
    images using this pattern until we're sure that all compute nodes
    are running a cache manager newer than grizzly-3. For now, we
    require admins to confirm that by setting the remove_unused_kernels
    boolean but, at some point in the future, we'll be safely able to
    assume this.
    """
    image_id = str(images[key])
    if ((not CONF.libvirt.remove_unused_kernels and
         key in ['kernel_id', 'ramdisk_id'])):
        return image_id
    else:
        return hashlib.sha1(image_id).hexdigest()


def get_info_filename(base_path):
    """Construct a filename for storing additional information about a base
    image.

    Returns a filename.
    """

    base_file = os.path.basename(base_path)
    return (CONF.libvirt.image_info_filename_pattern
            % {'image': base_file})


def is_valid_info_file(path):
    """Test if a given path matches the pattern for info files."""

    digest_size = hashlib.sha1().digestsize * 2
    regexp = (CONF.libvirt.image_info_filename_pattern
              % {'image': ('([0-9a-f]{%(digest_size)d}|'
                           '[0-9a-f]{%(digest_size)d}_sm|'
                           '[0-9a-f]{%(digest_size)d}_[0-9]+)'
                           % {'digest_size': digest_size})})
    m = re.match(regexp, path)
    if m:
        return True
    return False


def _read_possible_json(serialized, info_file):
    try:
        d = jsonutils.loads(serialized)

    except ValueError as e:
        LOG.error(_LE('Error reading image info file %(filename)s: '
                      '%(error)s'),
                  {'filename': info_file,
                   'error': e})
        d = {}

    return d


def read_stored_info(target, field=None, timestamped=False):
    """Read information about an image.

    Returns an empty dictionary if there is no info, just the field value if
    a field is requested, or the entire dictionary otherwise.
    """

    info_file = get_info_filename(target)
    if not os.path.exists(info_file):
        # NOTE(mikal): Special case to handle essex checksums being converted.
        # There is an assumption here that target is a base image filename.
        old_filename = target + '.sha1'
        if field == 'sha1' and os.path.exists(old_filename):
            hash_file = open(old_filename)
            hash_value = hash_file.read()
            hash_file.close()

            write_stored_info(target, field=field, value=hash_value)
            os.remove(old_filename)
            d = {field: hash_value}

        else:
            d = {}

    else:
        lock_name = 'info-%s' % os.path.split(target)[-1]
        lock_path = os.path.join(CONF.instances_path, 'locks')

        @utils.synchronized(lock_name, external=True, lock_path=lock_path)
        def read_file(info_file):
            LOG.debug('Reading image info file: %s', info_file)
            with open(info_file, 'r') as f:
                return f.read().rstrip()

        serialized = read_file(info_file)
        d = _read_possible_json(serialized, info_file)

    if field:
        if timestamped:
            return (d.get(field, None), d.get('%s-timestamp' % field, None))
        else:
            return d.get(field, None)
    return d


def write_stored_info(target, field=None, value=None):
    """Write information about an image."""

    if not field:
        return

    info_file = get_info_filename(target)
    LOG.info(_LI('Writing stored info to %s'), info_file)
    fileutils.ensure_tree(os.path.dirname(info_file))

    lock_name = 'info-%s' % os.path.split(target)[-1]
    lock_path = os.path.join(CONF.instances_path, 'locks')

    @utils.synchronized(lock_name, external=True, lock_path=lock_path)
    def write_file(info_file, field, value):
        d = {}

        if os.path.exists(info_file):
            with open(info_file, 'r') as f:
                d = _read_possible_json(f.read(), info_file)

        d[field] = value
        d['%s-timestamp' % field] = time.time()

        with open(info_file, 'w') as f:
            f.write(jsonutils.dumps(d))

    write_file(info_file, field, value)


def _hash_file(filename):
    """Generate a hash for the contents of a file."""
    checksum = hashlib.sha1()
    with open(filename) as f:
        for chunk in iter(lambda: f.read(32768), b''):
            checksum.update(chunk)
    return checksum.hexdigest()


def read_stored_checksum(target, timestamped=True):
    """Read the checksum.

    Returns the checksum (as hex) or None.
    """
    return read_stored_info(target, field='sha1', timestamped=timestamped)


def write_stored_checksum(target):
    """Write a checksum to disk for a file in _base."""
    write_stored_info(target, field='sha1', value=_hash_file(target))


class ImageCacheManager(imagecache.ImageCacheManager):
    def __init__(self):
        super(ImageCacheManager, self).__init__()
        self.lock_path = os.path.join(CONF.instances_path, 'locks')
        self._reset_state()

    def _reset_state(self):
        """Reset state variables used for each pass."""

        self.used_images = {}
        self.image_popularity = {}
        self.instance_names = set()

        self.back_swap_images = set()
        self.used_swap_images = set()

        self.active_base_files = []
        self.corrupt_base_files = []
        self.originals = []
        self.removable_base_files = []