Merge "Add networking-sfc flow-classifier resource plug-in"

This commit is contained in:
Jenkins 2016-10-10 07:23:36 +00:00 committed by Gerrit Code Review
commit 2e838e9315
3 changed files with 398 additions and 1 deletions

View File

@ -164,7 +164,8 @@ class NeutronClientPlugin(client_plugin.ClientPlugin):
path = "/sfc/port_pairs"
elif resource == 'port_pair_group':
path = "/sfc/port_pair_groups"
elif resource == 'flow_classifier':
path = "/sfc/flow_classifiers"
return path
def create_sfc_resource(self, resource, props):

View File

@ -0,0 +1,190 @@
#
# 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.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 FlowClassifier(neutron.NeutronResource):
""""Heat Template Resource for networking-sfc flow-classifier.
This resource used to select the traffic that can access the service chain.
Traffic that matches any flow classifier will be directed to the first
port in the chain.
"""
support_status = support.SupportStatus(
version='8.0.0',
status=support.UNSUPPORTED)
PROPERTIES = (
NAME, DESCRIPTION, PROTOCOL, ETHERTYPE,
SOURCE_IP_PREFIX, DESTINATION_IP_PREFIX, SOURCE_PORT_RANGE_MIN,
SOURCE_PORT_RANGE_MAX, DESTINATION_PORT_RANGE_MIN,
DESTINATION_PORT_RANGE_MAX, LOGICAL_SOURCE_PORT,
LOGICAL_DESTINATION_PORT, L7_PARAMETERS,
) = (
'name', 'description', 'protocol', 'ethertype',
'source_ip_prefix', 'destination_ip_prefix',
'source_port_range_min', 'source_port_range_max',
'destination_port_range_min', 'destination_port_range_max',
'logical_source_port', 'logical_destination_port', 'l7_parameters',
)
properties_schema = {
NAME: properties.Schema(
properties.Schema.STRING,
_('Name of the Flow Classifier.'),
update_allowed=True
),
DESCRIPTION: properties.Schema(
properties.Schema.STRING,
_('Description for the Flow Classifier.'),
update_allowed=True
),
PROTOCOL: properties.Schema(
properties.Schema.STRING,
_('IP Protocol for the Flow Classifier.'),
constraints=[
constraints.AllowedValues(['tcp', 'udp', 'icmp']),
],
),
ETHERTYPE: properties.Schema(
properties.Schema.STRING,
_('L2 ethertype.'),
default='IPv4',
constraints=[
constraints.AllowedValues(['IPv4', 'IPv6']),
],
),
SOURCE_IP_PREFIX: properties.Schema(
properties.Schema.STRING,
_('Source IP prefix or subnet.'),
constraints=[
constraints.CustomConstraint('net_cidr')
]
),
DESTINATION_IP_PREFIX: properties.Schema(
properties.Schema.STRING,
_('Destination IP prefix or subnet.'),
constraints=[
constraints.CustomConstraint('net_cidr')
]
),
SOURCE_PORT_RANGE_MIN: properties.Schema(
properties.Schema.INTEGER,
_('Source protocol port Minimum.'),
constraints=[
constraints.Range(1, 65535)
]
),
SOURCE_PORT_RANGE_MAX: properties.Schema(
properties.Schema.INTEGER,
_('Source protocol port Maximum.'),
constraints=[
constraints.Range(1, 65535)
]
),
DESTINATION_PORT_RANGE_MIN: properties.Schema(
properties.Schema.INTEGER,
_('Destination protocol port minimum.'),
constraints=[
constraints.Range(1, 65535)
]
),
DESTINATION_PORT_RANGE_MAX: properties.Schema(
properties.Schema.INTEGER,
_('Destination protocol port maximum.'),
constraints=[
constraints.Range(1, 65535)
]
),
LOGICAL_SOURCE_PORT: properties.Schema(
properties.Schema.STRING,
_('ID or name of the neutron source port.'),
constraints=[
constraints.CustomConstraint('neutron.port')
]
),
LOGICAL_DESTINATION_PORT: properties.Schema(
properties.Schema.STRING,
_('ID or name of the neutron destination port.'),
constraints=[
constraints.CustomConstraint('neutron.port')
]
),
L7_PARAMETERS: properties.Schema(
properties.Schema.MAP,
_('Dictionary of L7-parameters.'),
support_status=support.SupportStatus(
status=support.UNSUPPORTED,
message=_('Currently, no value is supported for this option.'),
),
),
}
def translation_rules(self, props):
return [
translation.TranslationRule(
props,
translation.TranslationRule.RESOLVE,
[self.LOGICAL_SOURCE_PORT],
client_plugin=self.client_plugin(),
finder='find_resourceid_by_name_or_id',
entity='port'
),
translation.TranslationRule(
props,
translation.TranslationRule.RESOLVE,
[self.LOGICAL_DESTINATION_PORT],
client_plugin=self.client_plugin(),
finder='find_resourceid_by_name_or_id',
entity='port'
)
]
def _show_resource(self):
return self.client_plugin().show_sfc_resource('flow_classifier',
self.resource_id)
def handle_create(self):
props = self.prepare_properties(
self.properties,
self.physical_resource_name())
flow_classifier = self.client_plugin().create_sfc_resource(
'flow_classifier', props)
self.resource_id_set(flow_classifier['id'])
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
if prop_diff:
self.prepare_update_properties(prop_diff)
self.client_plugin().update_sfc_resource('flow_classifier',
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_sfc_resource('flow_classifier',
self.resource_id)
def resource_mapping():
return {
'OS::Neutron::FlowClassifier': FlowClassifier,
}

View File

@ -0,0 +1,206 @@
#
# 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.sfc import flow_classifier
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::FlowClassifier',
'properties': {
'name': 'test_flow_classifier',
'description': 'flow_classifier_desc',
'protocol': 'tcp',
'ethertype': 'IPv4',
'source_ip_prefix': '10.0.3.21',
'destination_ip_prefix': '10.0.3.22',
'source_port_range_min': 1,
'source_port_range_max': 10,
'destination_port_range_min': 80,
'destination_port_range_max': 100,
'logical_source_port': 'port-id1',
'logical_destination_port': 'port-id2',
'l7_parameters': {"url": 'http://local'}
}
}
}
}
RESOURCE_TYPE = 'OS::Neutron::FlowClassifier'
class FlowClassifierTest(common.HeatTestCase):
def setUp(self):
super(FlowClassifierTest, 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 = flow_classifier.resource_mapping()
self.assertEqual(flow_classifier.FlowClassifier,
mapping['OS::Neutron::FlowClassifier'])
def _get_mock_resource(self):
value = mock.MagicMock()
value.id = '2a046ff4-cd7b-4500-b8f0-b60d96ce3e0c'
return value
def test_resource_handle_create(self):
mock_fc_create = self.test_client_plugin.create_sfc_resource
mock_resource = self._get_mock_resource()
mock_fc_create.return_value = mock_resource
# validate the properties
self.assertEqual(
'test_flow_classifier',
self.test_resource.properties.get(
flow_classifier.FlowClassifier.NAME))
self.assertEqual(
'flow_classifier_desc',
self.test_resource.properties.get(
flow_classifier.FlowClassifier.DESCRIPTION))
self.assertEqual(
'tcp',
self.test_resource.properties.get(
flow_classifier.FlowClassifier.PROTOCOL))
self.assertEqual(
'IPv4',
self.test_resource.properties.get(
flow_classifier.FlowClassifier.ETHERTYPE))
self.assertEqual(
'10.0.3.21',
self.test_resource.properties.get(
flow_classifier.FlowClassifier.SOURCE_IP_PREFIX))
self.assertEqual(
'10.0.3.22',
self.test_resource.properties.get(
flow_classifier.FlowClassifier.DESTINATION_IP_PREFIX))
self.assertEqual(
1,
self.test_resource.properties.get(
flow_classifier.FlowClassifier.SOURCE_PORT_RANGE_MIN))
self.assertEqual(
10,
self.test_resource.properties.get(
flow_classifier.FlowClassifier.SOURCE_PORT_RANGE_MAX))
self.assertEqual(
80,
self.test_resource.properties.get(
flow_classifier.FlowClassifier.DESTINATION_PORT_RANGE_MIN))
self.assertEqual(
100,
self.test_resource.properties.get(
flow_classifier.FlowClassifier.DESTINATION_PORT_RANGE_MAX))
self.assertEqual(
'port-id1',
self.test_resource.properties.get(
flow_classifier.FlowClassifier.LOGICAL_SOURCE_PORT))
self.assertEqual(
'port-id2',
self.test_resource.properties.get(
flow_classifier.FlowClassifier.LOGICAL_DESTINATION_PORT))
self.assertEqual(
{"url": 'http://local'},
self.test_resource.properties.get(
flow_classifier.FlowClassifier.L7_PARAMETERS))
self.test_resource.data_set = mock.Mock()
self.test_resource.handle_create()
mock_fc_create.assert_called_once_with(
'flow_classifier',
{
'name': 'test_flow_classifier',
'description': 'flow_classifier_desc',
'protocol': 'tcp',
'ethertype': 'IPv4',
'source_ip_prefix': '10.0.3.21',
'destination_ip_prefix': '10.0.3.22',
'source_port_range_min': 1,
'source_port_range_max': 10,
'destination_port_range_min': 80,
'destination_port_range_max': 100,
'logical_source_port': 'port-id1',
'logical_destination_port': 'port-id2',
'l7_parameters': {"url": 'http://local'}
}
)
def test_resource_handle_delete(self):
mock_fc_delete = self.test_client_plugin.delete_sfc_resource
self.test_resource.resource_id = '2a046ff4-cd7b-4500-b8f0-b60d96ce3e0c'
mock_fc_delete.return_value = None
self.assertIsNone(self.test_resource.handle_delete())
mock_fc_delete.assert_called_once_with(
'flow_classifier', 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_sfc_resource.call_count)
def test_resource_handle_delete_not_found(self):
self.test_resource.resource_id = '2a046ff4-cd7b-4500-b8f0-b60d96ce3e0c'
mock_fc_delete = self.test_client_plugin.delete_sfc_resource
mock_fc_delete.side_effect = self.test_client_plugin.NotFound
self.assertIsNone(self.test_resource.handle_delete())
def test_resource_show_resource(self):
mock_fc_get = self.test_client_plugin.show_sfc_resource
mock_fc_get.return_value = {}
self.assertEqual({},
self.test_resource._show_resource(),
'Failed to show resource')
def test_resource_handle_update(self):
mock_fc_patch = self.test_client_plugin.update_sfc_resource
self.test_resource.resource_id = '2a046ff4-cd7b-4500-b8f0-b60d96ce3e0c'
prop_diff = {
flow_classifier.FlowClassifier.NAME:
'name-updated',
flow_classifier.FlowClassifier.DESCRIPTION:
'description-updated',
}
self.test_resource.handle_update(json_snippet=None,
tmpl_diff=None,
prop_diff=prop_diff)
mock_fc_patch.assert_called_once_with(
'flow_classifier',
{
'name': 'name-updated',
'description': 'description-updated',
}, self.test_resource.resource_id)