Browse Source

Support Neutron L2Gateway resource

The Neutron extension for layer2 gateway (networking-l2gw) provides a API to
manage L2GW components. The proposed change is to implement two new Heat
resources to allow management of the L2GW and L2GW-connection resources.
This change implements the first of the two resources,
OS::Neutron::L2Gateway

Change-Id: Ib850f027833106cb39d3d1f6e644bbb1f79f1aac
Task: #19995
Story: #2002150
changes/28/573228/8
Jason Neatherway 3 years ago
parent
commit
0731857d0d
  1. 158
      heat/engine/resources/openstack/neutron/l2_gateway.py
  2. 312
      heat/tests/openstack/neutron/test_neutron_l2_gateway.py
  3. 6
      releasenotes/notes/neutron-l2gw-support-9fbb690bb5648f76.yaml

158
heat/engine/resources/openstack/neutron/l2_gateway.py

@ -0,0 +1,158 @@
# Copyright 2018 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 collections
import six
from heat.common.i18n import _
from heat.engine import properties
from heat.engine.resources.openstack.neutron import neutron
from heat.engine import support
class L2Gateway(neutron.NeutronResource):
"""A resource for managing Neutron L2 Gateways.
The are a number of use cases that can be addressed by an L2 Gateway API.
Most notably in cloud computing environments, a typical use case is
bridging the virtual with the physical. Translate this to Neutron and the
OpenStack world, and this means relying on L2 Gateway capabilities to
extend Neutron logical (overlay) networks into physical (provider)
networks that are outside the OpenStack realm.
"""
required_service_extension = 'l2-gateway'
entity = 'l2_gateway'
support_status = support.SupportStatus(version='12.0.0')
PROPERTIES = (
NAME, DEVICES,
) = (
'name', 'devices',
)
_DEVICE_KEYS = (
DEVICE_NAME, INTERFACES,
) = (
'device_name', 'interfaces',
)
_INTERFACE_KEYS = (
INTERFACE_NAME, SEGMENTATION_ID,
) = (
'name', 'segmentation_id',
)
_interface_schema = {
INTERFACE_NAME: properties.Schema(
properties.Schema.STRING,
_('The name of the interface on the gateway device.'),
required=True
),
SEGMENTATION_ID: properties.Schema(
properties.Schema.LIST,
_('A list of segmentation ids of the interface.')
),
}
_device_schema = {
DEVICE_NAME: properties.Schema(
properties.Schema.STRING,
_('The name of the gateway device.'),
required=True
),
INTERFACES: properties.Schema(
properties.Schema.LIST,
_('List of gateway device interfaces.'),
schema=properties.Schema(
properties.Schema.MAP,
schema=_interface_schema
),
required=True
)
}
properties_schema = {
NAME: properties.Schema(
properties.Schema.STRING,
_('A symbolic name for the l2-gateway, '
'which is not required to be unique.'),
required=True,
update_allowed=True
),
DEVICES: properties.Schema(
properties.Schema.LIST,
_('List of gateway devices.'),
schema=properties.Schema(
properties.Schema.MAP,
schema=_device_schema
),
required=True,
update_allowed=True
),
}
@staticmethod
def _remove_none_value_props(props):
if isinstance(props, collections.Mapping):
return dict((k, L2Gateway._remove_none_value_props(v)) for k, v
in props.items() if v is not None)
elif (isinstance(props, collections.Sequence) and
not isinstance(props, six.string_types)):
return list(L2Gateway._remove_none_value_props(l) for l in props
if l is not None)
return props
@staticmethod
def prepare_properties(properties, name):
# Overrides method from base class NeutronResource to ensure None
# values are removed from all levels of value_specs.
# TODO(neatherweb): move this recursive check for None to
# prepare_properties in NeutronResource
props = L2Gateway._remove_none_value_props(dict(properties))
if 'name' in properties:
props.setdefault('name', name)
return props
def handle_create(self):
props = self.prepare_properties(
self.properties,
self.physical_resource_name())
l2gw = self.client().create_l2_gateway(
{'l2_gateway': props})['l2_gateway']
self.resource_id_set(l2gw['id'])
def handle_delete(self):
if self.resource_id is None:
return
with self.client_plugin().ignore_not_found:
self.client().delete_l2_gateway(self.resource_id)
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
if prop_diff:
self.prepare_update_properties(prop_diff)
prop_diff = L2Gateway._remove_none_value_props(prop_diff)
self.client().update_l2_gateway(
self.resource_id, {'l2_gateway': prop_diff})
def resource_mapping():
return {
'OS::Neutron::L2Gateway': L2Gateway,
}

312
heat/tests/openstack/neutron/test_neutron_l2_gateway.py

