# Copyright 2016 Red Hat, Inc.
#
#    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.

import os

import fixtures
from oslo_log import log as logging

from neutron.common import utils


LOG = logging.getLogger(__name__)
MAX_ATTEMPTS = 100
TMP_DIR = '/tmp/neutron_exclusive_resources/'


class ExclusiveResource(fixtures.Fixture):
    def __init__(self, resource_name, allocator_function, validator=None):
        self.ra = ResourceAllocator(
            resource_name, allocator_function, validator)

    def _setUp(self):
        self.resource = self.ra.allocate()
        self.addCleanup(self.ra.release, self.resource)


class ResourceAllocator(object):
    """ResourceAllocator persists cross-process allocations of a resource.

    Allocations are persisted to a file determined by the 'resource_name',
    and are allocated via an allocator_function. The public interface
    (allocate and release) are guarded by a file lock. The intention
    is to allow atomic, cross-process allocation of shared resources
    such as ports and IP addresses. For usages of this class, please see
    ExclusiveIPAddress and its functional tests.

    Note that this class doesn't maintain in-memory state, and multiple
    instances of it may be initialized and used. A pool of resources
    is identified solely by the 'resource_name' argument.
    """
    def __init__(self, resource_name, allocator_function, validator=None):
        """Initialize a resource allocator.

        :param resource_name: A unique identifier for a pool of resources.
        :param allocator_function: A function with no parameters that generates
                                   a resource.
        :param validator: An optional function that accepts a resource and an
                          existing pool and returns if the generated resource
                          is valid.
        """
        def is_valid(new_resource, allocated_resources):
            return new_resource not in allocated_resources

        self._allocator_function = allocator_function
        self._state_file_path = os.path.join(TMP_DIR, resource_name)
        self._validator = validator if validator else is_valid
        self._resource_name = resource_name

    @utils.synchronized('resource_allocator', external=True, lock_path='/tmp')
    def allocate(self):
        allocations = self._get_allocations()

        for i in range(MAX_ATTEMPTS):
            resource = str(self._allocator_function())
            if self._validator(resource, allocations):
                allocations.add(resource)
                self._write_allocations(allocations)
                LOG.debug('Allocated exclusive resource %s of type %s. '
                          'The allocations are now: %s',
                          resource, self._resource_name, allocations)
                return resource

        raise ValueError(
            'Could not allocate a new resource of type %s from pool %s' %
            (self._resource_name, allocations))

    @utils.synchronized('resource_allocator', external=True, lock_path='/tmp')
    def release(self, resource):
        allocations = self._get_allocations()
        allocations.discard(resource)
        if allocations:
            self._write_allocations(allocations)
        else:  # Clean up the file if we're releasing the last allocation
            os.remove(self._state_file_path)
        LOG.debug('Released exclusive resource %s of type %s. The allocations '
                  'are now: %s',
                  resource, self._resource_name, allocations)

    def _get_allocations(self):
        utils.ensure_dir(TMP_DIR)

        try:
            with open(self._state_file_path, 'r') as allocations_file:
                contents = allocations_file.read()
        except IOError:
            contents = None

        # If the file was empty, we want to return an empty set, not {''}
        return set(contents.split(',')) if contents else set()

    def _write_allocations(self, allocations):
        with open(self._state_file_path, 'w') as allocations_file:
            allocations_file.write(','.join(allocations))
