diff --git a/heat/engine/resources/network_interface.py b/heat/engine/resources/network_interface.py index 4618e84dc..8e934ec7e 100644 --- a/heat/engine/resources/network_interface.py +++ b/heat/engine/resources/network_interface.py @@ -46,7 +46,7 @@ class NetworkInterface(resource.Resource): GROUP_SET: properties.Schema( properties.Schema.LIST, _('List of security group IDs associated with this interface.'), - default=[] + update_allowed=True ), PRIVATE_IP_ADDRESS: properties.Schema( properties.Schema.STRING @@ -115,8 +115,11 @@ class NetworkInterface(resource.Resource): 'network_id': network_id, 'fixed_ips': [fixed_ip] } - - if self.properties[self.GROUP_SET]: + # if without group_set, don't set the 'security_groups' property, + # neutron will create the port with the 'default' securityGroup, + # if has the group_set and the value is [], which means to create the + # port without securityGroup(same as the behavior of neutron) + if self.properties[self.GROUP_SET] is not None: sgs = self.client_plugin().get_secgroup_uuids( self.properties.get(self.GROUP_SET)) props['security_groups'] = sgs @@ -124,12 +127,34 @@ class NetworkInterface(resource.Resource): self.resource_id_set(port['id']) def handle_delete(self): + if self.resource_id is None: + return + client = self.neutron() try: client.delete_port(self.resource_id) except Exception as ex: self.client_plugin().ignore_not_found(ex) + def handle_update(self, json_snippet, tmpl_diff, prop_diff): + if prop_diff: + update_props = {} + if self.GROUP_SET in prop_diff: + group_set = prop_diff.get(self.GROUP_SET) + # update should keep the same behavior as creation, + # if without the GroupSet in update template, we should + # update the security_groups property to referent + # the 'default' security group + if group_set is not None: + sgs = self.client_plugin().get_secgroup_uuids(group_set) + else: + sgs = self.client_plugin().get_secgroup_uuids(['default']) + + update_props['security_groups'] = sgs + + self.neutron().update_port(self.resource_id, + {'port': update_props}) + def _get_fixed_ip_address(self, ): if self.fixed_ip_address is None: client = self.neutron() diff --git a/heat/tests/test_network_interface.py b/heat/tests/test_network_interface.py new file mode 100644 index 000000000..83b72e69a --- /dev/null +++ b/heat/tests/test_network_interface.py @@ -0,0 +1,145 @@ +# +# 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 copy + +from heat.engine import rsrc_defn +from heat.engine import scheduler +from heat.tests.common import HeatTestCase +from heat.tests import utils + +try: + from neutronclient.v2_0 import client as neutronclient +except ImportError: + neutronclient = None + +test_template = { + 'heat_template_version': '2013-05-23', + 'resources': { + 'my_nic': { + 'type': 'AWS::EC2::NetworkInterface', + 'properties': { + 'SubnetId': 'ssss' + } + } + } +} + + +class NetworkInterfaceTest(HeatTestCase): + def setUp(self): + super(NetworkInterfaceTest, self).setUp() + self.ctx = utils.dummy_context() + self.m.StubOutWithMock(neutronclient.Client, 'show_subnet') + self.m.StubOutWithMock(neutronclient.Client, 'create_port') + self.m.StubOutWithMock(neutronclient.Client, 'delete_port') + self.m.StubOutWithMock(neutronclient.Client, 'update_port') + self.stub_keystoneclient() + + def mock_show_subnet(self): + neutronclient.Client.show_subnet('ssss').AndReturn({ + 'subnet': { + 'name': 'my_subnet', + 'network_id': 'nnnn', + 'tenant_id': 'c1210485b2424d48804aad5d39c61b8f', + 'allocation_pools': [{'start': '10.0.0.2', + 'end': '10.0.0.254'}], + 'gateway_ip': '10.0.0.1', + 'ip_version': 4, + 'cidr': '10.0.0.0/24', + 'id': 'ssss', + 'enable_dhcp': False, + }}) + + def mock_create_network_interface(self, stack_name='my_stack', + resource_name='my_nic', + security_groups=None): + self.nic_name = utils.PhysName(stack_name, resource_name) + port = {'network_id': 'nnnn', + 'fixed_ips': [{ + 'subnet_id': u'ssss' + }], + 'name': self.nic_name, + 'admin_state_up': True} + + port_info = { + 'port': { + 'admin_state_up': True, + 'device_id': '', + 'device_owner': '', + 'fixed_ips': [ + { + 'ip_address': '10.0.0.100', + 'subnet_id': 'ssss' + } + ], + 'id': 'pppp', + 'mac_address': 'fa:16:3e:25:32:5d', + 'name': self.nic_name, + 'network_id': 'nnnn', + 'status': 'ACTIVE', + 'tenant_id': 'c1210485b2424d48804aad5d39c61b8f' + } + } + + if security_groups is not None: + port['security_groups'] = security_groups + port_info['security_groups'] = security_groups + else: + port_info['security_groups'] = ['default'] + + neutronclient.Client.create_port({'port': port}).AndReturn(port_info) + + def mock_update_network_interface(self, update_props, port_id='pppp'): + neutronclient.Client.update_port( + port_id, + {'port': update_props}).AndReturn(None) + + def mock_delete_network_interface(self, port_id='pppp'): + neutronclient.Client.delete_port(port_id).AndReturn(None) + + def test_network_interface_create_update_delete(self): + my_stack = utils.parse_stack(test_template, stack_name='my_stack') + nic_rsrc = my_stack['my_nic'] + + self.mock_show_subnet() + self.mock_create_network_interface() + + update_props = {} + update_sg_ids = ['0389f747-7785-4757-b7bb-2ab07e4b09c3'] + update_props['security_groups'] = update_sg_ids + + self.mock_update_network_interface(update_props) + self.mock_delete_network_interface() + + self.m.ReplayAll() + # create the nic without GroupSet + self.assertIsNone(nic_rsrc.validate()) + scheduler.TaskRunner(nic_rsrc.create)() + self.assertEqual((nic_rsrc.CREATE, my_stack.COMPLETE), + nic_rsrc.state) + + # update the nic with GroupSet + props = copy.deepcopy(nic_rsrc.properties.data) + props['GroupSet'] = update_sg_ids + update_snippet = rsrc_defn.ResourceDefinition(nic_rsrc.name, + nic_rsrc.type(), + props) + scheduler.TaskRunner(nic_rsrc.update, update_snippet)() + self.assertEqual((nic_rsrc.UPDATE, nic_rsrc.COMPLETE), nic_rsrc.state) + + # delete the nic + scheduler.TaskRunner(nic_rsrc.delete)() + self.assertEqual((nic_rsrc.DELETE, nic_rsrc.COMPLETE), nic_rsrc.state) + + self.m.VerifyAll()