@ -0,0 +1,312 @@
# Copyright 2018 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 mock
from neutronclient.common import exceptions
from neutronclient.v2_0 import client as neutronclient
import six
from heat.common import template_format
from heat.engine.clients.os import neutron
from heat.engine import scheduler
from heat.tests import common
from heat.tests import utils
try:
from networking_l2gw.services.l2gateway.exceptions import (
L2GatewaySegmentationRequired) # noqa
except ImportError:
class L2GatewaySegmentationRequired(exceptions.NeutronException):
message = ("L2 gateway segmentation id must be consistent for all "
"the interfaces")
class NeutronL2GatewayTest(common.HeatTestCase):
test_template = '''
heat_template_version: queens
description: Template to test L2Gateway Neutron resource
resources:
l2gw:
type: OS::Neutron::L2Gateway
properties:
name: L2GW01
devices:
- device_name: switch01
interfaces:
- name: eth0
- name: eth1
'''
test_template_update = '''
heat_template_version: queens
description: Template to test L2Gateway Neutron resource
resources:
l2gw:
type: OS::Neutron::L2Gateway
properties:
name: L2GW01
devices:
- device_name: switch01
interfaces:
- name: eth0
- name: eth1
- device_name: switch02
interfaces:
- name: eth5
- name: eth6
'''
test_template_with_seg = '''
heat_template_version: queens
description: Template to test L2Gateway Neutron resource
resources:
l2gw:
type: OS::Neutron::L2Gateway
properties:
name: L2GW01
devices:
- device_name: switch01
interfaces:
- name: eth0
segmentation_id:
- 101
- 102
- 103
- name: eth1
segmentation_id:
- 101
- 102
- 103
'''
test_template_invalid_seg = '''
heat_template_version: queens
description: Template to test L2Gateway Neutron resource
resources:
l2gw:
type: OS::Neutron::L2Gateway
properties:
name: L2GW01
devices:
- device_name: switch01
interfaces:
- name: eth0
segmentation_id:
- 101
- 102
- 103
- name: eth1
'''
mock_create_req = {
"l2_gateway": {
"name": "L2GW01",
"devices": [{
"device_name": "switch01",
"interfaces": [
{"name": "eth0"},
{"name": "eth1"}
]
}]
}}
mock_create_reply = {
"l2_gateway": {
"name": "L2GW01",
"id": "d3590f37-b072-4358-9719-71964d84a31c",
"tenant_id": "7ea656c7c9b8447494f33b0bc741d9e6",
"devices": [{
"device_name": "switch01",
"interfaces": [
{"name": "eth0"},
{"name": "eth1"}
]
}]
}}
mock_update_req = {
"l2_gateway": {
"devices": [{
"device_name": "switch01",
"interfaces": [
{"name": "eth0"},
{"name": "eth1"}
]
}, {
"device_name": "switch02",
"interfaces": [
{"name": "eth5"},
{"name": "eth6"}]
}]
}}
mock_update_reply = {
"l2_gateway": {
"name": "L2GW01",
"id": "d3590f37-b072-4358-9719-71964d84a31c",
"tenant_id": "7ea656c7c9b8447494f33b0bc741d9e6",
"devices": [{
"device_name": "switch01",
"interfaces": [
{"name": "eth0"},
{"name": "eth1"}]
}, {
"device_name": "switch02",
"interfaces": [
{"name": "eth5"},
{"name": "eth6"}]
}]
}}
mock_create_with_seg_req = {
"l2_gateway": {
"name": "L2GW01",
"devices": [{
"device_name": "switch01",
"interfaces": [
{"name": "eth0",
"segmentation_id": [101, 102, 103]},
{"name": "eth1",
"segmentation_id": [101, 102, 103]}
]
}]
}}
mock_create_with_seg_reply = {
"l2_gateway": {
"name": "L2GW01",
"id": "d3590f37-b072-4358-9719-71964d84a31c",
"tenant_id": "7ea656c7c9b8447494f33b0bc741d9e6",
"devices": [{
"device_name": "switch01",
"interfaces": [
{"name": "eth0",
"segmentation_id": ["101", "102", "103"]},
{"name": "eth1",
"segmentation_id": ["101", "102", "103"]}
]
}]
}}
mock_create_invalid_seg_req = {
"l2_gateway": {
"name": "L2GW01",
"devices": [{
"device_name": "switch01",
"interfaces": [
{"name": "eth0",
"segmentation_id": [101, 102, 103]},
{"name": "eth1"}
]
}]
}}
mock_create_invalid_seg_reply = {
"l2_gateway": {
"name": "L2GW01",
"id": "d3590f37-b072-4358-9719-71964d84a31c",
"tenant_id": "7ea656c7c9b8447494f33b0bc741d9e6",
"devices": [{
"device_name": "switch01",
"interfaces": [
{"name": "eth0",
"segmentation_id": ["101", "102", "103"]},
{"name": "eth1"}
]
}]
}}
def setUp(self):
super(NeutronL2GatewayTest, self).setUp()
self.mockclient = mock.MagicMock()
self.patchobject(neutronclient, 'Client', return_value=self.mockclient)
self.patchobject(neutron.NeutronClientPlugin, 'has_extension',
return_value=True)
def _create_l2_gateway(self, hot, reply):
# stack create
self.mockclient.create_l2_gateway.return_value = reply
self.mockclient.show_l2_gateway.return_value = reply
template = template_format.parse(hot)
self.stack = utils.parse_stack(template)
scheduler.TaskRunner(self.stack.create)()
self.l2gw_resource = self.stack['l2gw']
def test_l2_gateway_create(self):
self._create_l2_gateway(self.test_template, self.mock_create_reply)
self.assertIsNone(self.l2gw_resource.validate())
self.assertEqual((self.l2gw_resource.CREATE,
self.l2gw_resource.COMPLETE),
self.l2gw_resource.state)
self.assertEqual('d3590f37-b072-4358-9719-71964d84a31c',
self.l2gw_resource.FnGetRefId())
self.mockclient.create_l2_gateway.assert_called_once_with(
self.mock_create_req)
def test_l2_gateway_update(self):
self._create_l2_gateway(self.test_template, self.mock_create_reply)
# update l2_gateway with 2nd device
self.mockclient.update_l2_gateway.return_value = self.mock_update_reply
self.mockclient.show_l2_gateway.return_value = self.mock_update_reply
updated_tmpl = template_format.parse(self.test_template_update)
updated_stack = utils.parse_stack(updated_tmpl)
self.stack.update(updated_stack)
ud_l2gw_resource = self.stack['l2gw']
self.assertIsNone(ud_l2gw_resource.validate())
self.assertEqual((ud_l2gw_resource.UPDATE, ud_l2gw_resource.COMPLETE),
ud_l2gw_resource.state)
self.assertEqual('d3590f37-b072-4358-9719-71964d84a31c',
ud_l2gw_resource.FnGetRefId())
self.mockclient.update_l2_gateway.assert_called_once_with(
'd3590f37-b072-4358-9719-71964d84a31c',
self.mock_update_req)
def test_l2_gateway_create_with_seg(self):
# test with segmentation_id in template
self._create_l2_gateway(self.test_template_with_seg,
self.mock_create_with_seg_reply)
self.assertIsNone(self.l2gw_resource.validate())
self.assertEqual((self.l2gw_resource.CREATE,
self.l2gw_resource.COMPLETE),
self.l2gw_resource.state)
self.assertEqual('d3590f37-b072-4358-9719-71964d84a31c',
self.l2gw_resource.FnGetRefId())
self.mockclient.create_l2_gateway.assert_called_once_with(
self.mock_create_with_seg_req)
def test_l2_gateway_create_invalid_seg(self):
# test failure when segmentation_id is not consistent across
# all interfaces
self.mockclient.create_l2_gateway.side_effect = (
L2GatewaySegmentationRequired())
template = template_format.parse(self.test_template_invalid_seg)
self.stack = utils.parse_stack(template)
scheduler.TaskRunner(self.stack.create)()
self.l2gw_resource = self.stack['l2gw']
self.assertIsNone(self.l2gw_resource.validate())
self.assertEqual(
six.text_type('Resource CREATE failed: '
'L2GatewaySegmentationRequired: resources.l2gw: '
'L2 gateway segmentation id must be consistent for '
'all the interfaces'),
self.stack.status_reason)
self.assertEqual((self.l2gw_resource.CREATE,
self.l2gw_resource.FAILED),
self.l2gw_resource.state)
self.mockclient.create_l2_gateway.assert_called_once_with(
self.mock_create_invalid_seg_req)
def test_l2_gateway_delete(self):
self._create_l2_gateway(self.test_template, self.mock_create_reply)
self.stack.delete()
self.mockclient.delete_l2_gateway.assert_called_with(
'd3590f37-b072-4358-9719-71964d84a31c')

6
releasenotes/notes/neutron-l2gw-support-9fbb690bb5648f76.yaml

@ -0,0 +1,6 @@
---
features:
- New resource ``OS::Neutron::L2Gateway`` to allow management of Neutron
Layer2 Gateway. This resource provides life-cycle management of layer2
gateway instances. The resource depends on the Neutron ``l2-gateway``
extension.
Loading…
Cancel
Save