Default port policy to force replacement

This change adds a replacement_policy property to OS::Neutron::Port
which defaults to always replacing the port on stack-update
regardless of any changes to the template.

Currently when a server is replaced, the new server is booted with
the port that is still attached to the old server, which raises a
port-still-in-use error.

Even if heat managed to detach and attach a single port, Nova currently
deletes all ports on interface-detach and server delete, so the port
would no longer be available anyway.

This change ensures that a new server is always booted with a new port,
so it fixes the above 2 issues, however there are implications for
other scenarios.

If the server is not replaced during the stack-update, the server
handle_update will detach/attach to the new port, so this is fine.

If the port specifies a fixed_ips ip_address which doesn't change
during stack-update, an error will be raised that 2 ports exist
with the same IP address. The only current workaround would be
to set update_policy:AUTO and not make any changes to the server
which results in server replacement (or do 2 stack updates using
a transition ip_address).

Likewise, update_policy:AUTO will need to be set on any port
consumed by resources which don't support update without replacement
(such as OS::Neutron::FloatingIP and OS::Trove::Instance)

Change-Id: I558db6bac196f49e5c488a577f0580c934b06747
Closes-Bug: #1301486
Related-Bug: #1158684
Related-Bug: #1369748
This commit is contained in:
Steve Baker 2014-09-15 16:37:29 +12:00
parent dc0ba2ac54
commit 30ece56a21
2 changed files with 93 additions and 10 deletions

View File

@ -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})

View File

@ -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': [],