From 236ee7e6b4f4c14657586abc34bff370dc6cde1d Mon Sep 17 00:00:00 2001 From: Dmitriy Uvarenkov Date: Wed, 15 Jun 2016 11:00:27 +0300 Subject: [PATCH] Add new resource Security Group Rule As for now rules are a property of security group resource. The problem is that rules can contain security group id's as their parameters. This leads to circular dependencies. Now you can create security groups as before but also create new rules as separate resources and add them to security groups. Closes-Bug: #1581447 Implements bp securitygroupingressegress Change-Id: I3425960e3d5a63c54b0c6739e305a53780075095 --- .../openstack/neutron/security_group_rule.py | 200 ++++++++++++++++++ .../openstack/neutron/inline_templates.py | 13 ++ .../test_neutron_security_group_rule.py | 98 +++++++++ 3 files changed, 311 insertions(+) create mode 100644 heat/engine/resources/openstack/neutron/security_group_rule.py create mode 100644 heat/tests/openstack/neutron/test_neutron_security_group_rule.py diff --git a/heat/engine/resources/openstack/neutron/security_group_rule.py b/heat/engine/resources/openstack/neutron/security_group_rule.py new file mode 100644 index 0000000000..e06dd4830b --- /dev/null +++ b/heat/engine/resources/openstack/neutron/security_group_rule.py @@ -0,0 +1,200 @@ +# +# 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 SecurityGroupRule(neutron.NeutronResource): + """A resource for managing Neutron security group rules. + + Rules to use in security group resource. + """ + + required_service_extension = 'security-group' + + support_status = support.SupportStatus(version='7.0.0') + + PROPERTIES = ( + SECURITY_GROUP, DESCRIPTION, DIRECTION, ETHERTYPE, + PORT_RANGE_MIN, PORT_RANGE_MAX, PROTOCOL, REMOTE_GROUP, + REMOTE_IP_PREFIX + ) = ( + 'security_group', 'description', 'direction', 'ethertype', + 'port_range_min', 'port_range_max', 'protocol', 'remote_group', + 'remote_ip_prefix' + ) + + _allowed_protocols = list(range(256)) + [ + 'ah', 'dccp', 'egp', 'esp', 'gre', 'icmp', 'icmpv6', 'igmp', + 'ipv6-encap', 'ipv6-frag', 'ipv6-icmp', 'ipv6-nonxt', 'ipv6-opts', + 'ipv6-route', 'ospf', 'pgm', 'rsvp', 'sctp', 'tcp', 'udp', 'udplite', + 'vrrp' + ] + + properties_schema = { + SECURITY_GROUP: properties.Schema( + properties.Schema.STRING, + _('Security group name or ID to add rule.'), + required=True, + constraints=[ + constraints.CustomConstraint('neutron.security_group') + ] + ), + DESCRIPTION: properties.Schema( + properties.Schema.STRING, + _('Description of the security group rule.') + ), + DIRECTION: properties.Schema( + properties.Schema.STRING, + _('The direction in which the security group rule is applied. ' + 'For a compute instance, an ingress security group rule ' + 'matches traffic that is incoming (ingress) for that ' + 'instance. An egress rule is applied to traffic leaving ' + 'the instance.'), + default='ingress', + constraints=[ + constraints.AllowedValues(['ingress', 'egress']), + ] + ), + ETHERTYPE: properties.Schema( + properties.Schema.STRING, + _('Ethertype of the traffic.'), + default='IPv4', + constraints=[ + constraints.AllowedValues(['IPv4', 'IPv6']), + ] + ), + PORT_RANGE_MIN: properties.Schema( + properties.Schema.INTEGER, + _('The minimum port number in the range that is matched by the ' + 'security group rule. If the protocol is TCP or UDP, this ' + 'value must be less than or equal to the value of the ' + 'port_range_max attribute. If the protocol is ICMP, this ' + 'value must be an ICMP type.'), + constraints=[ + constraints.Range(0, 65535) + ] + ), + PORT_RANGE_MAX: properties.Schema( + properties.Schema.INTEGER, + _('The maximum port number in the range that is matched by the ' + 'security group rule. The port_range_min attribute constrains ' + 'the port_range_max attribute. If the protocol is ICMP, this ' + 'value must be an ICMP code.'), + constraints=[ + constraints.Range(0, 65535) + ] + ), + PROTOCOL: properties.Schema( + properties.Schema.STRING, + _('The protocol that is matched by the security group rule. ' + 'Allowed values are ah, dccp, egp, esp, gre, icmp, icmpv6, ' + 'igmp, ipv6-encap, ipv6-frag, ipv6-icmp, ipv6-nonxt, ipv6-opts, ' + 'ipv6-route, ospf, pgm, rsvp, sctp, tcp, udp, udplite, vrrp ' + 'and integer representations [0-255].'), + default='tcp', + constraints=[constraints.AllowedValues(_allowed_protocols)] + ), + REMOTE_GROUP: properties.Schema( + properties.Schema.STRING, + _('The remote group name or ID to be associated with this ' + 'security group rule.'), + constraints=[ + constraints.CustomConstraint('neutron.security_group') + ] + ), + REMOTE_IP_PREFIX: properties.Schema( + properties.Schema.STRING, + _('The remote IP prefix (CIDR) to be associated with this ' + 'security group rule.'), + constraints=[ + constraints.CustomConstraint('net_cidr') + ] + ) + } + + def translation_rules(self, props): + return [ + translation.TranslationRule( + props, + translation.TranslationRule.RESOLVE, + [self.SECURITY_GROUP], + client_plugin=self.client_plugin(), + finder='find_resourceid_by_name_or_id', + entity='security_group' + ), + translation.TranslationRule( + props, + translation.TranslationRule.RESOLVE, + [self.REMOTE_GROUP], + client_plugin=self.client_plugin(), + finder='find_resourceid_by_name_or_id', + entity='security_group' + ), + ] + + def _show_resource(self): + return self.client().show_security_group_rule( + self.resource_id)['security_group_rule'] + + def validate(self): + super(SecurityGroupRule, self).validate() + if (self.properties[self.REMOTE_GROUP] is not None and + self.properties[self.REMOTE_IP_PREFIX] is not None): + raise exception.ResourcePropertyConflict( + self.REMOTE_GROUP, self.REMOTE_IP_PREFIX) + port_max = self.properties[self.PORT_RANGE_MAX] + port_min = self.properties[self.PORT_RANGE_MIN] + protocol = self.properties[self.PROTOCOL] + if (port_max is not None and port_min is not None and + protocol not in ('icmp', 'icmpv6', 'ipv6-icmp') and + port_max < port_min): + msg = _('The minimum port number must be less than or equal to ' + 'the maximum port number.') + raise exception.StackValidationFailed(message=msg) + + def handle_create(self): + props = self.prepare_properties( + self.properties, + self.physical_resource_name()) + props['security_group_id'] = props.pop(self.SECURITY_GROUP) + if self.REMOTE_GROUP in props: + props['remote_group_id'] = props.pop(self.REMOTE_GROUP) + + for key in (self.PORT_RANGE_MIN, self.PORT_RANGE_MAX): + if props.get(key) is not None: + props[key] = str(props[key]) + + rule = self.client().create_security_group_rule( + {'security_group_rule': props})['security_group_rule'] + + self.resource_id_set(rule['id']) + + def handle_delete(self): + if self.resource_id is None: + return + + with self.client_plugin().ignore_not_found: + self.client().delete_security_group_rule(self.resource_id) + + +def resource_mapping(): + return { + 'OS::Neutron::SecurityGroupRule': SecurityGroupRule + } diff --git a/heat/tests/openstack/neutron/inline_templates.py b/heat/tests/openstack/neutron/inline_templates.py index 555cb5b91d..891122cb69 100644 --- a/heat/tests/openstack/neutron/inline_templates.py +++ b/heat/tests/openstack/neutron/inline_templates.py @@ -142,3 +142,16 @@ resources: type: HTTP url_path: /health ''' + +SECURITY_GROUP_RULE_TEMPLATE = ''' +heat_template_version: 2016-10-14 +resources: + security_group_rule: + type: OS::Neutron::SecurityGroupRule + properties: + security_group: 123 + description: test description + remote_group: 123 + protocol: tcp + port_range_min: 100 +''' diff --git a/heat/tests/openstack/neutron/test_neutron_security_group_rule.py b/heat/tests/openstack/neutron/test_neutron_security_group_rule.py new file mode 100644 index 0000000000..01b7036d59 --- /dev/null +++ b/heat/tests/openstack/neutron/test_neutron_security_group_rule.py @@ -0,0 +1,98 @@ +# +# 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.common import exception +from heat.common import template_format +from heat.engine.resources.openstack.neutron import security_group_rule +from heat.tests import common +from heat.tests.openstack.neutron import inline_templates +from heat.tests import utils + + +class SecurityGroupRuleTest(common.HeatTestCase): + + def test_resource_mapping(self): + mapping = security_group_rule.resource_mapping() + self.assertEqual(mapping['OS::Neutron::SecurityGroupRule'], + security_group_rule.SecurityGroupRule) + + @mock.patch('heat.engine.clients.os.neutron.' + 'NeutronClientPlugin.has_extension', return_value=True) + def _create_stack(self, ext_func, + tmpl=inline_templates.SECURITY_GROUP_RULE_TEMPLATE): + self.t = template_format.parse(tmpl) + self.stack = utils.parse_stack(self.t) + self.sg_rule = self.stack['security_group_rule'] + self.neutron_client = mock.MagicMock() + self.sg_rule.client = mock.MagicMock(return_value=self.neutron_client) + + self.sg_rule.client_plugin().find_resourceid_by_name_or_id = ( + mock.MagicMock(return_value='123')) + + def test_create(self): + self._create_stack() + self.neutron_client.create_security_group_rule.return_value = { + 'security_group_rule': {'id': '1234'}} + expected = { + 'security_group_rule': { + 'security_group_id': u'123', + 'description': u'test description', + 'remote_group_id': u'123', + 'protocol': u'tcp', + 'port_range_min': '100', + 'direction': 'ingress', + 'ethertype': 'IPv4' + } + } + + self.sg_rule.handle_create() + + self.neutron_client.create_security_group_rule.assert_called_with( + expected) + + def test_validate_conflict_props(self): + tmpl = inline_templates.SECURITY_GROUP_RULE_TEMPLATE + tmpl += ' remote_ip_prefix: "123"' + self._create_stack(tmpl=tmpl) + + self.assertRaises(exception.ResourcePropertyConflict, + self.sg_rule.validate) + + def test_validate_max_port_less_than_min_port(self): + tmpl = inline_templates.SECURITY_GROUP_RULE_TEMPLATE + tmpl += ' port_range_max: 50' + self._create_stack(tmpl=tmpl) + + self.assertRaises(exception.StackValidationFailed, + self.sg_rule.validate) + + def test_show_resource(self): + self._create_stack() + self.sg_rule.resource_id_set('1234') + self.neutron_client.show_security_group_rule.return_value = { + 'security_group_rule': {'id': '1234'} + } + + self.assertEqual({'id': '1234'}, self.sg_rule._show_resource()) + self.neutron_client.show_security_group_rule.assert_called_with('1234') + + def test_delete(self): + self._create_stack() + self.sg_rule.resource_id_set('1234') + + self.sg_rule.handle_delete() + + self.neutron_client.delete_security_group_rule.assert_called_with( + '1234')