Heat support for Tap-as-a-Service resources

Add heat support for Tap as a Service resources (Tap Service and Tap Flow)

Change-Id: I2383e02dd3126edece2acf8425143fd3745ef454
This commit is contained in:
Deepak Tiwari 2018-08-06 22:45:02 +05:30 committed by Zane Bitter
parent 262c432e96
commit b9af0cf53f
9 changed files with 695 additions and 0 deletions

View File

@ -199,6 +199,10 @@ class NeutronClientPlugin(client_plugin.ClientPlugin):
path = "/sfc/flow_classifiers" path = "/sfc/flow_classifiers"
elif resource == 'port_chain': elif resource == 'port_chain':
path = "/sfc/port_chains" path = "/sfc/port_chains"
elif resource == 'tap_service':
path = "/taas/tap_services"
elif resource == 'tap_flow':
path = "/taas/tap_flows"
return path return path
def create_ext_resource(self, resource, props): def create_ext_resource(self, resource, props):
@ -229,6 +233,13 @@ class NeutronClientPlugin(client_plugin.ClientPlugin):
return self.client().show_ext(path + '/%s', resource_id return self.client().show_ext(path + '/%s', resource_id
).get(resource) ).get(resource)
def check_ext_resource_status(self, resource, resource_id):
ext_resource = self.show_ext_resource(resource, resource_id)
status = ext_resource['status']
if status == 'ERROR':
raise exception.ResourceInError(resource_status=status)
return status == 'ACTIVE'
def resolve_ext_resource(self, resource, name_or_id): def resolve_ext_resource(self, resource, name_or_id):
"""Returns the id and validate neutron ext resource.""" """Returns the id and validate neutron ext resource."""

View File

@ -0,0 +1,32 @@
# Copyright (c) 2018 AT&T Corporation.
#
# All Rights Reserved.
# 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 heat.engine.clients.os.neutron import neutron_constraints as nc
CLIENT_NAME = 'neutron'
class TapServiceConstraint(nc.NeutronExtConstraint):
resource_name = 'tap_service'
extension = 'taas'
class TapFlowConstraint(nc.NeutronExtConstraint):
resource_name = 'tap_flow'
extension = 'taas'
class TaaSProviderConstraint(nc.ProviderConstraint):
service_type = 'TAPASASERVICE'

View File

