From 8b22a11fc6fa56d36295da6f5a5c386d6a301c0a Mon Sep 17 00:00:00 2001 From: Sergey Kraynev Date: Mon, 7 Sep 2015 10:57:22 -0400 Subject: [PATCH] Handle replace and rollback cases for Server Add support for handling replace case and rollback for Server resource with internal port. This patch adds ability to replace server with specified ip address and ability to make correct rollback if updated replace failed. Logic is similar on code introduced for Port resource, but here we extend existing resource data - "internal ports", by adding new key 'fixed_ips' in dictinary with port 'id'. implements bp rich-network-prop Change-Id: Id44bb358c9c874afa6644f126d284f31b6639b63 --- .../engine/resources/openstack/nova/server.py | 6 + .../openstack/nova/server_network_mixin.py | 42 ++++++ heat/tests/nova/test_server.py | 126 ++++++++++++++++++ 3 files changed, 174 insertions(+) diff --git a/heat/engine/resources/openstack/nova/server.py b/heat/engine/resources/openstack/nova/server.py index 83567e9153..8dcadabecf 100644 --- a/heat/engine/resources/openstack/nova/server.py +++ b/heat/engine/resources/openstack/nova/server.py @@ -1409,6 +1409,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}))