From: Simon Pasquier Date: Wed, 14 Aug 2013 13:00:44 +0000 (+0200) Subject: Add support for source security groups X-Git-Tag: 2014.1~136 X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=82f7086e232915ca2744a081afca51211c99e813;p=openstack-build%2Fheat-build.git Add support for source security groups This patch adds support for the SourceSecurityGroupName and SourceSecurityGroupId properties. It covers Nova and Neutron. Change-Id: Ic12512dfb4375ccccbe1282bb48b80cde16ceb9d Fixes: bug #1193415 --- diff --git a/heat/common/exception.py b/heat/common/exception.py index 08fdf8d1..74adf7c5 100644 --- a/heat/common/exception.py +++ b/heat/common/exception.py @@ -337,3 +337,8 @@ class HTTPExceptionDisguise(Exception): class TemplateTooBig(HeatException): message = _('Template exceeds maximum allowed size.') + + +class EgressRuleNotAllowed(HeatException): + message = _("Egress rules are only allowed when " + "Neutron is used and the 'VpcId' property is set.") diff --git a/heat/engine/resources/security_group.py b/heat/engine/resources/security_group.py index f0925ad4..a6ade948 100644 --- a/heat/engine/resources/security_group.py +++ b/heat/engine/resources/security_group.py @@ -16,6 +16,7 @@ from heat.engine import clients from heat.engine import resource +from heat.common import exception from heat.openstack.common import log as logging logger = logging.getLogger(__name__) @@ -34,6 +35,20 @@ class SecurityGroup(resource.Resource): else: self._handle_create_nova() + def _convert_to_neutron_rule(self, direction, sg_rule): + return { + 'direction': direction, + 'ethertype': 'IPv4', + 'remote_ip_prefix': sg_rule.get('CidrIp'), + 'port_range_min': sg_rule.get('FromPort'), + 'port_range_max': sg_rule.get('ToPort'), + 'protocol': sg_rule.get('IpProtocol'), + # Neutron understands both names and ids + 'remote_group_id': sg_rule.get('SourceSecurityGroupId') or + sg_rule.get('SourceSecurityGroupName'), + 'security_group_id': self.resource_id + } + def _handle_create_neutron(self): from neutronclient.common.exceptions import NeutronClientException client = self.neutron() @@ -43,28 +58,23 @@ class SecurityGroup(resource.Resource): 'description': self.properties['GroupDescription']} })['security_group'] + def sanitize_security_group(i): + # Neutron only accepts positive ints + if i.get('FromPort') is not None and int(i['FromPort']) < 0: + i['FromPort'] = None + if i.get('ToPort') is not None and int(i['ToPort']) < 0: + i['ToPort'] = None + if i.get('FromPort') is None and i.get('ToPort') is None: + i['CidrIp'] = None + self.resource_id_set(sec['id']) if self.properties['SecurityGroupIngress']: for i in self.properties['SecurityGroupIngress']: - # Neutron only accepts positive ints - if int(i['FromPort']) < 0: - i['FromPort'] = None - if int(i['ToPort']) < 0: - i['ToPort'] = None - if i['FromPort'] is None and i['ToPort'] is None: - i['CidrIp'] = None - + sanitize_security_group(i) try: rule = client.create_security_group_rule({ - 'security_group_rule': { - 'direction': 'ingress', - 'remote_ip_prefix': i['CidrIp'], - 'port_range_min': i['FromPort'], - 'ethertype': 'IPv4', - 'port_range_max': i['ToPort'], - 'protocol': i['IpProtocol'], - 'security_group_id': sec['id'] - } + 'security_group_rule': + self._convert_to_neutron_rule('ingress', i) }) except NeutronClientException as ex: if ex.status_code == 409: @@ -74,18 +84,17 @@ class SecurityGroup(resource.Resource): # unexpected error raise if self.properties['SecurityGroupEgress']: + # Delete the default rules which allow all egress traffic + for rule in sec['security_group_rules']: + if rule['direction'] == 'egress': + client.delete_security_group_rule(rule['id']) + for i in self.properties['SecurityGroupEgress']: + sanitize_security_group(i) try: rule = client.create_security_group_rule({ - 'security_group_rule': { - 'direction': 'egress', - 'remote_ip_prefix': i['CidrIp'], - 'port_range_min': i['FromPort'], - 'ethertype': 'IPv4', - 'port_range_max': i['ToPort'], - 'protocol': i['IpProtocol'], - 'security_group_id': sec['id'] - } + 'security_group_rule': + self._convert_to_neutron_rule('egress', i) }) except NeutronClientException as ex: if ex.status_code == 409: @@ -113,12 +122,22 @@ class SecurityGroup(resource.Resource): if self.properties['SecurityGroupIngress']: rules_client = self.nova().security_group_rules for i in self.properties['SecurityGroupIngress']: + source_group_id = None + if i.get('SourceSecurityGroupId') is not None: + source_group_id = i['SourceSecurityGroupId'] + elif i.get('SourceSecurityGroupName') is not None: + for group in groups: + if group.name == i['SourceSecurityGroupName']: + source_group_id = group.id + break try: - rule = rules_client.create(sec.id, - i['IpProtocol'], - i['FromPort'], - i['ToPort'], - i['CidrIp']) + rule = rules_client.create( + sec.id, + i.get('IpProtocol'), + i.get('FromPort'), + i.get('ToPort'), + i.get('CidrIp'), + source_group_id) except clients.novaclient.exceptions.BadRequest as ex: if ex.message.find('already exists') >= 0: # no worries, the rule is already there @@ -181,6 +200,16 @@ class SecurityGroup(resource.Resource): else: return self.physical_resource_name() + def validate(self): + res = super(SecurityGroup, self).validate() + if res: + return res + + if self.properties['SecurityGroupEgress'] and not( + self.properties['VpcId'] and + clients.neutronclient is not None): + raise exception.EgressRuleNotAllowed() + def resource_mapping(): return { diff --git a/heat/tests/test_security_group.py b/heat/tests/test_security_group.py index e8e12f2c..d3af56bc 100644 --- a/heat/tests/test_security_group.py +++ b/heat/tests/test_security_group.py @@ -15,6 +15,7 @@ import collections from heat.engine import clients +from heat.common import exception from heat.common import template_format from heat.engine import parser from heat.engine import resource @@ -55,6 +56,24 @@ Resources: FromPort : 80 ToPort : 80 CidrIp : 0.0.0.0/0 + - IpProtocol: tcp + SourceSecurityGroupName: test + - IpProtocol: icmp + SourceSecurityGroupId: 1 +''' + + test_template_nova_with_egress = ''' +HeatTemplateFormatVersion: '2012-12-12' +Resources: + the_sg: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: HTTP and SSH access + SecurityGroupEgress: + - IpProtocol: tcp + FromPort: 22 + ToPort: 22 + CidrIp: 0.0.0.0/0 ''' test_template_neutron = ''' @@ -74,11 +93,14 @@ Resources: FromPort : 80 ToPort : 80 CidrIp : 0.0.0.0/0 + - IpProtocol: tcp + SourceSecurityGroupId: wwww SecurityGroupEgress: - IpProtocol: tcp FromPort: 22 ToPort: 22 CidrIp: 10.0.1.0/24 + - SourceSecurityGroupName: xxxx ''' def setUp(self): @@ -142,9 +164,13 @@ Resources: clients.OpenStackClients.nova('compute').AndReturn(self.fc) nova_sgr.SecurityGroupRuleManager.create( - 2, 'tcp', 22, 22, '0.0.0.0/0').AndReturn(None) + 2, 'tcp', 22, 22, '0.0.0.0/0', None).AndReturn(None) + nova_sgr.SecurityGroupRuleManager.create( + 2, 'tcp', 80, 80, '0.0.0.0/0', None).AndReturn(None) nova_sgr.SecurityGroupRuleManager.create( - 2, 'tcp', 80, 80, '0.0.0.0/0').AndReturn(None) + 2, 'tcp', None, None, None, 1).AndReturn(None) + nova_sgr.SecurityGroupRuleManager.create( + 2, 'icmp', None, None, None, 1).AndReturn(None) # delete script clients.OpenStackClients.nova('compute').AndReturn(self.fc) @@ -172,6 +198,28 @@ Resources: 'cidr': '0.0.0.0/0' }, 'id': 131 + }, { + 'from_port': None, + 'group': { + 'tenant_id': 'f18ca530cc05425e8bac0a5ff92f7e88', + 'name': 'test' + }, + 'ip_protocol': 'tcp', + 'to_port': None, + 'parent_group_id': 2, + 'ip_range': {}, + 'id': 132 + }, { + 'from_port': None, + 'group': { + 'tenant_id': 'f18ca530cc05425e8bac0a5ff92f7e88', + 'name': 'test' + }, + 'ip_protocol': 'icmp', + 'to_port': None, + 'parent_group_id': 2, + 'ip_range': {}, + 'id': 133 }] )) clients.OpenStackClients.nova('compute').AndReturn(self.fc) @@ -179,6 +227,10 @@ Resources: clients.OpenStackClients.nova('compute').AndReturn(self.fc) nova_sgr.SecurityGroupRuleManager.delete(131).AndReturn(None) clients.OpenStackClients.nova('compute').AndReturn(self.fc) + nova_sgr.SecurityGroupRuleManager.delete(132).AndReturn(None) + clients.OpenStackClients.nova('compute').AndReturn(self.fc) + nova_sgr.SecurityGroupRuleManager.delete(133).AndReturn(None) + clients.OpenStackClients.nova('compute').AndReturn(self.fc) nova_sg.SecurityGroupManager.delete(2).AndReturn(None) self.m.ReplayAll() @@ -197,20 +249,36 @@ Resources: #create script clients.OpenStackClients.nova('compute').AndReturn(self.fc) sg_name = utils.PhysName('test_stack', 'the_sg') - nova_sg.SecurityGroupManager.list().AndReturn([NovaSG( - id=2, - name=sg_name, - description='HTTP and SSH access', - rules=[], - )]) + nova_sg.SecurityGroupManager.list().AndReturn([ + NovaSG( + id=2, + name=sg_name, + description='HTTP and SSH access', + rules=[], + ), + NovaSG( + id=1, + name='test', + description='FAKE_SECURITY_GROUP', + rules=[], + ) + ]) clients.OpenStackClients.nova('compute').AndReturn(self.fc) nova_sgr.SecurityGroupRuleManager.create( - 2, 'tcp', 22, 22, '0.0.0.0/0').AndRaise( + 2, 'tcp', 22, 22, '0.0.0.0/0', None).AndRaise( + clients.novaclient.exceptions.BadRequest( + 400, 'Rule already exists')) + nova_sgr.SecurityGroupRuleManager.create( + 2, 'tcp', 80, 80, '0.0.0.0/0', None).AndReturn( clients.novaclient.exceptions.BadRequest( 400, 'Rule already exists')) nova_sgr.SecurityGroupRuleManager.create( - 2, 'tcp', 80, 80, '0.0.0.0/0').AndReturn( + 2, 'tcp', None, None, None, 1).AndReturn( + clients.novaclient.exceptions.BadRequest( + 400, 'Rule already exists')) + nova_sgr.SecurityGroupRuleManager.create( + 2, 'icmp', None, None, None, 1).AndReturn( clients.novaclient.exceptions.BadRequest( 400, 'Rule already exists')) @@ -240,6 +308,28 @@ Resources: 'cidr': '0.0.0.0/0' }, 'id': 131 + }, { + 'from_port': None, + 'group': { + 'tenant_id': 'f18ca530cc05425e8bac0a5ff92f7e88', + 'name': 'test' + }, + 'ip_protocol': 'tcp', + 'to_port': None, + 'parent_group_id': 2, + 'ip_range': {}, + 'id': 132 + }, { + 'from_port': None, + 'group': { + 'tenant_id': 'f18ca530cc05425e8bac0a5ff92f7e88', + 'name': 'test' + }, + 'ip_protocol': 'icmp', + 'to_port': None, + 'parent_group_id': 2, + 'ip_range': {}, + 'id': 133 }] )) clients.OpenStackClients.nova('compute').AndReturn(self.fc) @@ -249,6 +339,12 @@ Resources: nova_sgr.SecurityGroupRuleManager.delete(131).AndRaise( clients.novaclient.exceptions.NotFound('goneburger')) clients.OpenStackClients.nova('compute').AndReturn(self.fc) + nova_sgr.SecurityGroupRuleManager.delete(132).AndRaise( + clients.novaclient.exceptions.NotFound('goneburger')) + clients.OpenStackClients.nova('compute').AndReturn(self.fc) + nova_sgr.SecurityGroupRuleManager.delete(133).AndRaise( + clients.novaclient.exceptions.NotFound('goneburger')) + clients.OpenStackClients.nova('compute').AndReturn(self.fc) nova_sg.SecurityGroupManager.delete(2).AndReturn(None) clients.OpenStackClients.nova('compute').AndReturn(self.fc) @@ -271,6 +367,13 @@ Resources: self.m.VerifyAll() + def test_security_group_nova_with_egress_rules(self): + t = template_format.parse(self.test_template_nova_with_egress) + stack = self.parse_stack(t) + + sg = stack['the_sg'] + self.assertRaises(exception.EgressRuleNotAllowed, sg.validate) + @utils.stack_delete_after def test_security_group_neutron(self): #create script @@ -287,7 +390,29 @@ Resources: 'tenant_id': 'f18ca530cc05425e8bac0a5ff92f7e88', 'name': sg_name, 'description': 'HTTP and SSH access', - 'security_group_rules': [], + 'security_group_rules': [{ + "direction": "egress", + "ethertype": "IPv4", + "id": "aaaa-1", + "port_range_max": None, + "port_range_min": None, + "protocol": None, + "remote_group_id": None, + "remote_ip_prefix": None, + "security_group_id": "aaaa", + "tenant_id": "f18ca530cc05425e8bac0a5ff92f7e88" + }, { + "direction": "egress", + "ethertype": "IPv6", + "id": "aaaa-2", + "port_range_max": None, + "port_range_min": None, + "protocol": None, + "remote_group_id": None, + "remote_ip_prefix": None, + "security_group_id": "aaaa", + "tenant_id": "f18ca530cc05425e8bac0a5ff92f7e88" + }], 'id': 'aaaa' } }) @@ -295,6 +420,7 @@ Resources: neutronclient.Client.create_security_group_rule({ 'security_group_rule': { 'direction': 'ingress', + 'remote_group_id': None, 'remote_ip_prefix': '0.0.0.0/0', 'port_range_min': 22, 'ethertype': 'IPv4', @@ -305,6 +431,7 @@ Resources: }).AndReturn({ 'security_group_rule': { 'direction': 'ingress', + 'remote_group_id': None, 'remote_ip_prefix': '0.0.0.0/0', 'port_range_min': 22, 'ethertype': 'IPv4', @@ -317,6 +444,7 @@ Resources: neutronclient.Client.create_security_group_rule({ 'security_group_rule': { 'direction': 'ingress', + 'remote_group_id': None, 'remote_ip_prefix': '0.0.0.0/0', 'port_range_min': 80, 'ethertype': 'IPv4', @@ -327,6 +455,7 @@ Resources: }).AndReturn({ 'security_group_rule': { 'direction': 'ingress', + 'remote_group_id': None, 'remote_ip_prefix': '0.0.0.0/0', 'port_range_min': 80, 'ethertype': 'IPv4', @@ -336,9 +465,38 @@ Resources: 'id': 'cccc' } }) + neutronclient.Client.create_security_group_rule({ + 'security_group_rule': { + 'direction': 'ingress', + 'remote_group_id': 'wwww', + 'remote_ip_prefix': None, + 'port_range_min': None, + 'ethertype': 'IPv4', + 'port_range_max': None, + 'protocol': 'tcp', + 'security_group_id': 'aaaa' + } + }).AndReturn({ + 'security_group_rule': { + 'direction': 'ingress', + 'remote_group_id': 'wwww', + 'remote_ip_prefix': None, + 'port_range_min': None, + 'ethertype': 'IPv4', + 'port_range_max': None, + 'protocol': 'tcp', + 'security_group_id': 'aaaa', + 'id': 'dddd' + } + }) + neutronclient.Client.delete_security_group_rule('aaaa-1').AndReturn( + None) + neutronclient.Client.delete_security_group_rule('aaaa-2').AndReturn( + None) neutronclient.Client.create_security_group_rule({ 'security_group_rule': { 'direction': 'egress', + 'remote_group_id': None, 'remote_ip_prefix': '10.0.1.0/24', 'port_range_min': 22, 'ethertype': 'IPv4', @@ -349,13 +507,38 @@ Resources: }).AndReturn({ 'security_group_rule': { 'direction': 'egress', + 'remote_group_id': None, 'remote_ip_prefix': '10.0.1.0/24', 'port_range_min': 22, 'ethertype': 'IPv4', 'port_range_max': 22, 'protocol': 'tcp', 'security_group_id': 'aaaa', - 'id': 'dddd' + 'id': 'eeee' + } + }) + neutronclient.Client.create_security_group_rule({ + 'security_group_rule': { + 'direction': 'egress', + 'remote_group_id': 'xxxx', + 'remote_ip_prefix': None, + 'port_range_min': None, + 'ethertype': 'IPv4', + 'port_range_max': None, + 'protocol': None, + 'security_group_id': 'aaaa' + } + }).AndReturn({ + 'security_group_rule': { + 'direction': 'egress', + 'remote_group_id': 'xxxx', + 'remote_ip_prefix': None, + 'port_range_min': None, + 'ethertype': 'IPv4', + 'port_range_max': None, + 'protocol': None, + 'security_group_id': 'aaaa', + 'id': 'ffff' } }) @@ -372,6 +555,7 @@ Resources: 'id': 'bbbb', 'ethertype': 'IPv4', 'security_group_id': 'aaaa', + 'remote_group_id': None, 'remote_ip_prefix': '0.0.0.0/0', 'tenant_id': 'f18ca530cc05425e8bac0a5ff92f7e88', 'port_range_min': 22 @@ -382,24 +566,50 @@ Resources: 'id': 'cccc', 'ethertype': 'IPv4', 'security_group_id': 'aaaa', + 'remote_group_id': None, 'remote_ip_prefix': '0.0.0.0/0', 'tenant_id': 'f18ca530cc05425e8bac0a5ff92f7e88', 'port_range_min': 80 + }, { + 'direction': 'ingress', + 'protocol': 'tcp', + 'port_range_max': None, + 'id': 'dddd', + 'ethertype': 'IPv4', + 'security_group_id': 'aaaa', + 'remote_group_id': 'wwww', + 'remote_ip_prefix': None, + 'tenant_id': 'f18ca530cc05425e8bac0a5ff92f7e88', + 'port_range_min': None }, { 'direction': 'egress', 'protocol': 'tcp', 'port_range_max': 22, - 'id': 'dddd', + 'id': 'eeee', 'ethertype': 'IPv4', 'security_group_id': 'aaaa', + 'remote_group_id': None, 'remote_ip_prefix': '10.0.1.0/24', 'tenant_id': 'f18ca530cc05425e8bac0a5ff92f7e88', 'port_range_min': 22 + }, { + 'direction': 'egress', + 'protocol': None, + 'port_range_max': None, + 'id': 'ffff', + 'ethertype': 'IPv4', + 'security_group_id': 'aaaa', + 'remote_group_id': None, + 'remote_ip_prefix': None, + 'tenant_id': 'f18ca530cc05425e8bac0a5ff92f7e88', + 'port_range_min': None }], 'id': 'aaaa'}}) neutronclient.Client.delete_security_group_rule('bbbb').AndReturn(None) neutronclient.Client.delete_security_group_rule('cccc').AndReturn(None) neutronclient.Client.delete_security_group_rule('dddd').AndReturn(None) + neutronclient.Client.delete_security_group_rule('eeee').AndReturn(None) + neutronclient.Client.delete_security_group_rule('ffff').AndReturn(None) neutronclient.Client.delete_security_group('aaaa').AndReturn(None) self.m.ReplayAll() @@ -437,6 +647,7 @@ Resources: neutronclient.Client.create_security_group_rule({ 'security_group_rule': { 'direction': 'ingress', + 'remote_group_id': None, 'remote_ip_prefix': '0.0.0.0/0', 'port_range_min': 22, 'ethertype': 'IPv4', @@ -449,6 +660,7 @@ Resources: neutronclient.Client.create_security_group_rule({ 'security_group_rule': { 'direction': 'ingress', + 'remote_group_id': None, 'remote_ip_prefix': '0.0.0.0/0', 'port_range_min': 80, 'ethertype': 'IPv4', @@ -458,9 +670,23 @@ Resources: } }).AndRaise( NeutronClientException(status_code=409)) + neutronclient.Client.create_security_group_rule({ + 'security_group_rule': { + 'direction': 'ingress', + 'remote_group_id': 'wwww', + 'remote_ip_prefix': None, + 'port_range_min': None, + 'ethertype': 'IPv4', + 'port_range_max': None, + 'protocol': 'tcp', + 'security_group_id': 'aaaa' + } + }).AndRaise( + NeutronClientException(status_code=409)) neutronclient.Client.create_security_group_rule({ 'security_group_rule': { 'direction': 'egress', + 'remote_group_id': None, 'remote_ip_prefix': '10.0.1.0/24', 'port_range_min': 22, 'ethertype': 'IPv4', @@ -470,6 +696,19 @@ Resources: } }).AndRaise( NeutronClientException(status_code=409)) + neutronclient.Client.create_security_group_rule({ + 'security_group_rule': { + 'direction': 'egress', + 'remote_group_id': 'xxxx', + 'remote_ip_prefix': None, + 'port_range_min': None, + 'ethertype': 'IPv4', + 'port_range_max': None, + 'protocol': None, + 'security_group_id': 'aaaa' + } + }).AndRaise( + NeutronClientException(status_code=409)) # delete script neutronclient.Client.show_security_group('aaaa').AndReturn({ @@ -484,6 +723,7 @@ Resources: 'id': 'bbbb', 'ethertype': 'IPv4', 'security_group_id': 'aaaa', + 'remote_group_id': None, 'remote_ip_prefix': '0.0.0.0/0', 'tenant_id': 'f18ca530cc05425e8bac0a5ff92f7e88', 'port_range_min': 22 @@ -494,19 +734,43 @@ Resources: 'id': 'cccc', 'ethertype': 'IPv4', 'security_group_id': 'aaaa', + 'remote_group_id': None, 'remote_ip_prefix': '0.0.0.0/0', 'tenant_id': 'f18ca530cc05425e8bac0a5ff92f7e88', 'port_range_min': 80 + }, { + 'direction': 'ingress', + 'protocol': 'tcp', + 'port_range_max': None, + 'id': 'dddd', + 'ethertype': 'IPv4', + 'security_group_id': 'aaaa', + 'remote_group_id': 'wwww', + 'remote_ip_prefix': None, + 'tenant_id': 'f18ca530cc05425e8bac0a5ff92f7e88', + 'port_range_min': None }, { 'direction': 'egress', 'protocol': 'tcp', 'port_range_max': 22, - 'id': 'dddd', + 'id': 'eeee', 'ethertype': 'IPv4', 'security_group_id': 'aaaa', + 'remote_group_id': None, 'remote_ip_prefix': '10.0.1.0/24', 'tenant_id': 'f18ca530cc05425e8bac0a5ff92f7e88', 'port_range_min': 22 + }, { + 'direction': 'egress', + 'protocol': None, + 'port_range_max': None, + 'id': 'ffff', + 'ethertype': 'IPv4', + 'security_group_id': 'aaaa', + 'remote_group_id': None, + 'remote_ip_prefix': None, + 'tenant_id': 'f18ca530cc05425e8bac0a5ff92f7e88', + 'port_range_min': None }], 'id': 'aaaa'}}) neutronclient.Client.delete_security_group_rule('bbbb').AndRaise( @@ -515,6 +779,10 @@ Resources: NeutronClientException(status_code=404)) neutronclient.Client.delete_security_group_rule('dddd').AndRaise( NeutronClientException(status_code=404)) + neutronclient.Client.delete_security_group_rule('eeee').AndRaise( + NeutronClientException(status_code=404)) + neutronclient.Client.delete_security_group_rule('ffff').AndRaise( + NeutronClientException(status_code=404)) neutronclient.Client.delete_security_group('aaaa').AndRaise( NeutronClientException(status_code=404)) diff --git a/heat/tests/test_vpc.py b/heat/tests/test_vpc.py index 9c6246a5..103dc2bb 100644 --- a/heat/tests/test_vpc.py +++ b/heat/tests/test_vpc.py @@ -192,6 +192,7 @@ class VPCTestBase(HeatTestCase): neutronclient.Client.create_security_group_rule({ 'security_group_rule': { 'direction': 'ingress', + 'remote_group_id': None, 'remote_ip_prefix': '0.0.0.0/0', 'port_range_min': 22, 'ethertype': 'IPv4', @@ -202,6 +203,7 @@ class VPCTestBase(HeatTestCase): }).AndReturn({ 'security_group_rule': { 'direction': 'ingress', + 'remote_group_id': None, 'remote_ip_prefix': '0.0.0.0/0', 'port_range_min': 22, 'ethertype': 'IPv4', @@ -226,6 +228,7 @@ class VPCTestBase(HeatTestCase): 'id': 'bbbb', 'ethertype': 'IPv4', 'security_group_id': 'eeee', + 'remote_group_id': None, 'remote_ip_prefix': '0.0.0.0/0', 'tenant_id': 'c1210485b2424d48804aad5d39c61b8f', 'port_range_min': 22