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:
parent
dc0ba2ac54
commit
30ece56a21
|
@ -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})
|
||||
|
||||
|
|
|
@ -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': [],
|
||||
|
|
Loading…
Reference in New Issue