@ -0,0 +1,173 @@
# Copyright (c) 2018 AT&T Corporation.
#
# 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 heat.common import exception
from heat.common.i18n import _
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
COMMA_SEPARATED_LIST_REGEX = r"^([0-9]+(-[0-9]+)?)(,([0-9]+(-[0-9]+)?))*$"
class TapFlow(neutron.NeutronResource):
"""A resource for neutron tap-as-a-service tap-flow.
This plug-in requires neutron-taas. So to enable this
plug-in, install this library and restart the heat-engine.
A Tap-Flow represents the port from which the traffic needs
to be mirrored.
"""
required_service_extension = 'taas'
entity = 'tap_flow'
support_status = support.SupportStatus(version='12.0.0')
PROPERTIES = (
NAME, DESCRIPTION, PORT, TAP_SERVICE, DIRECTION,
VLAN_FILTER
) = (
'name', 'description', 'port', 'tap_service', 'direction',
'vlan_filter'
)
properties_schema = {
NAME: properties.Schema(
properties.Schema.STRING,
_('Name for the Tap-Flow.'),
default="",
update_allowed=True
),
DESCRIPTION: properties.Schema(
properties.Schema.STRING,
_('Description for the Tap-Flow.'),
default="",
update_allowed=True
),
PORT: properties.Schema(
properties.Schema.STRING,
_('ID or name of the tap-flow neutron port.'),
constraints=[constraints.CustomConstraint('neutron.port')],
required=True,
),
TAP_SERVICE: properties.Schema(
properties.Schema.STRING,
_('ID or name of the neutron tap-service.'),
constraints=[
constraints.CustomConstraint('neutron.taas.tap_service')
],
required=True,
),
DIRECTION: properties.Schema(
properties.Schema.STRING,
_('The Direction to capture the traffic on.'),
default='BOTH',
constraints=[
constraints.AllowedValues(['IN', 'OUT', 'BOTH']),
]
),
VLAN_FILTER: properties.Schema(
properties.Schema.STRING,
_('Comma separated list of VLANs, data for which needs to be '
'captured on probe VM.'),
constraints=[
constraints.AllowedPattern(COMMA_SEPARATED_LIST_REGEX),
],
),
}
def translation_rules(self, props):
return [
translation.TranslationRule(
props,
translation.TranslationRule.RESOLVE,
[self.PORT],
client_plugin=self.client_plugin(),
finder='find_resourceid_by_name_or_id',
entity='port'
),
translation.TranslationRule(
props,
translation.TranslationRule.RESOLVE,
[self.TAP_SERVICE],
client_plugin=self.client_plugin(),
finder='find_resourceid_by_name_or_id',
entity='tap_service'
)
]
def _show_resource(self):
return self.client_plugin().show_ext_resource('tap_flow',
self.resource_id)
def handle_create(self):
props = self.prepare_properties(self.properties,
self.physical_resource_name())
props['source_port'] = props.pop(self.PORT)
props['tap_service_id'] = props.pop(self.TAP_SERVICE)
tap_flow = self.client_plugin().create_ext_resource('tap_flow',
props)
self.resource_id_set(tap_flow['id'])
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
if prop_diff:
self.prepare_update_properties(prop_diff)
self.client_plugin().update_ext_resource('tap_flow', prop_diff,
self.resource_id)
def handle_delete(self):
if self.resource_id is None:
return
with self.client_plugin().ignore_not_found:
self.client_plugin().delete_ext_resource('tap_flow',
self.resource_id)
def check_create_complete(self, data):
return self.client_plugin().check_ext_resource_status(
'tap_flow', self.resource_id)
def check_update_complete(self, prop_diff):
if prop_diff:
return self.client_plugin().check_ext_resource_status(
'tap_flow', self.resource_id)
return True
def check_delete_complete(self, data):
if self.resource_id is None:
return True
with self.client_plugin().ignore_not_found:
try:
if self.client_plugin().check_ext_resource_status(
'tap_flow', self.resource_id):
self.client_plugin().delete_ext_resource(
'tap_flow', self.resource_id)
except exception.ResourceInError:
# Still try to delete tap resource in error state
self.client_plugin().delete_ext_resource('tap_flow',
self.resource_id)
return False
return True
def resource_mapping():
return {
'OS::Neutron::TaaS::TapFlow': TapFlow,
}

View File

@ -0,0 +1,137 @@
# Copyright (c) 2018 AT&T Corporation.
#
# 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 heat.common import exception
from heat.common.i18n import _
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
class TapService(neutron.NeutronResource):
"""A resource for neutron tap-as-a-service tap-service.
This plug-in requires neutron-taas. So to enable this
plug-in, install this library and restart the heat-engine.
A Tap-Service represents the port on which the mirrored traffic is
delivered. Any VM that uses the mirrored data is attached to this port.
"""
required_service_extension = 'taas'
entity = 'tap_service'
support_status = support.SupportStatus(version='12.0.0')
PROPERTIES = (
NAME, DESCRIPTION, PORT,
) = (
'name', 'description', 'port',
)
properties_schema = {
NAME: properties.Schema(
properties.Schema.STRING,
_('Name for the Tap-Service.'),
default="",
update_allowed=True
),
DESCRIPTION: properties.Schema(
properties.Schema.STRING,
_('Description for the Tap-Service.'),
default="",
update_allowed=True
),
PORT: properties.Schema(
properties.Schema.STRING,
_('ID or name of the tap-service neutron port.'),
constraints=[constraints.CustomConstraint('neutron.port')],
required=True,
),
}
def translation_rules(self, props):
return [
translation.TranslationRule(
props,
translation.TranslationRule.RESOLVE,
[self.PORT],
client_plugin=self.client_plugin(),
finder='find_resourceid_by_name_or_id',
entity='port'
)
]
def _show_resource(self):
return self.client_plugin().show_ext_resource('tap_service',
self.resource_id)
def handle_create(self):
props = self.prepare_properties(self.properties,
self.physical_resource_name())
props['port_id'] = props.pop(self.PORT)
ts = self.client_plugin().create_ext_resource('tap_service',
props)
self.resource_id_set(ts['id'])
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
if prop_diff:
self.prepare_update_properties(prop_diff)
self.client_plugin().update_ext_resource('tap_service',
prop_diff,
self.resource_id)
def handle_delete(self):
if self.resource_id is None:
return
with self.client_plugin().ignore_not_found:
self.client_plugin().delete_ext_resource('tap_service',
self.resource_id)
def check_create_complete(self, data):
return self.client_plugin().check_ext_resource_status(
'tap_service', self.resource_id)
def check_update_complete(self, prop_diff):
if prop_diff:
return self.client_plugin().check_ext_resource_status(
'tap_service', self.resource_id)
return True
def check_delete_complete(self, data):
if self.resource_id is None:
return True
with self.client_plugin().ignore_not_found:
try:
if self.client_plugin().check_ext_resource_status(
'tap_service', self.resource_id):
self.client_plugin().delete_ext_resource(
'tap_service', self.resource_id)
except exception.ResourceInError:
# Still try to delete tap resource in error state
self.client_plugin().delete_ext_resource('tap_service',
self.resource_id)
return False
return True
def resource_mapping():
return {
'OS::Neutron::TaaS::TapService': TapService,
}

