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:
parent
a201c4b7c5
commit
1f8515ace2
@ -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:
|
||||
|
331
heat/engine/resources/openstack/neutron/trunk.py
Normal file
331
heat/engine/resources/openstack/neutron/trunk.py
Normal 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,
|
||||
}
|
431
heat/tests/openstack/neutron/test_neutron_trunk.py
Normal file
431
heat/tests/openstack/neutron/test_neutron_trunk.py
Normal 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'}
|
||||
]}
|
||||
)
|
@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- New resource ``OS::Neutron::Trunk`` is added to manage Neutron Trunks.
|
Loading…
x
Reference in New Issue
Block a user