OpenStack Networking (Neutron)
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.

434 lines
20KB

  1. # All Rights Reserved.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  4. # not use this file except in compliance with the License. You may obtain
  5. # a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  11. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  12. # License for the specific language governing permissions and limitations
  13. # under the License.
  14. import copy
  15. from neutron_lib.api.definitions import port as port_def
  16. from neutron_lib.api.definitions import portbindings
  17. from neutron_lib.callbacks import events
  18. from neutron_lib.callbacks import registry
  19. from neutron_lib.callbacks import resources
  20. from neutron_lib import context
  21. from neutron_lib.db import api as db_api
  22. from neutron_lib.db import resource_extend
  23. from neutron_lib.plugins import directory
  24. from neutron_lib.services import base as service_base
  25. from oslo_log import log as logging
  26. from oslo_utils import excutils
  27. from oslo_utils import uuidutils
  28. from neutron.db import common_db_mixin
  29. from neutron.db import db_base_plugin_common
  30. from neutron.objects import base as objects_base
  31. from neutron.objects import trunk as trunk_objects
  32. from neutron.services.trunk import callbacks
  33. from neutron.services.trunk import constants
  34. from neutron.services.trunk import drivers
  35. from neutron.services.trunk import exceptions as trunk_exc
  36. from neutron.services.trunk import rules
  37. from neutron.services.trunk.seg_types import validators
  38. LOG = logging.getLogger(__name__)
  39. @resource_extend.has_resource_extenders
  40. @registry.has_registry_receivers
  41. class TrunkPlugin(service_base.ServicePluginBase,
  42. common_db_mixin.CommonDbMixin):
  43. supported_extension_aliases = ["trunk", "trunk-details"]
  44. __native_pagination_support = True
  45. __native_sorting_support = True
  46. __filter_validation_support = True
  47. def __init__(self):
  48. self._rpc_backend = None
  49. self._drivers = []
  50. self._segmentation_types = {}
  51. self._interfaces = set()
  52. self._agent_types = set()
  53. drivers.register()
  54. registry.subscribe(rules.enforce_port_deletion_rules,
  55. resources.PORT, events.BEFORE_DELETE)
  56. registry.publish(constants.TRUNK_PLUGIN, events.AFTER_INIT, self)
  57. for driver in self._drivers:
  58. LOG.debug('Trunk plugin loaded with driver %s', driver.name)
  59. self.check_compatibility()
  60. @staticmethod
  61. @resource_extend.extends([port_def.COLLECTION_NAME])
  62. def _extend_port_trunk_details(port_res, port_db):
  63. """Add trunk details to a port."""
  64. if port_db.trunk_port:
  65. subports = {
  66. x.port_id: {'segmentation_id': x.segmentation_id,
  67. 'segmentation_type': x.segmentation_type,
  68. 'port_id': x.port_id}
  69. for x in port_db.trunk_port.sub_ports
  70. }
  71. core_plugin = directory.get_plugin()
  72. ports = core_plugin.get_ports(
  73. context.get_admin_context(), filters={'id': subports})
  74. for port in ports:
  75. subports[port['id']]['mac_address'] = port['mac_address']
  76. trunk_details = {'trunk_id': port_db.trunk_port.id,
  77. 'sub_ports': [x for x in subports.values()]}
  78. port_res['trunk_details'] = trunk_details
  79. return port_res
  80. def check_compatibility(self):
  81. """Verify the plugin can load correctly and fail otherwise."""
  82. self.check_driver_compatibility()
  83. self.check_segmentation_compatibility()
  84. def check_driver_compatibility(self):
  85. """Fail to load if no compatible driver is found."""
  86. if not any([driver.is_loaded for driver in self._drivers]):
  87. raise trunk_exc.IncompatibleTrunkPluginConfiguration()
  88. def check_segmentation_compatibility(self):
  89. """Fail to load if segmentation type conflicts are found.
  90. In multi-driver deployments each loaded driver must support the same
  91. set of segmentation types consistently.
  92. """
  93. # Get list of segmentation types for the loaded drivers.
  94. list_of_driver_seg_types = [
  95. set(driver.segmentation_types) for driver in self._drivers
  96. if driver.is_loaded
  97. ]
  98. # If not empty, check that there is at least one we can use.
  99. compat_segmentation_types = set()
  100. if list_of_driver_seg_types:
  101. compat_segmentation_types = (
  102. set.intersection(*list_of_driver_seg_types))
  103. if not compat_segmentation_types:
  104. raise trunk_exc.IncompatibleDriverSegmentationTypes()
  105. # If there is at least one, make sure the validator is defined.
  106. try:
  107. for seg_type in compat_segmentation_types:
  108. self.add_segmentation_type(
  109. seg_type, validators.get_validator(seg_type))
  110. except KeyError:
  111. raise trunk_exc.SegmentationTypeValidatorNotFound(
  112. seg_type=seg_type)
  113. def set_rpc_backend(self, backend):
  114. self._rpc_backend = backend
  115. def is_rpc_enabled(self):
  116. return self._rpc_backend is not None
  117. def register_driver(self, driver):
  118. """Register driver with trunk plugin."""
  119. if driver.agent_type:
  120. self._agent_types.add(driver.agent_type)
  121. self._interfaces = self._interfaces | set(driver.interfaces)
  122. self._drivers.append(driver)
  123. @property
  124. def registered_drivers(self):
  125. """The registered drivers."""
  126. return self._drivers
  127. @property
  128. def supported_interfaces(self):
  129. """A set of supported interfaces."""
  130. return self._interfaces
  131. @property
  132. def supported_agent_types(self):
  133. """A set of supported agent types."""
  134. return self._agent_types
  135. def add_segmentation_type(self, segmentation_type, id_validator):
  136. self._segmentation_types[segmentation_type] = id_validator
  137. LOG.debug('Added support for segmentation type %s', segmentation_type)
  138. def validate(self, context, trunk):
  139. """Return a valid trunk or raises an error if unable to do so."""
  140. trunk_details = trunk
  141. trunk_validator = rules.TrunkPortValidator(trunk['port_id'])
  142. trunk_details['port_id'] = trunk_validator.validate(context)
  143. subports_validator = rules.SubPortsValidator(
  144. self._segmentation_types, trunk['sub_ports'], trunk['port_id'])
  145. trunk_details['sub_ports'] = subports_validator.validate(context)
  146. return trunk_details
  147. def get_plugin_description(self):
  148. return "Trunk port service plugin"
  149. @classmethod
  150. def get_plugin_type(cls):
  151. return "trunk"
  152. @db_base_plugin_common.filter_fields
  153. @db_base_plugin_common.convert_result_to_dict
  154. def get_trunk(self, context, trunk_id, fields=None):
  155. """Return information for the specified trunk."""
  156. return self._get_trunk(context, trunk_id)
  157. @db_base_plugin_common.filter_fields
  158. @db_base_plugin_common.convert_result_to_dict
  159. def get_trunks(self, context, filters=None, fields=None,
  160. sorts=None, limit=None, marker=None, page_reverse=False):
  161. """Return information for available trunks."""
  162. filters = filters or {}
  163. pager = objects_base.Pager(sorts=sorts, limit=limit,
  164. page_reverse=page_reverse, marker=marker)
  165. return trunk_objects.Trunk.get_objects(context, _pager=pager,
  166. **filters)
  167. @db_base_plugin_common.convert_result_to_dict
  168. def create_trunk(self, context, trunk):
  169. """Create a trunk."""
  170. trunk = self.validate(context, trunk['trunk'])
  171. sub_ports = [trunk_objects.SubPort(
  172. context=context,
  173. port_id=p['port_id'],
  174. segmentation_id=p['segmentation_id'],
  175. segmentation_type=p['segmentation_type'])
  176. for p in trunk['sub_ports']]
  177. admin_state_up = trunk.get('admin_state_up', True)
  178. # NOTE(status_police): a trunk is created in DOWN status. Depending
  179. # on the nature of the create request, a driver may set the status
  180. # immediately to ACTIVE if no physical provisioning is required.
  181. # Otherwise a transition to BUILD (or ERROR) should be expected
  182. # depending on how the driver reacts. PRECOMMIT failures prevent the
  183. # trunk from being created altogether.
  184. trunk_description = trunk.get('description', "")
  185. trunk_obj = trunk_objects.Trunk(context=context,
  186. admin_state_up=admin_state_up,
  187. id=uuidutils.generate_uuid(),
  188. name=trunk.get('name', ""),
  189. description=trunk_description,
  190. project_id=trunk['tenant_id'],
  191. port_id=trunk['port_id'],
  192. status=constants.DOWN_STATUS,
  193. sub_ports=sub_ports)
  194. with db_api.autonested_transaction(context.session):
  195. trunk_obj.create()
  196. payload = callbacks.TrunkPayload(context, trunk_obj.id,
  197. current_trunk=trunk_obj)
  198. registry.notify(
  199. constants.TRUNK, events.PRECOMMIT_CREATE, self,
  200. payload=payload)
  201. registry.notify(
  202. constants.TRUNK, events.AFTER_CREATE, self, payload=payload)
  203. return trunk_obj
  204. @db_base_plugin_common.convert_result_to_dict
  205. def update_trunk(self, context, trunk_id, trunk):
  206. """Update information for the specified trunk."""
  207. trunk_data = trunk['trunk']
  208. with db_api.autonested_transaction(context.session):
  209. trunk_obj = self._get_trunk(context, trunk_id)
  210. original_trunk = copy.deepcopy(trunk_obj)
  211. # NOTE(status_police): a trunk status should not change during an
  212. # update_trunk(), even in face of PRECOMMIT failures. This is
  213. # because only name and admin_state_up are being affected, and
  214. # these are DB properties only.
  215. trunk_obj.update_fields(trunk_data, reset_changes=True)
  216. trunk_obj.update()
  217. payload = events.DBEventPayload(
  218. context, resource_id=trunk_id, states=(original_trunk,),
  219. desired_state=trunk_obj, request_body=trunk_data)
  220. registry.publish(constants.TRUNK, events.PRECOMMIT_UPDATE, self,
  221. payload=payload)
  222. registry.notify(constants.TRUNK, events.AFTER_UPDATE, self,
  223. payload=callbacks.TrunkPayload(
  224. context, trunk_id,
  225. original_trunk=original_trunk,
  226. current_trunk=trunk_obj))
  227. return trunk_obj
  228. def delete_trunk(self, context, trunk_id):
  229. """Delete the specified trunk."""
  230. with db_api.autonested_transaction(context.session):
  231. trunk = self._get_trunk(context, trunk_id)
  232. rules.trunk_can_be_managed(context, trunk)
  233. trunk_port_validator = rules.TrunkPortValidator(trunk.port_id)
  234. if trunk_port_validator.can_be_trunked_or_untrunked(context):
  235. # NOTE(status_police): when a trunk is deleted, the logical
  236. # object disappears from the datastore, therefore there is no
  237. # status transition involved. If PRECOMMIT failures occur,
  238. # the trunk remains in the status where it was.
  239. try:
  240. trunk.delete()
  241. except Exception as e:
  242. with excutils.save_and_reraise_exception():
  243. LOG.warning('Trunk driver raised exception when '
  244. 'deleting trunk port %s: %s', trunk_id,
  245. str(e))
  246. payload = callbacks.TrunkPayload(context, trunk_id,
  247. original_trunk=trunk)
  248. registry.notify(resources.TRUNK,
  249. events.PRECOMMIT_DELETE,
  250. self, payload=payload)
  251. else:
  252. LOG.info('Trunk driver does not consider trunk %s '
  253. 'untrunkable', trunk_id)
  254. raise trunk_exc.TrunkInUse(trunk_id=trunk_id)
  255. registry.notify(constants.TRUNK, events.AFTER_DELETE, self,
  256. payload=payload)
  257. @db_base_plugin_common.convert_result_to_dict
  258. def add_subports(self, context, trunk_id, subports):
  259. """Add one or more subports to trunk."""
  260. with db_api.autonested_transaction(context.session):
  261. trunk = self._get_trunk(context, trunk_id)
  262. # Check for basic validation since the request body here is not
  263. # automatically validated by the API layer.
  264. subports = subports['sub_ports']
  265. subports_validator = rules.SubPortsValidator(
  266. self._segmentation_types, subports, trunk['port_id'])
  267. subports = subports_validator.validate(
  268. context, basic_validation=True)
  269. added_subports = []
  270. rules.trunk_can_be_managed(context, trunk)
  271. original_trunk = copy.deepcopy(trunk)
  272. # NOTE(status_police): the trunk status should transition to
  273. # DOWN (and finally in ACTIVE or ERROR), only if it is not in
  274. # ERROR status already. A user should attempt to resolve the ERROR
  275. # condition before adding more subports to the trunk. Should a
  276. # trunk be in DOWN or BUILD state (e.g. when dealing with
  277. # multiple concurrent requests), the status is still forced to
  278. # DOWN and thus can potentially overwrite an interleaving state
  279. # change to ACTIVE. Eventually the driver should bring the status
  280. # back to ACTIVE or ERROR.
  281. if trunk.status == constants.ERROR_STATUS:
  282. raise trunk_exc.TrunkInErrorState(trunk_id=trunk_id)
  283. else:
  284. trunk.update(status=constants.DOWN_STATUS)
  285. for subport in subports:
  286. obj = trunk_objects.SubPort(
  287. context=context,
  288. trunk_id=trunk_id,
  289. port_id=subport['port_id'],
  290. segmentation_type=subport['segmentation_type'],
  291. segmentation_id=subport['segmentation_id'])
  292. obj.create()
  293. trunk['sub_ports'].append(obj)
  294. added_subports.append(obj)
  295. payload = callbacks.TrunkPayload(context, trunk_id,
  296. current_trunk=trunk,
  297. original_trunk=original_trunk,
  298. subports=added_subports)
  299. if added_subports:
  300. registry.notify(constants.SUBPORTS, events.PRECOMMIT_CREATE,
  301. self, payload=payload)
  302. if added_subports:
  303. registry.notify(
  304. constants.SUBPORTS, events.AFTER_CREATE, self, payload=payload)
  305. return trunk
  306. @db_base_plugin_common.convert_result_to_dict
  307. def remove_subports(self, context, trunk_id, subports):
  308. """Remove one or more subports from trunk."""
  309. subports = subports['sub_ports']
  310. with db_api.autonested_transaction(context.session):
  311. trunk = self._get_trunk(context, trunk_id)
  312. original_trunk = copy.deepcopy(trunk)
  313. rules.trunk_can_be_managed(context, trunk)
  314. subports_validator = rules.SubPortsValidator(
  315. self._segmentation_types, subports)
  316. # the subports are being removed, therefore we do not need to
  317. # enforce any specific trunk rules, other than basic validation
  318. # of the request body.
  319. subports = subports_validator.validate(
  320. context, basic_validation=True,
  321. trunk_validation=False)
  322. current_subports = {p.port_id: p for p in trunk.sub_ports}
  323. removed_subports = []
  324. for subport in subports:
  325. subport_obj = current_subports.pop(subport['port_id'], None)
  326. if not subport_obj:
  327. raise trunk_exc.SubPortNotFound(trunk_id=trunk_id,
  328. port_id=subport['port_id'])
  329. subport_obj.delete()
  330. removed_subports.append(subport_obj)
  331. del trunk.sub_ports[:]
  332. trunk.sub_ports.extend(current_subports.values())
  333. # NOTE(status_police): the trunk status should transition to
  334. # DOWN irrespective of the status in which it is in to allow
  335. # the user to resolve potential conflicts due to prior add_subports
  336. # operations.
  337. # Should a trunk be in DOWN or BUILD state (e.g. when dealing
  338. # with multiple concurrent requests), the status is still forced
  339. # to DOWN. See add_subports() for more details.
  340. trunk.update(status=constants.DOWN_STATUS)
  341. payload = callbacks.TrunkPayload(context, trunk_id,
  342. current_trunk=trunk,
  343. original_trunk=original_trunk,
  344. subports=removed_subports)
  345. if removed_subports:
  346. registry.notify(constants.SUBPORTS, events.PRECOMMIT_DELETE,
  347. self, payload=payload)
  348. if removed_subports:
  349. registry.notify(
  350. constants.SUBPORTS, events.AFTER_DELETE, self, payload=payload)
  351. return trunk
  352. @db_base_plugin_common.filter_fields
  353. def get_subports(self, context, trunk_id, fields=None):
  354. """Return subports for the specified trunk."""
  355. trunk = self.get_trunk(context, trunk_id)
  356. return {'sub_ports': trunk['sub_ports']}
  357. def _get_trunk(self, context, trunk_id):
  358. """Return the trunk object or raise if not found."""
  359. obj = trunk_objects.Trunk.get_object(context, id=trunk_id)
  360. if obj is None:
  361. raise trunk_exc.TrunkNotFound(trunk_id=trunk_id)
  362. return obj
  363. # NOTE(tidwellr) Consider keying off of PRECOMMIT_UPDATE if we find
  364. # AFTER_UPDATE to be problematic for setting trunk status when a
  365. # a parent port becomes unbound.
  366. @registry.receives(resources.PORT, [events.AFTER_UPDATE])
  367. def _trigger_trunk_status_change(self, resource, event, trigger, **kwargs):
  368. updated_port = kwargs['port']
  369. trunk_details = updated_port.get('trunk_details')
  370. # If no trunk_details, the port is not the parent of a trunk.
  371. if not trunk_details:
  372. return
  373. context = kwargs['context']
  374. original_port = kwargs['original_port']
  375. orig_vif_type = original_port.get(portbindings.VIF_TYPE)
  376. new_vif_type = updated_port.get(portbindings.VIF_TYPE)
  377. vif_type_changed = orig_vif_type != new_vif_type
  378. if vif_type_changed and new_vif_type == portbindings.VIF_TYPE_UNBOUND:
  379. trunk_id = trunk_details['trunk_id']
  380. # NOTE(status_police) Trunk status goes to DOWN when the parent
  381. # port is unbound. This means there are no more physical resources
  382. # associated with the logical resource.
  383. self.update_trunk(context, trunk_id,
  384. {'trunk': {'status': constants.DOWN_STATUS}})