diff --git a/heat/engine/resources/openstack/neutron/port.py b/heat/engine/resources/openstack/neutron/port.py index 11b1dfac1b..c759637cf1 100644 --- a/heat/engine/resources/openstack/neutron/port.py +++ b/heat/engine/resources/openstack/neutron/port.py @@ -12,6 +12,7 @@ # under the License. from oslo_log import log as logging +from oslo_serialization import jsonutils import six from heat.common import exception @@ -432,6 +433,25 @@ class Port(neutron.NeutronResource): attributes = self._show_resource() return self.is_built(attributes) + def prepare_for_replace(self): + # store port fixed_ips for restoring after failed update + fixed_ips = self._show_resource().get('fixed_ips', []) + self.data_set('port_fip', jsonutils.dumps(fixed_ips)) + # reset fixed_ips for this port by setting fixed_ips to [] + props = {'fixed_ips': []} + self.client().update_port(self.resource_id, {'port': props}) + + def restore_after_rollback(self): + old_port = self.stack._backup_stack().resources.get(self.name) + fixed_ips = old_port.data().get('port_fip', []) + # restore fixed_ips for this port by setting fixed_ips to [] + props = {'fixed_ips': []} + old_props = {'fixed_ips': jsonutils.loads(fixed_ips)} + # remove ip from new port + self.client().update_port(self.resource_id, {'port': props}) + # restore ip for old port + self.client().update_port(old_port.resource_id, {'port': old_props}) + def resource_mapping(): return { diff --git a/heat/tests/neutron/test_neutron_port.py b/heat/tests/neutron/test_neutron_port.py index 940c1b133d..60080a9a33 100644 --- a/heat/tests/neutron/test_neutron_port.py +++ b/heat/tests/neutron/test_neutron_port.py @@ -12,10 +12,12 @@ # under the License. import copy +import mock import mox from neutronclient.common import exceptions as qe from neutronclient.neutron import v2_0 as neutronV20 from neutronclient.v2_0 import client as neutronclient +from oslo_serialization import jsonutils from heat.common import exception from heat.common import template_format @@ -717,3 +719,61 @@ class NeutronPortTest(common.HeatTestCase): self.assertEqual('direct', port.properties['binding:vnic_type']) self.m.VerifyAll() + + def test_prepare_for_replace_port(self): + t = template_format.parse(neutron_port_template) + stack = utils.parse_stack(t) + port = stack['port'] + port.resource_id = 'test_res_id' + _value = { + 'fixed_ips': { + 'subnet_id': 'test_subnet', + 'ip_address': '42.42.42.42' + } + } + port._show_resource = mock.Mock(return_value=_value) + port.data_set = mock.Mock() + n_client = mock.Mock() + port.client = mock.Mock(return_value=n_client) + + # execute prepare_for_replace + port.prepare_for_replace() + + # check, that data was stored + port.data_set.assert_called_once_with( + 'port_fip', jsonutils.dumps(_value.get('fixed_ips'))) + + # check, that port was updated and ip was removed + expected_props = {'port': {'fixed_ips': []}} + n_client.update_port.assert_called_once_with('test_res_id', + expected_props) + + def test_restore_after_rollback_port(self): + t = template_format.parse(neutron_port_template) + stack = utils.parse_stack(t) + new_port = stack['port'] + new_port.resource_id = 'new_res_id' + # mock backup stack to return only one mocked old_port + old_port = mock.Mock() + new_port.stack._backup_stack = mock.Mock() + new_port.stack._backup_stack().resources.get.return_value = old_port + old_port.resource_id = 'old_res_id' + _value = { + 'subnet_id': 'test_subnet', + 'ip_address': '42.42.42.42' + } + old_port.data = mock.Mock( + return_value={'port_fip': jsonutils.dumps(_value)}) + + n_client = mock.Mock() + new_port.client = mock.Mock(return_value=n_client) + + # execute prepare_for_replace + new_port.restore_after_rollback() + + # check, that ports were updated: old port get ip and + # same ip was removed from old port + expected_new_props = {'port': {'fixed_ips': []}} + expected_old_props = {'port': {'fixed_ips': _value}} + n_client.update_port.has_calls(('new_res_id', expected_new_props), + ('old_res_id', expected_old_props)) diff --git a/heat_integrationtests/functional/test_create_update_neutron_port.py b/heat_integrationtests/functional/test_create_update_neutron_port.py index 4b2df59788..575d21cf66 100644 --- a/heat_integrationtests/functional/test_create_update_neutron_port.py +++ b/heat_integrationtests/functional/test_create_update_neutron_port.py @@ -10,8 +10,6 @@ # License for the specific language governing permissions and limitations # under the License. -from testtools import testcase - from heat_integrationtests.functional import functional_base @@ -38,6 +36,12 @@ resources: fixed_ips: - subnet: {get_resource: subnet} ip_address: 11.11.11.11 + test: + depends_on: port + type: OS::Heat::TestResource + properties: + value: Test1 + fail: False outputs: port_ip: value: {get_attr: [port, fixed_ips, 0, ip_address]} @@ -73,7 +77,6 @@ class UpdatePortTest(functional_base.FunctionalTestsBase): self.assertNotEqual(_ip, new_ip) self.assertNotEqual(_id, new_id) - @testcase.skip('Skipped until bug #1455100 is fixed.') def test_stack_update_replace_with_ip(self): # create with default 'mac' parameter stack_identifier = self.stack_create(template=test_template) @@ -92,6 +95,62 @@ class UpdatePortTest(functional_base.FunctionalTestsBase): self.assertEqual(_ip, new_ip) self.assertNotEqual(_id, new_id) + def test_stack_update_replace_with_ip_rollback(self): + # create with default 'mac' parameter + stack_identifier = self.stack_create(template=test_template) + + _id, _ip = self.get_port_id_and_ip(stack_identifier) + + # Update with another 'mac' parameter + parameters = {'mac': '00-00-00-00-AA-AA'} + + # make test resource failing during update + fail_template = test_template.replace('fail: False', + 'fail: True') + fail_template = fail_template.replace('value: Test1', + 'value: Rollback') + + # port should be replaced with same ip + self.update_stack(stack_identifier, fail_template, + parameters=parameters, + expected_status='ROLLBACK_COMPLETE', + disable_rollback=False) + + new_id, new_ip = self.get_port_id_and_ip(stack_identifier) + # port id and ip should be the same after rollback + self.assertEqual(_ip, new_ip) + self.assertEqual(_id, new_id) + + def test_stack_update_replace_with_ip_after_failed_update(self): + # create with default 'mac' parameter + stack_identifier = self.stack_create(template=test_template) + + _id, _ip = self.get_port_id_and_ip(stack_identifier) + + # Update with another 'mac' parameter + parameters = {'mac': '00-00-00-00-AA-AA'} + + # make test resource failing during update + fail_template = test_template.replace('fail: False', + 'fail: True') + fail_template = fail_template.replace('value: Test1', + 'value: Rollback') + + # port should be replaced with same ip + self.update_stack(stack_identifier, fail_template, + parameters=parameters, + expected_status='UPDATE_FAILED') + + # port should be replaced with same ip + self.update_stack(stack_identifier, test_template, + parameters=parameters) + + new_id, new_ip = self.get_port_id_and_ip(stack_identifier) + # ip should be the same, but port id should be different, because it's + # restore replace + self.assertEqual(_ip, new_ip) + self.assertNotEqual(_id, new_id) + def test_stack_update_in_place_remove_ip(self): # create with default 'mac' parameter and defined ip_address stack_identifier = self.stack_create(template=test_template)