You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
117 lines
4.6 KiB
Python
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))
|