You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
349 lines
16 KiB
349 lines
16 KiB
# 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.
|
|
|
|
import copy
|
|
|
|
from oslo_log import log as logging
|
|
from oslo_utils import uuidutils
|
|
|
|
from neutron.api.v2 import attributes
|
|
from neutron.callbacks import events
|
|
from neutron.callbacks import registry
|
|
from neutron.callbacks import resources
|
|
from neutron.db import api as db_api
|
|
from neutron.db import common_db_mixin
|
|
from neutron.db import db_base_plugin_common
|
|
from neutron.db import db_base_plugin_v2
|
|
from neutron.objects import base as objects_base
|
|
from neutron.objects import trunk as trunk_objects
|
|
from neutron.services import service_base
|
|
from neutron.services.trunk import callbacks
|
|
from neutron.services.trunk import constants
|
|
from neutron.services.trunk import drivers
|
|
from neutron.services.trunk import exceptions as trunk_exc
|
|
from neutron.services.trunk import rules
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
def _extend_port_trunk_details(core_plugin, port_res, port_db):
|
|
"""Add trunk details to a port."""
|
|
if port_db.trunk_port:
|
|
subports = [{'segmentation_id': x.segmentation_id,
|
|
'segmentation_type': x.segmentation_type,
|
|
'port_id': x.port_id}
|
|
for x in port_db.trunk_port.sub_ports]
|
|
trunk_details = {'trunk_id': port_db.trunk_port.id,
|
|
'sub_ports': subports}
|
|
port_res['trunk_details'] = trunk_details
|
|
|
|
return port_res
|
|
|
|
|
|
class TrunkPlugin(service_base.ServicePluginBase,
|
|
common_db_mixin.CommonDbMixin):
|
|
|
|
supported_extension_aliases = ["trunk", "trunk-details"]
|
|
|
|
__native_pagination_support = True
|
|
__native_sorting_support = True
|
|
|
|
def __init__(self):
|
|
db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs(
|
|
attributes.PORTS, [_extend_port_trunk_details])
|
|
self._rpc_backend = None
|
|
self._drivers = []
|
|
self._segmentation_types = {}
|
|
self._interfaces = set()
|
|
self._agent_types = set()
|
|
drivers.register()
|
|
registry.subscribe(rules.enforce_port_deletion_rules,
|
|
resources.PORT, events.BEFORE_DELETE)
|
|
registry.notify(constants.TRUNK_PLUGIN, events.AFTER_INIT, self)
|
|
for driver in self._drivers:
|
|
LOG.debug('Trunk plugin loaded with driver %s', driver.name)
|
|
self.check_compatibility()
|
|
|
|
def check_compatibility(self):
|
|
"""Fail to load if no compatible driver is found."""
|
|
if not any([driver.is_loaded for driver in self._drivers]):
|
|
raise trunk_exc.IncompatibleTrunkPluginConfiguration()
|
|
|
|
def set_rpc_backend(self, backend):
|
|
self._rpc_backend = backend
|
|
|
|
def is_rpc_enabled(self):
|
|
return self._rpc_backend is not None
|
|
|
|
def register_driver(self, driver):
|
|
"""Register driver with trunk plugin."""
|
|
if driver.agent_type:
|
|
self._agent_types.add(driver.agent_type)
|
|
self._interfaces = self._interfaces | set(driver.interfaces)
|
|
self._drivers.append(driver)
|
|
|
|
@property
|
|
def registered_drivers(self):
|
|
"""The registered drivers."""
|
|
return self._drivers
|
|
|
|
@property
|
|
def supported_interfaces(self):
|
|
"""A set of supported interfaces."""
|
|
return self._interfaces
|
|
|
|
@property
|
|
def supported_agent_types(self):
|
|
"""A set of supported agent types."""
|
|
return self._agent_types
|
|
|
|
def add_segmentation_type(self, segmentation_type, id_validator):
|
|
self._segmentation_types[segmentation_type] = id_validator
|
|
LOG.debug('Added support for segmentation type %s', segmentation_type)
|
|
|
|
def validate(self, context, trunk):
|
|
"""Return a valid trunk or raises an error if unable to do so."""
|
|
trunk_details = trunk
|
|
|
|
trunk_validator = rules.TrunkPortValidator(trunk['port_id'])
|
|
trunk_details['port_id'] = trunk_validator.validate(context)
|
|
|
|
subports_validator = rules.SubPortsValidator(
|
|
self._segmentation_types, trunk['sub_ports'], trunk['port_id'])
|
|
trunk_details['sub_ports'] = subports_validator.validate(context)
|
|
return trunk_details
|
|
|
|
def get_plugin_description(self):
|
|
return "Trunk port service plugin"
|
|
|
|
@classmethod
|
|
def get_plugin_type(cls):
|
|
return "trunk"
|
|
|
|
@db_base_plugin_common.filter_fields
|
|
@db_base_plugin_common.convert_result_to_dict
|
|
def get_trunk(self, context, trunk_id, fields=None):
|
|
"""Return information for the specified trunk."""
|
|
return self._get_trunk(context, trunk_id)
|
|
|
|
@db_base_plugin_common.filter_fields
|
|
@db_base_plugin_common.convert_result_to_dict
|
|
def get_trunks(self, context, filters=None, fields=None,
|
|
sorts=None, limit=None, marker=None, page_reverse=False):
|
|
"""Return information for available trunks."""
|
|
filters = filters or {}
|
|
pager = objects_base.Pager(sorts=sorts, limit=limit,
|
|
page_reverse=page_reverse, marker=marker)
|
|
return trunk_objects.Trunk.get_objects(context, _pager=pager,
|
|
**filters)
|
|
|
|
@db_base_plugin_common.convert_result_to_dict
|
|
def create_trunk(self, context, trunk):
|
|
"""Create a trunk."""
|
|
trunk = self.validate(context, trunk['trunk'])
|
|
sub_ports = [trunk_objects.SubPort(
|
|
context=context,
|
|
port_id=p['port_id'],
|
|
segmentation_id=p['segmentation_id'],
|
|
segmentation_type=p['segmentation_type'])
|
|
for p in trunk['sub_ports']]
|
|
admin_state_up = trunk.get('admin_state_up', True)
|
|
# NOTE(status_police): a trunk is created in PENDING status. Depending
|
|
# on the nature of the create request, a driver may set the status
|
|
# immediately to ACTIVE if no physical provisioning is required.
|
|
# Otherwise a transition to BUILD (or ERROR) should be expected
|
|
# depending on how the driver reacts. PRECOMMIT failures prevent the
|
|
# trunk from being created altogether.
|
|
trunk_obj = trunk_objects.Trunk(context=context,
|
|
admin_state_up=admin_state_up,
|
|
id=uuidutils.generate_uuid(),
|
|
name=trunk.get('name', ""),
|
|
tenant_id=trunk['tenant_id'],
|
|
port_id=trunk['port_id'],
|
|
status=constants.PENDING_STATUS,
|
|
sub_ports=sub_ports)
|
|
with db_api.autonested_transaction(context.session):
|
|
trunk_obj.create()
|
|
payload = callbacks.TrunkPayload(context, trunk_obj.id,
|
|
current_trunk=trunk_obj)
|
|
registry.notify(
|
|
constants.TRUNK, events.PRECOMMIT_CREATE, self,
|
|
payload=payload)
|
|
registry.notify(
|
|
constants.TRUNK, events.AFTER_CREATE, self, payload=payload)
|
|
return trunk_obj
|
|
|
|
@db_base_plugin_common.convert_result_to_dict
|
|
def update_trunk(self, context, trunk_id, trunk):
|
|
"""Update information for the specified trunk."""
|
|
trunk_data = trunk['trunk']
|
|
with db_api.autonested_transaction(context.session):
|
|
trunk_obj = self._get_trunk(context, trunk_id)
|
|
original_trunk = copy.deepcopy(trunk_obj)
|
|
# NOTE(status_police): a trunk status should not change during an
|
|
# update_trunk(), even in face of PRECOMMIT failures. This is
|
|
# because only name and admin_state_up are being affected, and
|
|
# these are DB properties only.
|
|
trunk_obj.update_fields(trunk_data, reset_changes=True)
|
|
trunk_obj.update()
|
|
payload = callbacks.TrunkPayload(context, trunk_id,
|
|
original_trunk=original_trunk,
|
|
current_trunk=trunk_obj)
|
|
registry.notify(constants.TRUNK, events.PRECOMMIT_UPDATE, self,
|
|
payload=payload)
|
|
registry.notify(constants.TRUNK, events.AFTER_UPDATE, self,
|
|
payload=payload)
|
|
return trunk_obj
|
|
|
|
def delete_trunk(self, context, trunk_id):
|
|
"""Delete the specified trunk."""
|
|
with db_api.autonested_transaction(context.session):
|
|
trunk = self._get_trunk(context, trunk_id)
|
|
rules.trunk_can_be_managed(context, trunk)
|
|
trunk_port_validator = rules.TrunkPortValidator(trunk.port_id)
|
|
if not trunk_port_validator.is_bound(context):
|
|
# NOTE(status_police): when a trunk is deleted, the logical
|
|
# object disappears from the datastore, therefore there is no
|
|
# status transition involved. If PRECOMMIT failures occur,
|
|
# the trunk remains in the status where it was.
|
|
trunk.delete()
|
|
payload = callbacks.TrunkPayload(context, trunk_id,
|
|
original_trunk=trunk)
|
|
registry.notify(constants.TRUNK, events.PRECOMMIT_DELETE, self,
|
|
payload=payload)
|
|
else:
|
|
raise trunk_exc.TrunkInUse(trunk_id=trunk_id)
|
|
registry.notify(constants.TRUNK, events.AFTER_DELETE, self,
|
|
payload=payload)
|
|
|
|
@db_base_plugin_common.convert_result_to_dict
|
|
def add_subports(self, context, trunk_id, subports):
|
|
"""Add one or more subports to trunk."""
|
|
# Check for basic validation since the request body here is not
|
|
# automatically validated by the API layer.
|
|
subports = subports['sub_ports']
|
|
subports_validator = rules.SubPortsValidator(
|
|
self._segmentation_types, subports)
|
|
subports = subports_validator.validate(context, basic_validation=True)
|
|
added_subports = []
|
|
|
|
with db_api.autonested_transaction(context.session):
|
|
trunk = self._get_trunk(context, trunk_id)
|
|
rules.trunk_can_be_managed(context, trunk)
|
|
original_trunk = copy.deepcopy(trunk)
|
|
# NOTE(status_police): the trunk status should transition to
|
|
# PENDING (and consequently to BUILD and finally in ACTIVE
|
|
# or ERROR), only if it is not in ERROR status already. A user
|
|
# should attempt to resolve the ERROR condition before adding
|
|
# more subports to the trunk. Should a trunk be in PENDING or
|
|
# BUILD state (e.g. when dealing with multiple concurrent
|
|
# requests), the status is still forced to PENDING and thus
|
|
# can potentially overwrite an interleaving state change to
|
|
# ACTIVE. Eventually the driver should bring the status back
|
|
# to ACTIVE or ERROR.
|
|
if trunk.status == constants.ERROR_STATUS:
|
|
raise trunk_exc.TrunkInErrorState(trunk_id=trunk_id)
|
|
else:
|
|
trunk.status = constants.PENDING_STATUS
|
|
trunk.update()
|
|
|
|
for subport in subports:
|
|
obj = trunk_objects.SubPort(
|
|
context=context,
|
|
trunk_id=trunk_id,
|
|
port_id=subport['port_id'],
|
|
segmentation_type=subport['segmentation_type'],
|
|
segmentation_id=subport['segmentation_id'])
|
|
obj.create()
|
|
trunk['sub_ports'].append(obj)
|
|
added_subports.append(obj)
|
|
payload = callbacks.TrunkPayload(context, trunk_id,
|
|
current_trunk=trunk,
|
|
original_trunk=original_trunk,
|
|
subports=added_subports)
|
|
if added_subports:
|
|
registry.notify(constants.SUBPORTS, events.PRECOMMIT_CREATE,
|
|
self, payload=payload)
|
|
if added_subports:
|
|
registry.notify(
|
|
constants.SUBPORTS, events.AFTER_CREATE, self, payload=payload)
|
|
return trunk
|
|
|
|
@db_base_plugin_common.convert_result_to_dict
|
|
def remove_subports(self, context, trunk_id, subports):
|
|
"""Remove one or more subports from trunk."""
|
|
subports = subports['sub_ports']
|
|
with db_api.autonested_transaction(context.session):
|
|
trunk = self._get_trunk(context, trunk_id)
|
|
original_trunk = copy.deepcopy(trunk)
|
|
rules.trunk_can_be_managed(context, trunk)
|
|
|
|
subports_validator = rules.SubPortsValidator(
|
|
self._segmentation_types, subports)
|
|
# the subports are being removed, therefore we do not need to
|
|
# enforce any specific trunk rules, other than basic validation
|
|
# of the request body.
|
|
subports = subports_validator.validate(
|
|
context, basic_validation=True,
|
|
trunk_validation=False)
|
|
|
|
current_subports = {p.port_id: p for p in trunk.sub_ports}
|
|
removed_subports = []
|
|
|
|
for subport in subports:
|
|
subport_obj = current_subports.pop(subport['port_id'], None)
|
|
|
|
if not subport_obj:
|
|
raise trunk_exc.SubPortNotFound(trunk_id=trunk_id,
|
|
port_id=subport['port_id'])
|
|
subport_obj.delete()
|
|
removed_subports.append(subport_obj)
|
|
|
|
del trunk.sub_ports[:]
|
|
trunk.sub_ports.extend(current_subports.values())
|
|
# NOTE(status_police): the trunk status should transition to
|
|
# PENDING irrespective of the status in which it is in to allow
|
|
# the user to resolve potential conflicts due to prior add_subports
|
|
# operations.
|
|
# Should a trunk be in PENDING or BUILD state (e.g. when dealing
|
|
# with multiple concurrent requests), the status is still forced
|
|
# to PENDING. See add_subports() for more details.
|
|
trunk.status = constants.PENDING_STATUS
|
|
trunk.update()
|
|
payload = callbacks.TrunkPayload(context, trunk_id,
|
|
current_trunk=trunk,
|
|
original_trunk=original_trunk,
|
|
subports=removed_subports)
|
|
if removed_subports:
|
|
registry.notify(constants.SUBPORTS, events.PRECOMMIT_DELETE,
|
|
self, payload=payload)
|
|
if removed_subports:
|
|
registry.notify(
|
|
constants.SUBPORTS, events.AFTER_DELETE, self, payload=payload)
|
|
return trunk
|
|
|
|
@db_base_plugin_common.filter_fields
|
|
def get_subports(self, context, trunk_id, fields=None):
|
|
"""Return subports for the specified trunk."""
|
|
trunk = self.get_trunk(context, trunk_id)
|
|
return {'sub_ports': trunk['sub_ports']}
|
|
|
|
def _get_trunk(self, context, trunk_id):
|
|
"""Return the trunk object or raise if not found."""
|
|
obj = trunk_objects.Trunk.get_object(context, id=trunk_id)
|
|
if obj is None:
|
|
raise trunk_exc.TrunkNotFound(trunk_id=trunk_id)
|
|
|
|
return obj
|