View File

@ -0,0 +1,180 @@
#
# 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 heat.engine.resources.openstack.neutron.taas import tap_flow
from heat.engine import stack
from heat.engine import template
from heat.tests import common
from heat.tests import utils
sample_template = {
'heat_template_version': '2016-04-08',
'resources': {
'test_resource': {
'type': 'OS::Neutron::TaaS::TapFlow',
'properties': {
'name': 'test_tap_flow',
'description': 'desc',
'port': '6af055d3-26f6-48dd-a597-7611d7e58d35',
'tap_service': '6af055d3-26f6-48dd-a597-7611d7e58d35',
'direction': 'BOTH',
'vlan_filter': '1-5,9,18,27-30,99-108,4000-4095'
}
}
}
}
RESOURCE_TYPE = 'OS::Neutron::TaaS::TapFlow'
class TapFlowTest(common.HeatTestCase):
def setUp(self):
super(TapFlowTest, self).setUp()
self.ctx = utils.dummy_context()
self.stack = stack.Stack(
self.ctx, 'test_stack',
template.Template(sample_template)
)
self.test_resource = self.stack['test_resource']
self.test_client_plugin = mock.MagicMock()
self.test_resource.client_plugin = mock.MagicMock(
return_value=self.test_client_plugin)
self.test_client = mock.MagicMock()
self.test_resource.client = mock.MagicMock(
return_value=self.test_client)
self.test_client_plugin.get_notification = mock.MagicMock(
return_value='sample_notification')
def test_resource_mapping(self):
mapping = tap_flow.resource_mapping()
self.assertEqual(
tap_flow.TapFlow,
mapping['OS::Neutron::TaaS::TapFlow'])
def _get_mock_resource(self):
value = mock.MagicMock()
value.id = '477e8273-60a7-4c41-b683-fdb0bc7cd152'
return value
def test_resource_handle_create(self):
mock_tap_flow_create = self.test_client_plugin.create_ext_resource
mock_resource = self._get_mock_resource()
mock_tap_flow_create.return_value = mock_resource
# validate the properties
self.assertEqual(
'test_tap_flow',
self.test_resource.properties.get(
tap_flow.TapFlow.NAME))
self.assertEqual(
'desc',
self.test_resource.properties.get(
tap_flow.TapFlow.DESCRIPTION))
self.assertEqual(
'6af055d3-26f6-48dd-a597-7611d7e58d35',
self.test_resource.properties.get(
tap_flow.TapFlow.PORT))
self.assertEqual(
'6af055d3-26f6-48dd-a597-7611d7e58d35',
self.test_resource.properties.get(
tap_flow.TapFlow.TAP_SERVICE))
self.assertEqual(
'BOTH',
self.test_resource.properties.get(
tap_flow.TapFlow.DIRECTION))
self.assertEqual(
'1-5,9,18,27-30,99-108,4000-4095',
self.test_resource.properties.get(
tap_flow.TapFlow.VLAN_FILTER))
self.test_resource.data_set = mock.Mock()
self.test_resource.handle_create()
mock_tap_flow_create.assert_called_once_with(
'tap_flow',
{
'name': 'test_tap_flow',
'description': 'desc',
'port': '6af055d3-26f6-48dd-a597-7611d7e58d35',
'tap_service': '6af055d3-26f6-48dd-a597-7611d7e58d35',
'direction': 'BOTH',
'vlan_filter': '1-5,9,18,27-30,99-108,4000-4095',
}
)
def test_resource_handle_delete(self):
mock_tap_flow_delete = self.test_client_plugin.delete_ext_resource
self.test_resource.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151'
mock_tap_flow_delete.return_value = None
self.assertIsNone(self.test_resource.handle_delete())
mock_tap_flow_delete.assert_called_once_with(
'tap_flow', self.test_resource.resource_id)
def test_resource_handle_delete_resource_id_is_none(self):
self.test_resource.resource_id = None
self.assertIsNone(self.test_resource.handle_delete())
self.assertEqual(0, self.test_client_plugin.
delete_ext_resource.call_count)
def test_resource_handle_delete_not_found(self):
self.test_resource.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151'
mock_tap_flow_delete = self.test_client_plugin.delete_ext_resource
mock_tap_flow_delete.side_effect = self.test_client_plugin.NotFound
self.assertIsNone(self.test_resource.handle_delete())
def test_resource_show_resource(self):
mock_tap_flow_get = self.test_client_plugin.show_ext_resource
mock_tap_flow_get.return_value = {}
self.assertEqual({},
self.test_resource._show_resource(),
'Failed to show resource')
def test_resource_handle_update(self):
mock_tap_flow_patch = self.test_client_plugin.update_ext_resource
self.test_resource.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151'
prop_diff = {
tap_flow.TapFlow.NAME:
'name-updated',
tap_flow.TapFlow.DESCRIPTION:
'description-updated',
tap_flow.TapFlow.PORT:
'6af055d3-26f6-48dd-a597-7611d7e58d35',
tap_flow.TapFlow.TAP_SERVICE:
'6af055d3-26f6-48dd-a597-7611d7e58d35',
tap_flow.TapFlow.DIRECTION:
'BOTH',
tap_flow.TapFlow.VLAN_FILTER:
'1-5,9,18,27-30,99-108,4000-4095'
}
self.test_resource.handle_update(json_snippet=None,
tmpl_diff=None,
prop_diff=prop_diff)
mock_tap_flow_patch.assert_called_once_with(
'tap_flow',
{
'name': 'name-updated',
'description': 'description-updated',
'port': '6af055d3-26f6-48dd-a597-7611d7e58d35',
'tap_service': '6af055d3-26f6-48dd-a597-7611d7e58d35',
'direction': 'BOTH',
'vlan_filter': '1-5,9,18,27-30,99-108,4000-4095',
}, self.test_resource.resource_id)

