neutron/neutron/tests/common/exclusive_resources/resource_allocator.py

117 lines
4.6 KiB
Python

# 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 neutron_lib.utils import runtime
from oslo_log import log as logging
from oslo_utils import fileutils
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
@runtime.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))
@runtime.synchronized('resource_allocator', external=True,
lock_path='/tmp')
def release(self, resource):
allocations = self._get_allocations()
allocations.remove(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):
fileutils.ensure_tree(TMP_DIR, mode=0o755)
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))