diff --git a/heat/engine/resources/neutron/port.py b/heat/engine/resources/neutron/port.py index d9fe74092c..4fc887ee0a 100644 --- a/heat/engine/resources/neutron/port.py +++ b/heat/engine/resources/neutron/port.py @@ -12,7 +12,9 @@ # under the License. from heat.engine import attributes +from heat.engine import constraints from heat.engine import properties +from heat.engine import resource from heat.engine.resources.neutron import neutron from heat.engine.resources.neutron import subnet from heat.engine import support @@ -27,12 +29,12 @@ class Port(neutron.NeutronResource): NETWORK_ID, NETWORK, NAME, VALUE_SPECS, ADMIN_STATE_UP, FIXED_IPS, MAC_ADDRESS, DEVICE_ID, SECURITY_GROUPS, ALLOWED_ADDRESS_PAIRS, - DEVICE_OWNER, + DEVICE_OWNER, REPLACEMENT_POLICY, ) = ( 'network_id', 'network', 'name', 'value_specs', 'admin_state_up', 'fixed_ips', 'mac_address', 'device_id', 'security_groups', 'allowed_address_pairs', - 'device_owner', + 'device_owner', 'replacement_policy', ) _FIXED_IP_KEYS = ( @@ -154,6 +156,18 @@ class Port(neutron.NeutronResource): 'or network:router_interface or network:dhcp'), update_allowed=True ), + REPLACEMENT_POLICY: properties.Schema( + properties.Schema.STRING, + _('Policy on how to respond to a stack-update for this resource. ' + 'REPLACE_ALWAYS will replace the port regardless of any ' + 'property changes. AUTO will update the existing port for any ' + 'changed update-allowed property.'), + default='REPLACE_ALWAYS', + constraints=[ + constraints.AllowedValues(['REPLACE_ALWAYS', 'AUTO']), + ], + update_allowed=True + ), } attributes_schema = { @@ -211,27 +225,27 @@ class Port(neutron.NeutronResource): # It is not known which subnet a port might be assigned # to so all subnets in a network should be created before # the ports in that network. - for resource in self.stack.itervalues(): - if resource.has_interface('OS::Neutron::Subnet'): - dep_network = resource.properties.get( - subnet.Subnet.NETWORK) or resource.properties.get( + for res in self.stack.itervalues(): + if res.has_interface('OS::Neutron::Subnet'): + dep_network = res.properties.get( + subnet.Subnet.NETWORK) or res.properties.get( subnet.Subnet.NETWORK_ID) network = self.properties.get( self.NETWORK) or self.properties.get(self.NETWORK_ID) if dep_network == network: - deps += (self, resource) + deps += (self, res) def handle_create(self): props = self.prepare_properties( self.properties, self.physical_resource_name()) self.client_plugin().resolve_network(props, self.NETWORK, 'network_id') - self._prepare_list_properties(props) + self._prepare_port_properties(props) port = self.neutron().create_port({'port': props})['port'] self.resource_id_set(port['id']) - def _prepare_list_properties(self, props): + def _prepare_port_properties(self, props): for fixed_ip in props.get(self.FIXED_IPS, []): for key, value in fixed_ip.items(): if value is None: @@ -255,6 +269,8 @@ class Port(neutron.NeutronResource): if not props[self.FIXED_IPS]: del(props[self.FIXED_IPS]) + del(props[self.REPLACEMENT_POLICY]) + def _show_resource(self): return self.neutron().show_port( self.resource_id)['port'] @@ -288,10 +304,19 @@ class Port(neutron.NeutronResource): return subnets return super(Port, self)._resolve_attribute(name) + def _needs_update(self, after, before, after_props, before_props, + prev_resource): + + if after_props.get(self.REPLACEMENT_POLICY) == 'REPLACE_ALWAYS': + raise resource.UpdateReplace(self.name) + + return super(Port, self)._needs_update( + after, before, after_props, before_props, prev_resource) + def handle_update(self, json_snippet, tmpl_diff, prop_diff): props = self.prepare_update_properties(json_snippet) - self._prepare_list_properties(props) + self._prepare_port_properties(props) LOG.debug('updating port with %s' % props) self.neutron().update_port(self.resource_id, {'port': props}) diff --git a/heat/tests/test_neutron.py b/heat/tests/test_neutron.py index 53235c69ae..ffd3dbc60b 100644 --- a/heat/tests/test_neutron.py +++ b/heat/tests/test_neutron.py @@ -2533,6 +2533,64 @@ class NeutronPortTest(HeatTestCase): self.m.VerifyAll() + def test_port_needs_update(self): + props = {'network_id': u'net1234', + 'name': utils.PhysName('test_stack', 'port'), + 'admin_state_up': True, + 'device_owner': u'network:dhcp'} + + neutronV20.find_resourceid_by_name_or_id( + mox.IsA(neutronclient.Client), + 'network', + 'net1234' + ).AndReturn('net1234') + neutronclient.Client.create_port( + {'port': props} + ).AndReturn({'port': { + "status": "BUILD", + "id": "fc68ea2c-b60b-4b4f-bd82-94ec81110766" + }}) + neutronclient.Client.show_port( + 'fc68ea2c-b60b-4b4f-bd82-94ec81110766' + ).AndReturn({'port': { + "status": "ACTIVE", + "id": "fc68ea2c-b60b-4b4f-bd82-94ec81110766", + "fixed_ips": { + "subnet_id": "d0e971a6-a6b4-4f4c-8c88-b75e9c120b7e", + "ip_address": "10.0.0.2" + } + }}) + + self.m.ReplayAll() + + # create port + t = template_format.parse(neutron_port_template) + t['Resources']['port']['Properties'].pop('fixed_ips') + stack = utils.parse_stack(t) + + port = stack['port'] + scheduler.TaskRunner(port.create)() + + new_props = props.copy() + + # test always replace + new_props['replacement_policy'] = 'REPLACE_ALWAYS' + update_snippet = rsrc_defn.ResourceDefinition(port.name, port.type(), + new_props) + self.assertRaises(resource.UpdateReplace, port._needs_update, + update_snippet, port.frozen_definition(), + new_props, props, None) + + # test deferring to Resource._needs_update + new_props['replacement_policy'] = 'AUTO' + update_snippet = rsrc_defn.ResourceDefinition(port.name, port.type(), + new_props) + self.assertTrue(port._needs_update(update_snippet, + port.frozen_definition(), + new_props, props, None)) + + self.m.VerifyAll() + def test_get_port_attributes(self): subnet_dict = {'name': 'test-subnet', 'enable_dhcp': True, 'network_id': 'net1234', 'dns_nameservers': [],