New resource: Neutron Trunk

Add a new OS::Neutron::Trunk resource and support the creation,
deletion and update of Neutron Trunks.

Co-Authored-By: Bence Romsics <bence.romsics@ericsson.com>
Co-Authored-By: David Toth <david.t.toth@ericsson.com>
Change-Id: Iea12844f77abf8c254f6224d55470663eba66aab
Implements: blueprint support-trunk-port
This commit is contained in:
Norbert Illes 2017-05-30 11:28:07 +02:00 committed by Bence Romsics
parent a201c4b7c5
commit 1f8515ace2
4 changed files with 766 additions and 1 deletions

View File

@ -104,7 +104,7 @@ class NeutronResource(resource.Resource):
return False
if status in ('ACTIVE', 'DOWN'):
return True
elif status == 'ERROR':
elif status in ('ERROR', 'DEGRADED'):
raise exception.ResourceInError(
resource_status=status)
else:

View File

@ -0,0 +1,331 @@
# Copyright 2017 Ericsson
#
# 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.
from oslo_log import log as logging
from heat.common.i18n import _
from heat.engine import attributes
from heat.engine import constraints
from heat.engine import properties
from heat.engine.resources.openstack.neutron import neutron
from heat.engine import support
from heat.engine import translation
LOG = logging.getLogger(__name__)
class Trunk(neutron.NeutronResource):
"""A resource for managing Neutron trunks.
Requires Neutron Trunk Extension to be enabled::
$ openstack extension show trunk
The network trunk service allows multiple networks to be connected to
an instance using a single virtual NIC (vNIC). Multiple networks can
be presented to an instance by connecting the instance to a single port.
Users can create a port, associate it with a trunk (as the trunk's parent)
and launch an instance on that port. Users can dynamically attach and
detach additional networks without disrupting operation of the instance.
Every trunk has a parent port and can have any number (0, 1, ...) of
subports. The parent port is the port that the instance is directly
associated with and its traffic is always untagged inside the instance.
Users must specify the parent port of the trunk when launching an
instance attached to a trunk.
A network presented by a subport is the network of the associated port.
When creating a subport, a ``segmentation_type`` and ``segmentation_id``
may be required by the driver so the user can distinguish the networks
inside the instance. As of release Pike only ``segmentation_type``
``vlan`` is supported. ``segmentation_id`` defines the segmentation ID
on which the subport network is presented to the instance.
Note that some Neutron backends (primarily Open vSwitch) only allow
trunk creation before an instance is booted on the parent port. To avoid
a possible race condition when booting an instance with a trunk it is
strongly recommended to refer to the trunk's parent port indirectly in
the template via ``get_attr``. For example::
trunk:
type: OS::Neutron::Trunk
properties:
port: ...
instance:
type: OS::Nova::Server
properties:
networks:
- { port: { get_attr: [trunk, port_id] } }
Though other Neutron backends may tolerate the direct port reference
(and the possible reverse ordering of API requests implied) it's a good
idea to avoid writing Neutron backend specific templates.
"""
entity = 'trunk'
required_service_extension = 'trunk'
support_status = support.SupportStatus(
status=support.SUPPORTED,
version='9.0.0',
)
PROPERTIES = (
NAME, PARENT_PORT, SUB_PORTS, DESCRIPTION, ADMIN_STATE_UP,
) = (
'name', 'port', 'sub_ports', 'description', 'admin_state_up',
)
_SUBPORT_KEYS = (
PORT, SEGMENTATION_TYPE, SEGMENTATION_ID,
) = (
'port', 'segmentation_type', 'segmentation_id',
)
_subport_schema = {
PORT: properties.Schema(
properties.Schema.STRING,
_('ID or name of a port to be used as a subport.'),
required=True,
constraints=[
constraints.CustomConstraint('neutron.port'),
],
),
SEGMENTATION_TYPE: properties.Schema(
properties.Schema.STRING,
_('Segmentation type to be used on the subport.'),
required=True,
# TODO(nilles): custom constraint 'neutron.trunk_segmentation_type'
constraints=[
constraints.AllowedValues(['vlan']),
],
),
SEGMENTATION_ID: properties.Schema(
properties.Schema.INTEGER,
_('The segmentation ID on which the subport network is presented '
'to the instance.'),
required=True,
# TODO(nilles): custom constraint 'neutron.trunk_segmentation_id'
constraints=[
constraints.Range(1, 4094),
],
),
}
ATTRIBUTES = (
PORT_ATTR,
) = (
'port_id',
)
properties_schema = {
NAME: properties.Schema(
properties.Schema.STRING,
_('A string specifying a symbolic name for the trunk, which is '
'not required to be uniqe.'),
update_allowed=True,
),
PARENT_PORT: properties.Schema(
properties.Schema.STRING,
_('ID or name of a port to be used as a parent port.'),
required=True,
immutable=True,
constraints=[
constraints.CustomConstraint('neutron.port'),
],
),
SUB_PORTS: properties.Schema(
properties.Schema.LIST,
_('List with 0 or more map elements containing subport details.'),
schema=properties.Schema(
properties.Schema.MAP,
schema=_subport_schema,
),
update_allowed=True,
),
DESCRIPTION: properties.Schema(
properties.Schema.STRING,
_('Description for the trunk.'),
update_allowed=True,
),
ADMIN_STATE_UP: properties.Schema(
properties.Schema.BOOLEAN,
_('Enable/disable subport addition, removal and trunk delete.'),
update_allowed=True,
),
}
attributes_schema = {
PORT_ATTR: attributes.Schema(
_('ID or name of a port used as a parent port.'),
type=attributes.Schema.STRING,
),
}
def translation_rules(self, props):
return [
translation.TranslationRule(
props,
translation.TranslationRule.RESOLVE,
[self.PARENT_PORT],
client_plugin=self.client_plugin(),
finder='find_resourceid_by_name_or_id',
entity='port',
),
translation.TranslationRule(
props,
translation.TranslationRule.RESOLVE,
translation_path=[self.SUB_PORTS, self.PORT],
client_plugin=self.client_plugin(),
finder='find_resourceid_by_name_or_id',
entity='port',
),
]
def handle_create(self):
props = self.prepare_properties(
self.properties,
self.physical_resource_name())
props['port_id'] = props.pop(self.PARENT_PORT)
if self.SUB_PORTS in props and props[self.SUB_PORTS]:
for sub_port in props[self.SUB_PORTS]:
sub_port['port_id'] = sub_port.pop(self.PORT)
LOG.debug('attempt to create trunk: %s', props)
trunk = self.client().create_trunk({'trunk': props})['trunk']
self.resource_id_set(trunk['id'])
def check_create_complete(self, *args):
attributes = self._show_resource()
return self.is_built(attributes)
def handle_delete(self):
if self.resource_id is not None:
with self.client_plugin().ignore_not_found:
LOG.debug('attempt to delete trunk: %s', self.resource_id)
self.client().delete_trunk(self.resource_id)
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
"""Handle update to a trunk in (at most) three neutron calls.
Call #1) Update all changed properties but 'sub_ports'.
PUT /v2.0/trunks/TRUNK_ID
openstack network trunk set
Call #2) Delete subports not needed anymore.
PUT /v2.0/trunks/TRUNK_ID/remove_subports
openstack network trunk unset --subport
Call #3) Create new subports.
PUT /v2.0/trunks/TRUNK_ID/add_subports
openstack network trunk set --subport
A single neutron port cannot be two subports at the same time (ie.
have two segmentation (type, ID)s on the same trunk or to belong to
two trunks). Therefore we have to delete old subports before creating
new ones to avoid conflicts.
"""
LOG.debug('attempt to update trunk %s', self.resource_id)
# NOTE(bence romsics): We want to do set operations on the subports,
# however we receive subports represented as dicts. In Python
# mutable objects like dicts are not hashable so they cannot be
# inserted into sets. So we convert subport dicts to (immutable)
# frozensets in order to do the set operations.
def dict2frozenset(d):
"""Convert a dict to a frozenset.
Create an immutable equivalent of a dict, so it's hashable
therefore can be used as an element of a set or a key of another
dictionary.
"""
return frozenset(d.items())
# NOTE(bence romsics): prop_diff contains a shallow diff of the
# properties, so if we had used that to update subports we would
# re-create all subports even if just a single subport changed. So we
# need to forget about prop_diff['sub_ports'] and diff out the real
# subport changes from self.properties and json_snippet.
if 'sub_ports' in prop_diff:
del prop_diff['sub_ports']
sub_ports_prop_old = self.properties[self.SUB_PORTS] or []
sub_ports_prop_new = json_snippet.properties(
self.properties_schema)[self.SUB_PORTS] or []
subports_old = {dict2frozenset(d): d for d in sub_ports_prop_old}
subports_new = {dict2frozenset(d): d for d in sub_ports_prop_new}
old_set = set(subports_old.keys())
new_set = set(subports_new.keys())
delete = old_set - new_set
create = new_set - old_set
dicts_delete = [subports_old[fs] for fs in delete]
dicts_create = [subports_new[fs] for fs in create]
LOG.debug('attempt to delete subports of trunk %s: %s',
self.resource_id, dicts_delete)
LOG.debug('attempt to create subports of trunk %s: %s',
self.resource_id, dicts_create)
if prop_diff:
self.prepare_update_properties(prop_diff)
self.client().update_trunk(self.resource_id, {'trunk': prop_diff})
if dicts_delete:
delete_body = self.prepare_trunk_remove_subports_body(dicts_delete)
self.client().trunk_remove_subports(self.resource_id, delete_body)
if dicts_create:
create_body = self.prepare_trunk_add_subports_body(dicts_create)
self.client().trunk_add_subports(self.resource_id, create_body)
def check_update_complete(self, *args):
attributes = self._show_resource()
return self.is_built(attributes)
@staticmethod
def prepare_trunk_remove_subports_body(subports):
"""Prepares body for PUT /v2.0/trunks/TRUNK_ID/remove_subports."""
return {
'sub_ports': [
{'port_id': sp['port']} for sp in subports
]
}
@staticmethod
def prepare_trunk_add_subports_body(subports):
"""Prepares body for PUT /v2.0/trunks/TRUNK_ID/add_subports."""
return {
'sub_ports': [
{'port_id': sp['port'],
'segmentation_type': sp['segmentation_type'],
'segmentation_id': sp['segmentation_id']}
for sp in subports
]
}
def resource_mapping():
return {
'OS::Neutron::Trunk': Trunk,
}