View File

@ -0,0 +1,152 @@
#
# 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 heat.engine.resources.openstack.neutron.taas import tap_service
from heat.engine import stack
from heat.engine import template
from heat.tests import common
from heat.tests import utils
sample_template = {
'heat_template_version': '2016-04-08',
'resources': {
'test_resource': {
'type': 'OS::Neutron::TaaS::TapService',
'properties': {
'name': 'test_tap_service',
'description': 'desc',
'port': '6af055d3-26f6-48dd-a597-7611d7e58d35',
}
}
}
}
RESOURCE_TYPE = 'OS::Neutron::TaaS::TapService'
class TapServiceTest(common.HeatTestCase):
def setUp(self):
super(TapServiceTest, self).setUp()
self.ctx = utils.dummy_context()
self.stack = stack.Stack(
self.ctx, 'test_stack',
template.Template(sample_template)
)
self.test_resource = self.stack['test_resource']
self.test_client_plugin = mock.MagicMock()
self.test_resource.client_plugin = mock.MagicMock(
return_value=self.test_client_plugin)
self.test_client = mock.MagicMock()
self.test_resource.client = mock.MagicMock(
return_value=self.test_client)
self.test_client_plugin.get_notification = mock.MagicMock(
return_value='sample_notification')
def test_resource_mapping(self):
mapping = tap_service.resource_mapping()
self.assertEqual(tap_service.TapService,
mapping['OS::Neutron::TaaS::TapService'])
def _get_mock_resource(self):
value = mock.MagicMock()
value.id = '477e8273-60a7-4c41-b683-fdb0bc7cd152'
return value
def test_resource_handle_create(self):
mock_tap_service_create = self.test_client_plugin.create_ext_resource
mock_resource = self._get_mock_resource()
mock_tap_service_create.return_value = mock_resource
# validate the properties
self.assertEqual(
'test_tap_service',
self.test_resource.properties.get(
tap_service.TapService.NAME))
self.assertEqual(
'desc',
self.test_resource.properties.get(
tap_service.TapService.DESCRIPTION))
self.assertEqual(
'6af055d3-26f6-48dd-a597-7611d7e58d35',
self.test_resource.properties.get(
tap_service.TapService.PORT))
self.test_resource.data_set = mock.Mock()
self.test_resource.handle_create()
mock_tap_service_create.assert_called_once_with(
'tap_service',
{
'name': 'test_tap_service',
'description': 'desc',
'port': '6af055d3-26f6-48dd-a597-7611d7e58d35',
}
)
def test_resource_handle_delete(self):
mock_tap_service_delete = self.test_client_plugin.delete_ext_resource
self.test_resource.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151'
mock_tap_service_delete.return_value = None
self.assertIsNone(self.test_resource.handle_delete())
mock_tap_service_delete.assert_called_once_with(
'tap_service', self.test_resource.resource_id)
def test_resource_handle_delete_resource_id_is_none(self):
self.test_resource.resource_id = None
self.assertIsNone(self.test_resource.handle_delete())
self.assertEqual(0, self.test_client_plugin.
delete_ext_resource.call_count)
def test_resource_handle_delete_not_found(self):
self.test_resource.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151'
mock_tap_service_delete = self.test_client_plugin.delete_ext_resource
mock_tap_service_delete.side_effect = self.test_client_plugin.NotFound
self.assertIsNone(self.test_resource.handle_delete())
def test_resource_show_resource(self):
mock_tap_service_get = self.test_client_plugin.show_ext_resource
mock_tap_service_get.return_value = {}
self.assertEqual({},
self.test_resource._show_resource(),
'Failed to show resource')
def test_resource_handle_update(self):
mock_tap_service_patch = self.test_client_plugin.update_ext_resource
self.test_resource.resource_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151'
prop_diff = {
tap_service.TapService.NAME:
'name-updated',
tap_service.TapService.DESCRIPTION:
'description-updated',
tap_service.TapService.PORT:
'6af055d3-26f6-48dd-a597-7611d7e58d35',
}
self.test_resource.handle_update(json_snippet=None,
tmpl_diff=None,
prop_diff=prop_diff)
mock_tap_service_patch.assert_called_once_with(
'tap_service',
{
'name': 'name-updated',
'description': 'description-updated',
'port': '6af055d3-26f6-48dd-a597-7611d7e58d35',
}, self.test_resource.resource_id)

