Add framework for managing segment id pools.

Also added the virst implementation, vxlan, to the NVP driver.
This commit is contained in:
Brad Morgan
2016-01-23 01:17:04 -08:00
parent cb3b2b2a0d
commit ed46b2f0b4
14 changed files with 1373 additions and 5 deletions

View File

@@ -0,0 +1,295 @@
# Copyright 2013 Openstack Foundation
# 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.
"""
Provide strategies for allocating network segments. (vlan, vxlan, etc)
"""
from quark.db import api as db_api
from quark import exceptions as quark_exceptions
from oslo_log import log as logging
from oslo_utils import timeutils
import itertools
import random
LOG = logging.getLogger(__name__)
class BaseSegmentAllocation(object):
segment_type = None
def _validate_range(self, context, sa_range):
raise NotImplementedError()
def _chunks(self, iterable, chunk_size):
"""Chunks data into chunk with size<=chunk_size."""
iterator = iter(iterable)
chunk = list(itertools.islice(iterator, 0, chunk_size))
while chunk:
yield chunk
chunk = list(itertools.islice(iterator, 0, chunk_size))
def _check_collisions(self, new_range, existing_ranges):
"""Check for overlapping ranges."""
def _contains(num, r1):
return (num >= r1[0] and
num <= r1[1])
def _is_overlap(r1, r2):
return (_contains(r1[0], r2) or
_contains(r1[1], r2) or
_contains(r2[0], r1) or
_contains(r2[1], r1))
for existing_range in existing_ranges:
if _is_overlap(new_range, existing_range):
return True
return False
def _make_segment_allocation_dict(self, id, sa_range):
return dict(
id=id,
segment_id=sa_range["segment_id"],
segment_type=sa_range["segment_type"],
segment_allocation_range_id=sa_range["id"],
deallocated=True
)
def _populate_range(self, context, sa_range):
first_id = sa_range["first_id"]
last_id = sa_range["last_id"]
id_range = xrange(first_id, last_id + 1)
LOG.info("Starting segment allocation population for "
"range:%s size:%s."
% (sa_range["id"], len(id_range)))
total_added = 0
for chunk in self._chunks(id_range, 5000):
sa_dicts = []
for segment_id in chunk:
sa_dict = self._make_segment_allocation_dict(
segment_id, sa_range)
sa_dicts.append(sa_dict)
db_api.segment_allocation_range_populate_bulk(context, sa_dicts)
context.session.flush()
total_added = total_added + len(sa_dicts)
LOG.info("Populated %s/%s segment ids for range:%s"
% (total_added, len(id_range), sa_range["id"]))
LOG.info("Finished segment allocation population for "
"range:%s size:%s."
% (sa_range["id"], len(id_range)))
def _create_range(self, context, sa_range):
with context.session.begin(subtransactions=True):
# Validate any range-specific things, like min/max ids.
self._validate_range(context, sa_range)
# Check any existing ranges for this segment for collisions
segment_id = sa_range["segment_id"]
segment_type = sa_range["segment_type"]
filters = {"segment_id": segment_id,
"segment_type": segment_type}
existing_ranges = db_api.segment_allocation_range_find(
context, lock_mode=True, scope=db_api.ALL, **filters)
collides = self._check_collisions(
(sa_range["first_id"], sa_range["last_id"]),
[(r["first_id"], r["last_id"]) for r in existing_ranges])
if collides:
raise quark_exceptions.InvalidSegmentAllocationRange(
msg=("The specified allocation collides with existing "
"range"))
return db_api.segment_allocation_range_create(
context, **sa_range)
def create_range(self, context, sa_range):
return self._create_range(context, sa_range)
def populate_range(self, context, sa_range):
return self._populate_range(context, sa_range)
def _try_allocate(self, context, segment_id, network_id):
"""Find a deallocated network segment id and reallocate it.
NOTE(morgabra) This locks the segment table, but only the rows
in use by the segment, which is pretty handy if we ever have
more than 1 segment or segment type.
"""
LOG.info("Attempting to allocate segment for network %s "
"segment_id %s segment_type %s"
% (network_id, segment_id, self.segment_type))
filter_dict = {
"segment_id": segment_id,
"segment_type": self.segment_type,
"do_not_use": False
}
available_ranges = db_api.segment_allocation_range_find(
context, scope=db_api.ALL, **filter_dict)
available_range_ids = [r["id"] for r in available_ranges]
try:
with context.session.begin(subtransactions=True):
# Search for any deallocated segment ids for the
# given segment.
filter_dict = {
"deallocated": True,
"segment_id": segment_id,
"segment_type": self.segment_type,
"segment_allocation_range_ids": available_range_ids
}
# NOTE(morgabra) We select 100 deallocated segment ids from
# the table here, and then choose 1 randomly. This is to help
# alleviate the case where an uncaught exception might leave
# an allocation active on a remote service but we do not have
# a record of it locally. If we *do* end up choosing a
# conflicted id, the caller should simply allocate another one
# and mark them all as reserved. If a single object has
# multiple reservations on the same segment, they will not be
# deallocated, and the operator must resolve the conficts
# manually.
allocations = db_api.segment_allocation_find(
context, lock_mode=True, **filter_dict).limit(100).all()
if allocations:
allocation = random.choice(allocations)
# Allocate the chosen segment.
update_dict = {
"deallocated": False,
"deallocated_at": None,
"network_id": network_id
}
allocation = db_api.segment_allocation_update(
context, allocation, **update_dict)
LOG.info("Allocated segment %s for network %s "
"segment_id %s segment_type %s"
% (allocation["id"], network_id, segment_id,
self.segment_type))
return allocation
except Exception:
LOG.exception("Error in segment reallocation.")
LOG.info("Cannot find reallocatable segment for network %s "
"segment_id %s segment_type %s"
% (network_id, segment_id, self.segment_type))
def allocate(self, context, segment_id, network_id):
allocation = self._try_allocate(
context, segment_id, network_id)
if allocation:
return allocation
raise quark_exceptions.SegmentAllocationFailure(
segment_id=segment_id, segment_type=self.segment_type)
def _try_deallocate(self, context, segment_id, network_id):
LOG.info("Attempting to deallocate segment for network %s "
"segment_id %s segment_type %s"
% (network_id, segment_id, self.segment_type))
with context.session.begin(subtransactions=True):
filter_dict = {
"deallocated": False,
"segment_id": segment_id,
"segment_type": self.segment_type,
"network_id": network_id
}
allocations = db_api.segment_allocation_find(
context, **filter_dict).all()
if not allocations:
LOG.info("Could not find allocated segment for network %s "
"segment_id %s segment_type %s for deallocate."
% (network_id, segment_id, self.segment_type))
return
if len(allocations) > 1:
LOG.error("Found multiple allocated segments for network %s "
"segment_id %s segment_type %s for deallocate. "
"Refusing to deallocate, these allocations are now "
"orphaned."
% (network_id, segment_id, self.segment_type))
return
allocation = allocations[0]
# Deallocate the found segment.
update_dict = {
"deallocated": True,
"deallocated_at": timeutils.utcnow(),
"network_id": None
}
allocation = db_api.segment_allocation_update(
context, allocation, **update_dict)
LOG.info("Deallocated %s allocated segment(s) for network %s "
"segment_id %s segment_type %s"
% (len(allocations), network_id, segment_id,
self.segment_type))
def deallocate(self, context, segment_id, network_id):
self._try_deallocate(context, segment_id, network_id)
class VXLANSegmentAllocation(BaseSegmentAllocation):
VXLAN_MIN = 1
VXLAN_MAX = (2 ** 24) - 1
segment_type = 'vxlan'
def _validate_range(self, context, sa_range):
# Validate that the range is legal and makes sense.
try:
first_id = sa_range["first_id"]
last_id = sa_range["last_id"]
first_id, last_id = (int(first_id), int(last_id))
assert first_id >= self.VXLAN_MIN
assert last_id <= self.VXLAN_MAX
assert first_id <= last_id
except Exception:
raise quark_exceptions.InvalidSegmentAllocationRange(
msg="The specified allocation range is invalid")
class SegmentAllocationRegistry(object):
def __init__(self):
self.strategies = {
VXLANSegmentAllocation.segment_type: VXLANSegmentAllocation(),
}
def is_valid_strategy(self, strategy_name):
if strategy_name in self.strategies:
return True
return False
def get_strategy(self, strategy_name):
if self.is_valid_strategy(strategy_name):
return self.strategies[strategy_name]
raise Exception("Segment allocation strategy %s not found."
% (strategy_name))
REGISTRY = SegmentAllocationRegistry()