diff --git a/heat/engine/resources/openstack/nova/server.py b/heat/engine/resources/openstack/nova/server.py index 24872f83da..d17f8af779 100644 --- a/heat/engine/resources/openstack/nova/server.py +++ b/heat/engine/resources/openstack/nova/server.py @@ -1411,6 +1411,12 @@ class Server(stack_user.StackUser, sh.SchedulerHintsMixin, props[self.IMAGE] = image_id return defn.freeze(properties=props) + def prepare_for_replace(self): + self.prepare_ports_for_replace() + + def restore_after_rollback(self): + self.restore_ports_after_rollback() + def resource_mapping(): return { diff --git a/heat/engine/resources/openstack/nova/server_network_mixin.py b/heat/engine/resources/openstack/nova/server_network_mixin.py index 8d6046d720..569fd1f632 100644 --- a/heat/engine/resources/openstack/nova/server_network_mixin.py +++ b/heat/engine/resources/openstack/nova/server_network_mixin.py @@ -329,3 +329,45 @@ class ServerNetworkMixin(object): add_nets.append(handler_kwargs) return remove_ports, add_nets + + def prepare_ports_for_replace(self): + data = {'external_ports': [], + 'internal_ports': []} + port_data = itertools.chain( + [('internal_ports', port) for port in self._data_get_ports()], + [('external_ports', port) + for port in self._data_get_ports('external_ports')]) + for port_type, port in port_data: + # store port fixed_ips for restoring after failed update + port_details = self.client('neutron').show_port(port['id'])['port'] + fixed_ips = port_details.get('fixed_ips', []) + data[port_type].append({'id': port['id'], 'fixed_ips': fixed_ips}) + + if data.get('internal_ports'): + self.data_set('internal_ports', + jsonutils.dumps(data['internal_ports'])) + if data.get('external_ports'): + self.data_set('external_ports', + jsonutils.dumps(data['external_ports'])) + # reset fixed_ips for these ports by setting for each of them + # fixed_ips to [] + for port_type, port in port_data: + self.client('neutron').update_port( + port['id'], {'port': {'fixed_ips': []}}) + + def restore_ports_after_rollback(self): + old_server = self.stack._backup_stack().resources.get(self.name) + + port_data = itertools.chain(self._data_get_ports(), + self._data_get_ports('external_ports')) + for port in port_data: + self.client('neutron').update_port(port['id'], + {'port': {'fixed_ips': []}}) + + old_port_data = itertools.chain( + old_server._data_get_ports(), + old_server._data_get_ports('external_ports')) + for port in old_port_data: + fixed_ips = port['fixed_ips'] + self.client('neutron').update_port( + port['id'], {'port': {'fixed_ips': fixed_ips}}) diff --git a/heat/tests/nova/test_server.py b/heat/tests/nova/test_server.py index 11ae6b1cc8..2414f2dc01 100644 --- a/heat/tests/nova/test_server.py +++ b/heat/tests/nova/test_server.py @@ -19,6 +19,7 @@ import mox from neutronclient.neutron import v2_0 as neutronV20 from neutronclient.v2_0 import client as neutronclient from novaclient import exceptions as nova_exceptions +from oslo_serialization import jsonutils from oslo_utils import uuidutils import six from six.moves.urllib import parse as urlparse @@ -1924,6 +1925,7 @@ class ServersTest(common.HeatTestCase): nova.NovaClientPlugin._create().AndReturn(self.fc) self._mock_get_image_id_success('F17-x86_64-gold', 'image_id') self.m.ReplayAll() + self.patchobject(servers.Server, 'prepare_for_replace') tmpl['Resources']['WebServer']['Properties'][ 'flavor_update_policy'] = 'REPLACE' @@ -1944,6 +1946,7 @@ class ServersTest(common.HeatTestCase): self._mock_get_image_id_success('F17-x86_64-gold', 'image_id') self.m.ReplayAll() + self.patchobject(servers.Server, 'prepare_for_replace') resource_defns = tmpl.resource_definitions(stack) server = servers.Server('server_server_update_flavor_replace', resource_defns['WebServer'], stack) @@ -1960,6 +1963,7 @@ class ServersTest(common.HeatTestCase): def test_server_update_image_replace(self): stack_name = 'update_imgrep' (tmpl, stack) = self._setup_test_stack(stack_name) + self.patchobject(servers.Server, 'prepare_for_replace') tmpl.t['Resources']['WebServer']['Properties'][ 'image_update_policy'] = 'REPLACE' @@ -3822,6 +3826,10 @@ class ServerInternalPortTest(common.HeatTestCase): 'create_port') self.port_delete = self.patchobject(neutronclient.Client, 'delete_port') + self.port_show = self.patchobject(neutronclient.Client, + 'show_port') + self.port_update = self.patchobject(neutronclient.Client, + 'update_port') def _return_template_stack_and_rsrc_defn(self, stack_name, temp): templ = template.Template(template_format.parse(temp), @@ -4163,3 +4171,121 @@ class ServerInternalPortTest(common.HeatTestCase): update_data.call_args_list[1][0]) self.assertEqual({'port_type': 'external_ports'}, update_data.call_args_list[1][1]) + + def test_prepare_ports_for_replace(self): + tmpl = """ + heat_template_version: 2015-10-15 + resources: + server: + type: OS::Nova::Server + properties: + flavor: m1.small + image: F17-x86_64-gold + networks: + - network: 4321 + """ + t, stack, server = self._return_template_stack_and_rsrc_defn('test', + tmpl) + port_ids = [{'id': 1122}, {'id': 3344}] + external_port_ids = [{'id': 5566}] + server._data = {"internal_ports": jsonutils.dumps(port_ids), + "external_ports": jsonutils.dumps(external_port_ids)} + data_set = self.patchobject(server, 'data_set') + + port1_fixed_ip = { + 'fixed_ips': { + 'subnet_id': 'test_subnet1', + 'ip_address': '41.41.41.41' + } + } + port2_fixed_ip = { + 'fixed_ips': { + 'subnet_id': 'test_subnet2', + 'ip_address': '42.42.42.42' + } + } + port3_fixed_ip = { + 'fixed_ips': { + 'subnet_id': 'test_subnet3', + 'ip_address': '43.43.43.43' + } + } + self.port_show.side_effect = [{'port': port1_fixed_ip}, + {'port': port2_fixed_ip}, + {'port': port3_fixed_ip}] + + server.prepare_for_replace() + + # check, that data was updated + port_ids[0].update(port1_fixed_ip) + port_ids[1].update(port2_fixed_ip) + external_port_ids[0].update(port3_fixed_ip) + + expected_data = jsonutils.dumps(port_ids) + expected_external_data = jsonutils.dumps(external_port_ids) + data_set.has_calls(('internal_ports', expected_data), + ('external_ports', expected_external_data)) + + # check, that all ip were removed from ports + empty_fixed_ips = {'port': {'fixed_ips': []}} + self.port_update.has_calls((1122, empty_fixed_ips), + (3344, empty_fixed_ips), + (5566, empty_fixed_ips)) + + def test_restore_ports_after_rollback(self): + tmpl = """ + heat_template_version: 2015-10-15 + resources: + server: + type: OS::Nova::Server + properties: + flavor: m1.small + image: F17-x86_64-gold + networks: + - network: 4321 + """ + t, stack, server = self._return_template_stack_and_rsrc_defn('test', + tmpl) + port_ids = [{'id': 1122}, {'id': 3344}] + external_port_ids = [{'id': 5566}] + server._data = {"internal_ports": jsonutils.dumps(port_ids), + "external_ports": jsonutils.dumps(external_port_ids)} + port1_fixed_ip = { + 'fixed_ips': { + 'subnet_id': 'test_subnet1', + 'ip_address': '41.41.41.41' + } + } + port2_fixed_ip = { + 'fixed_ips': { + 'subnet_id': 'test_subnet2', + 'ip_address': '42.42.42.42' + } + } + port3_fixed_ip = { + 'fixed_ips': { + 'subnet_id': 'test_subnet3', + 'ip_address': '43.43.43.43' + } + } + port_ids[0].update(port1_fixed_ip) + port_ids[1].update(port2_fixed_ip) + external_port_ids[0].update(port3_fixed_ip) + # add data to old server in backup stack + old_server = mock.Mock() + stack._backup_stack = mock.Mock() + stack._backup_stack().resources.get.return_value = old_server + old_server._data_get_ports.side_effect = [port_ids, []] + + server.restore_after_rollback() + + # check, that all ip were removed from new_ports + empty_fixed_ips = {'port': {'fixed_ips': []}} + self.port_update.has_calls((1122, empty_fixed_ips), + (3344, empty_fixed_ips), + (5566, empty_fixed_ips)) + + # check, that all ip were restored for old_ports + self.port_update.has_calls((1122, {'port': port1_fixed_ip}), + (3344, {'port': port2_fixed_ip}), + (5566, {'port': port3_fixed_ip}))