View File

@ -0,0 +1,8 @@
---
features:
- |
A new ``OS::Neutron::TaaS::TapService`` resource is added to support a
Tap Service in the Neutron Tap-as-a-service plugin.
- |
A new ``OS::Neutron::TaaS::TapFlow`` resource is added to support a Tap
Flow in the Neutron Tap-as-a-service plugin.

View File

@ -142,6 +142,8 @@ heat.constraints =
neutron.segment = heat.engine.clients.os.openstacksdk:SegmentConstraint neutron.segment = heat.engine.clients.os.openstacksdk:SegmentConstraint
neutron.subnet = heat.engine.clients.os.neutron.neutron_constraints:SubnetConstraint neutron.subnet = heat.engine.clients.os.neutron.neutron_constraints:SubnetConstraint
neutron.subnetpool = heat.engine.clients.os.neutron.neutron_constraints:SubnetPoolConstraint neutron.subnetpool = heat.engine.clients.os.neutron.neutron_constraints:SubnetPoolConstraint
neutron.taas.tap_service = heat.engine.clients.os.neutron.taas_constraints:TapServiceConstraint
neutron.taas.tap_flow = heat.engine.clients.os.neutron.taas_constraints:TapFlowConstraint
nova.flavor = heat.engine.clients.os.nova:FlavorConstraint nova.flavor = heat.engine.clients.os.nova:FlavorConstraint
nova.host = heat.engine.clients.os.nova:HostConstraint nova.host = heat.engine.clients.os.nova:HostConstraint
nova.keypair = heat.engine.clients.os.nova:KeypairConstraint nova.keypair = heat.engine.clients.os.nova:KeypairConstraint