Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
Cloud:OpenStack:Newton
openstack-heat-doc
0001-Add-Neutron-Trunk-Resource.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File 0001-Add-Neutron-Trunk-Resource.patch of Package openstack-heat-doc
commit d9f5d8a1cdfee28da4c2b3e031f50110216fd97c Author: Norbert Illes <norbert.e.illes@ericsson.com> Date: Tue May 30 11:28:07 2017 +0200 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 (cherry picked from commit 1f8515ace2b0a8d58f4e02ef6d58fb0947661cea) diff --git a/heat/engine/resources/openstack/neutron/neutron.py b/heat/engine/resources/openstack/neutron/neutron.py index 556295793..455a0a9c5 100644 --- a/heat/engine/resources/openstack/neutron/neutron.py +++ b/heat/engine/resources/openstack/neutron/neutron.py @@ -86,7 +86,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: diff --git a/heat/engine/resources/openstack/neutron/trunk.py b/heat/engine/resources/openstack/neutron/trunk.py new file mode 100644 index 000000000..90ffd9766 --- /dev/null +++ b/heat/engine/resources/openstack/neutron/trunk.py @@ -0,0 +1,335 @@ +# 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 _show_resource(self): + return self.client().show_trunk( + self.resource_id)['trunk'] + + 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, + } diff --git a/heat/tests/openstack/neutron/test_neutron_trunk.py b/heat/tests/openstack/neutron/test_neutron_trunk.py new file mode 100644 index 000000000..ea878d63f --- /dev/null +++ b/heat/tests/openstack/neutron/test_neutron_trunk.py @@ -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'} + ]} + ) diff --git a/releasenotes/notes/bp-support-trunk-port-733019c49a429826.yaml b/releasenotes/notes/bp-support-trunk-port-733019c49a429826.yaml new file mode 100644 index 000000000..a308b2c75 --- /dev/null +++ b/releasenotes/notes/bp-support-trunk-port-733019c49a429826.yaml @@ -0,0 +1,3 @@ +--- +features: + - New resource ``OS::Neutron::Trunk`` is added to manage Neutron Trunks.
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor