summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhuangtianhua <huangtianhua@huawei.com>2016-01-13 14:17:57 +0800
committerSteve Baker <sbaker@redhat.com>2016-09-21 03:51:40 +0000
commitd83ab28e32da1c17ffc05fe3d10369c91a5e5cdb (patch)
tree6537eac849b2a5502616cb7b085122631cb053a9
parentf32dbe7838c9cf9b935247a145bd55e806ea44b2 (diff)
downloadheat-d83ab28e32da1c17ffc05fe3d10369c91a5e5cdb.tar.gz
Fix prepare_for_replace/restore_prev_rsrc handing for server
Now, we set 'fixed_ips' to [] for server ports when prepare for server replacement, but the ports are still in-use if only set 'fixed_ips' to []. So this patch will to detach the ports from nova server to make sure same ports can be attached to new one in prepare_for_replace(). Also, when restoring server, we need to detach ports from existing server, and then to attach them to previous server. We check the interface attach/detach complete by list the server.interfaces, this change will use 'retry' wrapper to re-poll the server interfaces ten times, then will raise exception if the attach/detach still not complete. Closes-Bug: #1533076 (cherry picked from commit 163d46bdc8bbfa2e7da2989f5a5d608826de2dcc) Change-Id: I7b322f9cf16c100dcd0365bc3091c289f00f0548
-rw-r--r--heat/common/exception.py10
-rw-r--r--heat/engine/clients/client_plugin.py4
-rw-r--r--heat/engine/clients/os/nova.py25
-rw-r--r--heat/engine/resources/openstack/nova/server_network_mixin.py91
-rw-r--r--heat/tests/nova/test_server.py203
5 files changed, 203 insertions, 130 deletions
diff --git a/heat/common/exception.py b/heat/common/exception.py
index ee91d995b..026e19aaa 100644
--- a/heat/common/exception.py
+++ b/heat/common/exception.py
@@ -479,6 +479,16 @@ class ServiceNotFound(HeatException):
msg_fmt = _("Service %(service_id)s not found")
+class InterfaceAttachFailed(HeatException):
+ msg_fmt = _("Failed to attach interface (%(port)s) "
+ "to server (%(server)s)")
+
+
+class InterfaceDetachFailed(HeatException):
+ msg_fmt = _("Failed to detach interface (%(port)s) "
+ "from server (%(server)s)")
+
+
class UnsupportedObjectError(HeatException):
msg_fmt = _('Unsupported object type %(objtype)s')
diff --git a/heat/engine/clients/client_plugin.py b/heat/engine/clients/client_plugin.py
index 4b0306ceb..e8c15a707 100644
--- a/heat/engine/clients/client_plugin.py
+++ b/heat/engine/clients/client_plugin.py
@@ -228,3 +228,7 @@ class ClientPlugin(object):
return True
except exceptions.EndpointNotFound:
return False
+
+
+def retry_if_result_is_false(result):
+ return result is False
diff --git a/heat/engine/clients/os/nova.py b/heat/engine/clients/os/nova.py
index a83fa4347..a778c152b 100644
--- a/heat/engine/clients/os/nova.py
+++ b/heat/engine/clients/os/nova.py
@@ -25,6 +25,7 @@ from novaclient import exceptions
from oslo_config import cfg
from oslo_serialization import jsonutils
from oslo_utils import uuidutils
+from retrying import retry
import six
from six.moves.urllib import parse as urlparse
@@ -656,6 +657,30 @@ echo -e '%s\tALL=(ALL)\tNOPASSWD: ALL' >> /etc/sudoers
else:
return False
+ @retry(stop_max_attempt_number=10,
+ wait_fixed=500,
+ retry_on_result=client_plugin.retry_if_result_is_false)
+ def check_interface_detach(self, server_id, port_id):
+ server = self.fetch_server(server_id)
+ if server:
+ interfaces = server.interface_list()
+ for iface in interfaces:
+ if iface.port_id == port_id:
+ return False
+ return True
+
+ @retry(stop_max_attempt_number=10,
+ wait_fixed=500,
+ retry_on_result=client_plugin.retry_if_result_is_false)
+ def check_interface_attach(self, server_id, port_id):
+ server = self.fetch_server(server_id)
+ if server:
+ interfaces = server.interface_list()
+ for iface in interfaces:
+ if iface.port_id == port_id:
+ return True
+ return False
+
def has_extension(self, alias):
"""Check if extension is present."""
extensions = self.client().list_extensions.show_all()
diff --git a/heat/engine/resources/openstack/nova/server_network_mixin.py b/heat/engine/resources/openstack/nova/server_network_mixin.py
index 49af5abc2..fe1693d62 100644
--- a/heat/engine/resources/openstack/nova/server_network_mixin.py
+++ b/heat/engine/resources/openstack/nova/server_network_mixin.py
@@ -16,6 +16,7 @@ import itertools
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_utils import netutils
+import retrying
from heat.common import exception
from heat.common.i18n import _
@@ -190,7 +191,6 @@ class ServerNetworkMixin(object):
def _build_nics(self, networks, security_groups=None):
if not networks:
return None
-
nics = []
for idx, net in enumerate(networks):
@@ -355,39 +355,72 @@ class ServerNetworkMixin(object):
[('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 []
+ data[port_type].append({'id': port['id']})
+
+ # detach the ports from the server
+ server_id = self.resource_id
for port_type, port in port_data:
- self.client('neutron').update_port(
- port['id'], {'port': {'fixed_ips': []}})
+ self.client_plugin().interface_detach(server_id, port['id'])
+ try:
+ if self.client_plugin().check_interface_detach(
+ server_id, port['id']):
+ LOG.info(_LI('Detach interface %(port)s successful '
+ 'from server %(server)s when prepare '
+ 'for replace.')
+ % {'port': port['id'],
+ 'server': server_id})
+ except retrying.RetryError:
+ raise exception.InterfaceDetachFailed(
+ port=port['id'], server=server_id)
def restore_ports_after_rollback(self):
if not self.is_using_neutron():
return
- old_server = self.stack._backup_stack().resources.get(self.name)
+ backup_res = self.stack._backup_stack().resources.get(self.name)
+ prev_server = backup_res
+ existing_server = self
+
+ port_data = itertools.chain(
+ existing_server._data_get_ports(),
+ existing_server._data_get_ports('external_ports')
+ )
- port_data = itertools.chain(self._data_get_ports(),
- self._data_get_ports('external_ports'))
+ existing_server_id = existing_server.resource_id
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}})
+ # detach the ports from current resource
+ self.client_plugin().interface_detach(
+ existing_server_id, port['id'])
+ try:
+ if self.client_plugin().check_interface_detach(
+ existing_server_id, port['id']):
+ LOG.info(_LI('Detach interface %(port)s successful from '
+ 'server %(server)s when restore after '
+ 'rollback.')
+ % {'port': port['id'],
+ 'server': existing_server_id})
+ except retrying.RetryError:
+ raise exception.InterfaceDetachFailed(
+ port=port['id'], server=existing_server_id)
+
+ # attach the ports for old resource
+ prev_port_data = itertools.chain(
+ prev_server._data_get_ports(),
+ prev_server._data_get_ports('external_ports'))
+
+ prev_server_id = prev_server.resource_id
+
+ for port in prev_port_data:
+ self.client_plugin().interface_attach(prev_server_id,
+ port['id'])
+ try:
+ if self.client_plugin().check_interface_attach(
+ prev_server_id, port['id']):
+ LOG.info(_LI('Attach interface %(port)s successful to '
+ 'server %(server)s when restore after '
+ 'rollback.')
+ % {'port': port['id'],
+ 'server': prev_server_id})
+ except retrying.RetryError:
+ raise exception.InterfaceAttachFailed(
+ port=port['id'], server=prev_server_id)
diff --git a/heat/tests/nova/test_server.py b/heat/tests/nova/test_server.py
index a123e5a34..23eda8e08 100644
--- a/heat/tests/nova/test_server.py
+++ b/heat/tests/nova/test_server.py
@@ -113,6 +113,18 @@ resources:
network: 12345
'''
+tmpl_server_with_network_id = """
+heat_template_version: 2015-10-15
+resources:
+ server:
+ type: OS::Nova::Server
+ properties:
+ flavor: m1.small
+ image: F17-x86_64-gold
+ networks:
+ - network: 4321
+"""
+
tmpl_server_with_sub_secu_group = """
heat_template_version: 2015-10-15
resources:
@@ -4087,8 +4099,6 @@ class ServerInternalPortTest(common.HeatTestCase):
'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),
@@ -4444,127 +4454,118 @@ class ServerInternalPortTest(common.HeatTestCase):
self.assertEqual({'port_type': 'external_ports'},
update_data.call_args_list[1][1])
+ def test_prepare_ports_for_replace_detach_failed(self):
+ t, stack, server = self._return_template_stack_and_rsrc_defn(
+ 'test', tmpl_server_with_network_id)
+
+ class Fake(object):
+ def interface_list(self):
+ return [iface(1122)]
+ iface = collections.namedtuple('iface', ['port_id'])
+
+ server.resource_id = 'ser-11'
+ port_ids = [{'id': 1122}]
+
+ server._data = {"internal_ports": jsonutils.dumps(port_ids)}
+ self.patchobject(nova.NovaClientPlugin, 'interface_detach')
+ self.patchobject(nova.NovaClientPlugin, 'fetch_server')
+ nova.NovaClientPlugin.fetch_server.side_effect = [Fake()] * 10
+
+ exc = self.assertRaises(exception.InterfaceDetachFailed,
+ server.prepare_for_replace)
+ self.assertIn('Failed to detach interface (1122) from server '
+ '(ser-11)',
+ six.text_type(exc))
+
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)
+ t, stack, server = self._return_template_stack_and_rsrc_defn(
+ 'test', tmpl_server_with_network_id)
+ server.resource_id = 'test_server'
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}]
+ self.patchobject(nova.NovaClientPlugin, 'interface_detach')
+ self.patchobject(nova.NovaClientPlugin, 'check_interface_detach',
+ return_value=True)
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.assert_has_calls([
- mock.call('internal_ports', expected_data),
- mock.call('external_ports', expected_external_data)])
-
- # check, that all ip were removed from ports
- empty_fixed_ips = {'port': {'fixed_ips': []}}
- self.port_update.assert_has_calls([
- mock.call(1122, empty_fixed_ips),
- mock.call(3344, empty_fixed_ips),
- mock.call(5566, empty_fixed_ips)])
+ # check, that the ports were detached from server
+ nova.NovaClientPlugin.interface_detach.assert_has_calls([
+ mock.call('test_server', 1122),
+ mock.call('test_server', 3344),
+ mock.call('test_server', 5566)])
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)
+ t, stack, server = self._return_template_stack_and_rsrc_defn(
+ 'test', tmpl_server_with_network_id)
+ server.resource_id = 'existing_server'
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()
+ old_server.resource_id = 'old_server'
stack._backup_stack = mock.Mock()
stack._backup_stack().resources.get.return_value = old_server
old_server._data_get_ports.side_effect = [port_ids, external_port_ids]
+ self.patchobject(nova.NovaClientPlugin, 'interface_detach')
+ self.patchobject(nova.NovaClientPlugin, 'check_interface_detach',
+ return_value=True)
+ self.patchobject(nova.NovaClientPlugin, 'interface_attach')
+ self.patchobject(nova.NovaClientPlugin, 'check_interface_attach',
+ return_value=True)
+
server.restore_after_rollback()
- # check, that all ip were removed from new_ports
- empty_fixed_ips = {'port': {'fixed_ips': []}}
- self.port_update.assert_has_calls([
- mock.call(1122, empty_fixed_ips),
- mock.call(3344, empty_fixed_ips),
- mock.call(5566, empty_fixed_ips)])
-
- # check, that all ip were restored for old_ports
- self.port_update.assert_has_calls([
- mock.call(1122, {'port': port1_fixed_ip}),
- mock.call(3344, {'port': port2_fixed_ip}),
- mock.call(5566, {'port': port3_fixed_ip})])
+ # check, that ports were detached from new server
+ nova.NovaClientPlugin.interface_detach.assert_has_calls([
+ mock.call('existing_server', 1122),
+ mock.call('existing_server', 3344),
+ mock.call('existing_server', 5566)])
+
+ # check, that ports were attached to old server
+ nova.NovaClientPlugin.interface_attach.assert_has_calls([
+ mock.call('old_server', 1122),
+ mock.call('old_server', 3344),
+ mock.call('old_server', 5566)])
+
+ def test_restore_ports_after_rollback_attach_failed(self):
+ t, stack, server = self._return_template_stack_and_rsrc_defn(
+ 'test', tmpl_server_with_network_id)
+ server.resource_id = 'existing_server'
+ port_ids = [{'id': 1122}, {'id': 3344}]
+ server._data = {"internal_ports": jsonutils.dumps(port_ids)}
+
+ # add data to old server in backup stack
+ old_server = mock.Mock()
+ old_server.resource_id = 'old_server'
+ stack._backup_stack = mock.Mock()
+ stack._backup_stack().resources.get.return_value = old_server
+ old_server._data_get_ports.side_effect = [port_ids, []]
+
+ class Fake(object):
+ def interface_list(self):
+ return [iface(1122)]
+ iface = collections.namedtuple('iface', ['port_id'])
+
+ self.patchobject(nova.NovaClientPlugin, 'interface_detach')
+ self.patchobject(nova.NovaClientPlugin, 'check_interface_detach',
+ return_value=True)
+ self.patchobject(nova.NovaClientPlugin, 'interface_attach')
+ self.patchobject(nova.NovaClientPlugin, 'fetch_server')
+ # need to mock 11 times: 1 for port 1122, 10 for port 3344
+ nova.NovaClientPlugin.fetch_server.side_effect = [Fake()] * 11
+
+ exc = self.assertRaises(exception.InterfaceAttachFailed,
+ server.restore_after_rollback)
+ self.assertIn('Failed to attach interface (3344) to server '
+ '(old_server)',
+ six.text_type(exc))
def test_store_external_ports_os_interface_not_installed(self):
tmpl = """