View File

@ -0,0 +1,431 @@
# Copyright 2017 Ericsson
#
# 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
import six
from oslo_log import log as logging
from heat.common import exception
from heat.common import template_format
from heat.engine.clients.os import neutron
from heat.engine.resources.openstack.neutron import trunk
from heat.engine import scheduler
from heat.tests import common
from heat.tests import utils
from neutronclient.common import exceptions as ncex
from neutronclient.neutron import v2_0 as neutronV20
from neutronclient.v2_0 import client as neutronclient
LOG = logging.getLogger(__name__)
create_template = '''
heat_template_version: 2017-09-01
description: Template to test Neutron Trunk resource
resources:
parent_port:
type: OS::Neutron::Port
properties:
network: parent_port_net
subport_1:
type: OS::Neutron::Port
properties:
network: subport_1_net
subport_2:
type: OS::Neutron::Port
properties:
network: subport_2_net
trunk:
type: OS::Neutron::Trunk
properties:
name: trunk name
description: trunk description
port: { get_resource: parent_port }
sub_ports:
- { port: { get_resource: subport_1 },
segmentation_type: vlan,
segmentation_id: 101 }
- { port: { get_resource: subport_2 },
segmentation_type: vlan,
segmentation_id: 102 }
'''
update_template = '''
heat_template_version: 2017-09-01
description: Template to test Neutron Trunk resource
resources:
trunk:
type: OS::Neutron::Trunk
properties:
name: trunk name
description: trunk description
port: parent_port_id
sub_ports:
- { port: subport_1_id,
segmentation_type: vlan,
segmentation_id: 101 }
- { port: subport_2_id,
segmentation_type: vlan,
segmentation_id: 102 }
'''
class NeutronTrunkTest(common.HeatTestCase):
def setUp(self):
super(NeutronTrunkTest, self).setUp()
self.patchobject(
neutron.NeutronClientPlugin, 'has_extension', return_value=True)
self.create_trunk_mock = self.patchobject(
neutronclient.Client, 'create_trunk')
self.delete_trunk_mock = self.patchobject(
neutronclient.Client, 'delete_trunk')
self.show_trunk_mock = self.patchobject(
neutronclient.Client, 'show_trunk')
self.update_trunk_mock = self.patchobject(
neutronclient.Client, 'update_trunk')
self.trunk_remove_subports_mock = self.patchobject(
neutronclient.Client, 'trunk_remove_subports')
self.trunk_add_subports_mock = self.patchobject(
neutronclient.Client, 'trunk_add_subports')
self.find_resource_mock = self.patchobject(
neutronV20, 'find_resourceid_by_name_or_id')
rv = {
'trunk': {
'id': 'trunk id',
'status': 'DOWN',
}
}
self.create_trunk_mock.return_value = rv
self.show_trunk_mock.return_value = rv
def find_resourceid_by_name_or_id(
_client, _resource, name_or_id, **_kwargs):
return name_or_id
self.find_resource_mock.side_effect = find_resourceid_by_name_or_id
def _create_trunk(self, stack):
trunk = stack['trunk']
scheduler.TaskRunner(trunk.create)()
self.assertEqual((trunk.CREATE, trunk.COMPLETE), trunk.state)
def _delete_trunk(self, stack):
trunk = stack['trunk']
scheduler.TaskRunner(trunk.delete)()
self.assertEqual((trunk.DELETE, trunk.COMPLETE), trunk.state)
def test_create_missing_port_property(self):
t = template_format.parse(create_template)
del t['resources']['trunk']['properties']['port']
stack = utils.parse_stack(t)
self.assertRaises(
exception.StackValidationFailed,
stack.validate)
def test_create_no_subport(self):
t = template_format.parse(create_template)
del t['resources']['trunk']['properties']['sub_ports']
del t['resources']['subport_1']
del t['resources']['subport_2']
stack = utils.parse_stack(t)
self.patchobject(
stack['parent_port'], 'FnGetRefId', return_value='parent port id')
self.find_resource_mock.return_value = 'parent port id'
self._create_trunk(stack)
self.create_trunk_mock.assert_called_once_with({
'trunk': {
'description': 'trunk description',
'name': 'trunk name',
'port_id': 'parent port id',
}}
)
def test_create_one_subport(self):
t = template_format.parse(create_template)
del t['resources']['trunk']['properties']['sub_ports'][1:]
del t['resources']['subport_2']
stack = utils.parse_stack(t)
self.patchobject(
stack['parent_port'], 'FnGetRefId', return_value='parent port id')
self.patchobject(
stack['subport_1'], 'FnGetRefId', return_value='subport id')
self._create_trunk(stack)
self.create_trunk_mock.assert_called_once_with({
'trunk': {
'description': 'trunk description',
'name': 'trunk name',
'port_id': 'parent port id',
'sub_ports': [
{'port_id': 'subport id',
'segmentation_type': 'vlan',
'segmentation_id': 101},
],
}}
)
def test_create_two_subports(self):
t = template_format.parse(create_template)
del t['resources']['trunk']['properties']['sub_ports'][2:]
stack = utils.parse_stack(t)
self.patchobject(
stack['parent_port'], 'FnGetRefId', return_value='parent_port_id')
self.patchobject(
stack['subport_1'], 'FnGetRefId', return_value='subport_1_id')
self.patchobject(
stack['subport_2'], 'FnGetRefId', return_value='subport_2_id')
self._create_trunk(stack)
self.create_trunk_mock.assert_called_once_with({
'trunk': {
'description': 'trunk description',
'name': 'trunk name',
'port_id': 'parent_port_id',
'sub_ports': [
{'port_id': 'subport_1_id',
'segmentation_type': 'vlan',
'segmentation_id': 101},
{'port_id': 'subport_2_id',
'segmentation_type': 'vlan',
'segmentation_id': 102},
],
}}
)
def test_create_degraded(self):
t = template_format.parse(create_template)
stack = utils.parse_stack(t)
rv = {
'trunk': {
'id': 'trunk id',
'status': 'DEGRADED',
}
}
self.create_trunk_mock.return_value = rv
self.show_trunk_mock.return_value = rv
trunk = stack['trunk']
e = self.assertRaises(
exception.ResourceInError,
trunk.check_create_complete,
trunk.resource_id)
self.assertIn(
'Went to status DEGRADED due to',
six.text_type(e))
def test_create_parent_port_by_name(self):
t = template_format.parse(create_template)
t['resources']['parent_port'][
'properties']['name'] = 'parent port name'
t['resources']['trunk'][
'properties']['port'] = 'parent port name'
del t['resources']['trunk']['properties']['sub_ports']
stack = utils.parse_stack(t)
self.patchobject(
stack['parent_port'], 'FnGetRefId', return_value='parent port id')
def find_resourceid_by_name_or_id(
_client, _resource, name_or_id, **_kwargs):
name_to_id = {
'parent port name': 'parent port id',
'parent port id': 'parent port id',
}
return name_to_id[name_or_id]
self.find_resource_mock.side_effect = find_resourceid_by_name_or_id
self._create_trunk(stack)
self.create_trunk_mock.assert_called_once_with({
'trunk': {
'description': 'trunk description',
'name': 'trunk name',
'port_id': 'parent port id',
}}
)
def test_create_subport_by_name(self):
t = template_format.parse(create_template)
del t['resources']['trunk']['properties']['sub_ports'][1:]
del t['resources']['subport_2']
t['resources']['subport_1'][
'properties']['name'] = 'subport name'
t['resources']['trunk'][
'properties']['sub_ports'][0]['port'] = 'subport name'
stack = utils.parse_stack(t)
self.patchobject(
stack['parent_port'], 'FnGetRefId', return_value='parent port id')
self.patchobject(
stack['subport_1'], 'FnGetRefId', return_value='subport id')
def find_resourceid_by_name_or_id(
_client, _resource, name_or_id, **_kwargs):
name_to_id = {
'subport name': 'subport id',
'subport id': 'subport id',
'parent port name': 'parent port id',
'parent port id': 'parent port id',
}
return name_to_id[name_or_id]
self.find_resource_mock.side_effect = find_resourceid_by_name_or_id
self._create_trunk(stack)
self.create_trunk_mock.assert_called_once_with({
'trunk': {
'description': 'trunk description',
'name': 'trunk name',
'port_id': 'parent port id',
'sub_ports': [
{'port_id': 'subport id',
'segmentation_type': 'vlan',
'segmentation_id': 101},
],
}}
)
def test_delete_proper(self):
t = template_format.parse(create_template)
stack = utils.parse_stack(t)
self._create_trunk(stack)
self._delete_trunk(stack)
self.delete_trunk_mock.assert_called_once_with('trunk id')
def test_delete_already_gone(self):
t = template_format.parse(create_template)
stack = utils.parse_stack(t)
self._create_trunk(stack)
self.delete_trunk_mock.side_effect = ncex.NeutronClientException(
status_code=404)
self._delete_trunk(stack)
self.delete_trunk_mock.assert_called_once_with('trunk id')
def test_update_basic_properties(self):
t = template_format.parse(update_template)
stack = utils.parse_stack(t)
rsrc_defn = stack.t.resource_definitions(stack)['trunk']
rsrc = trunk.Trunk('trunk', rsrc_defn, stack)
scheduler.TaskRunner(rsrc.create)()
self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state)
props = copy.deepcopy(t['resources']['trunk']['properties'])
props['name'] = 'new trunk name'
rsrc_defn = rsrc_defn.freeze(properties=props)
scheduler.TaskRunner(rsrc.update, rsrc_defn)()
self.assertEqual((rsrc.UPDATE, rsrc.COMPLETE), rsrc.state)
self.update_trunk_mock.assert_called_once_with(
'trunk id', {'trunk': {'name': 'new trunk name'}}
)
self.trunk_remove_subports_mock.assert_not_called()
self.trunk_add_subports_mock.assert_not_called()
def test_update_subport_delete(self):
t = template_format.parse(update_template)
stack = utils.parse_stack(t)
rsrc_defn = stack.t.resource_definitions(stack)['trunk']
rsrc = trunk.Trunk('trunk', rsrc_defn, stack)
scheduler.TaskRunner(rsrc.create)()
self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state)
props = copy.deepcopy(t['resources']['trunk']['properties'])
del props['sub_ports'][1]
rsrc_defn = rsrc_defn.freeze(properties=props)
scheduler.TaskRunner(rsrc.update, rsrc_defn)()
self.assertEqual((rsrc.UPDATE, rsrc.COMPLETE), rsrc.state)
self.update_trunk_mock.assert_not_called()
self.trunk_remove_subports_mock.assert_called_once_with(
'trunk id', {'sub_ports': [{'port_id': u'subport_2_id'}]}
)
self.trunk_add_subports_mock.assert_not_called()
def test_update_subport_add(self):
t = template_format.parse(update_template)
stack = utils.parse_stack(t)
rsrc_defn = stack.t.resource_definitions(stack)['trunk']
rsrc = trunk.Trunk('trunk', rsrc_defn, stack)
scheduler.TaskRunner(rsrc.create)()
self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state)
props = copy.deepcopy(t['resources']['trunk']['properties'])
props['sub_ports'].append(
{'port': 'subport_3_id',
'segmentation_type': 'vlan',
'segmentation_id': 103})
rsrc_defn = rsrc_defn.freeze(properties=props)
scheduler.TaskRunner(rsrc.update, rsrc_defn)()
self.assertEqual((rsrc.UPDATE, rsrc.COMPLETE), rsrc.state)
self.update_trunk_mock.assert_not_called()
self.trunk_remove_subports_mock.assert_not_called()
self.trunk_add_subports_mock.assert_called_once_with(
'trunk id',
{'sub_ports': [
{'port_id': 'subport_3_id',
'segmentation_id': 103,
'segmentation_type': 'vlan'}
]}
)
def test_update_subport_change(self):
t = template_format.parse(update_template)
stack = utils.parse_stack(t)
rsrc_defn = stack.t.resource_definitions(stack)['trunk']
rsrc = trunk.Trunk('trunk', rsrc_defn, stack)
scheduler.TaskRunner(rsrc.create)()
self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state)
props = copy.deepcopy(t['resources']['trunk']['properties'])
props['sub_ports'][1]['segmentation_id'] = 103
rsrc_defn = rsrc_defn.freeze(properties=props)
scheduler.TaskRunner(rsrc.update, rsrc_defn)()
self.assertEqual((rsrc.UPDATE, rsrc.COMPLETE), rsrc.state)
self.update_trunk_mock.assert_not_called()
self.trunk_remove_subports_mock.assert_called_once_with(
'trunk id', {'sub_ports': [{'port_id': u'subport_2_id'}]}
)
self.trunk_add_subports_mock.assert_called_once_with(
'trunk id',
{'sub_ports': [
{'port_id': 'subport_2_id',
'segmentation_id': 103,
'segmentation_type': 'vlan'}
]}
)

View File

@ -0,0 +1,3 @@
---
features:
- New resource ``OS::Neutron::Trunk`` is added to manage Neutron Trunks.