diff --git a/heat/engine/resources/instance.py b/heat/engine/resources/instance.py index 3d9130fadd..15f0d165ab 100644 --- a/heat/engine/resources/instance.py +++ b/heat/engine/resources/instance.py @@ -11,6 +11,8 @@ # License for the specific language governing permissions and limitations # under the License. +import copy + from oslo.config import cfg import six @@ -208,7 +210,8 @@ class Instance(resource.Resource): ), NETWORK_INTERFACES: properties.Schema( properties.Schema.LIST, - _('Network interfaces to associate with instance.') + _('Network interfaces to associate with instance.'), + update_allowed=True ), SOURCE_DEST_CHECK: properties.Schema( properties.Schema.BOOLEAN, @@ -217,7 +220,8 @@ class Instance(resource.Resource): ), SUBNET_ID: properties.Schema( properties.Schema.STRING, - _('Subnet ID to launch instance in.') + _('Subnet ID to launch instance in.'), + update_allowed=True ), TAGS: properties.Schema( properties.Schema.LIST, @@ -531,10 +535,18 @@ class Instance(resource.Resource): return ((vol[self.VOLUME_ID], vol[self.VOLUME_DEVICE]) for vol in volumes) + def _remove_matched_ifaces(self, old_network_ifaces, new_network_ifaces): + # find matches and remove them from old and new ifaces + old_network_ifaces_copy = copy.deepcopy(old_network_ifaces) + for iface in old_network_ifaces_copy: + if iface in new_network_ifaces: + new_network_ifaces.remove(iface) + old_network_ifaces.remove(iface) + def handle_update(self, json_snippet, tmpl_diff, prop_diff): if 'Metadata' in tmpl_diff: self.metadata = tmpl_diff['Metadata'] - + checkers = [] server = None if self.TAGS in prop_diff: server = self.nova().servers.get(self.resource_id) @@ -549,11 +561,82 @@ class Instance(resource.Resource): server = self.nova().servers.get(self.resource_id) checker = scheduler.TaskRunner(nova_utils.resize, server, flavor, flavor_id) - checker.start() - return checker + checkers.append(checker) + if self.NETWORK_INTERFACES in prop_diff: + new_network_ifaces = prop_diff.get(self.NETWORK_INTERFACES) + old_network_ifaces = self.properties.get(self.NETWORK_INTERFACES) + subnet_id = ( + prop_diff.get(self.SUBNET_ID) or + self.properties.get(self.SUBNET_ID)) + security_groups = self._get_security_groups() + if not server: + server = self.nova().servers.get(self.resource_id) + # if there is entrys in old_network_ifaces and new_network_ifaces, + # remove the same entrys from old and new ifaces + if old_network_ifaces and new_network_ifaces: + # there are four situations: + # 1.old includes new, such as: old = 2,3, new = 2 + # 2.new includes old, such as: old = 2,3, new = 1,2,3 + # 3.has overlaps, such as: old = 2,3, new = 1,2 + # 4.different, such as: old = 2,3, new = 1,4 + # detach unmatched ones in old, attach unmatched ones in new + self._remove_matched_ifaces(old_network_ifaces, + new_network_ifaces) + if old_network_ifaces: + old_nics = self._build_nics(old_network_ifaces) + for nic in old_nics: + checker = scheduler.TaskRunner( + server.interface_detach, + nic['port-id']) + checkers.append(checker) + if new_network_ifaces: + new_nics = self._build_nics(new_network_ifaces) + for nic in new_nics: + checker = scheduler.TaskRunner( + server.interface_attach, + nic['port-id'], + None, None) + checkers.append(checker) + # if the interfaces not come from property 'NetworkInterfaces', + # the situation is somewhat complex, so to detach the old ifaces, + # and then attach the new ones. + else: + interfaces = server.interface_list() + for iface in interfaces: + checker = scheduler.TaskRunner(server.interface_detach, + iface.port_id) + checkers.append(checker) + nics = self._build_nics(new_network_ifaces, + security_groups=security_groups, + subnet_id=subnet_id) + # 'SubnetId' property is empty(or None) and + # 'NetworkInterfaces' property is empty(or None), + # _build_nics() will return nics = None,we should attach + # first free port, according to similar behavior during + # instance creation + if not nics: + checker = scheduler.TaskRunner(server.interface_attach, + None, None, None) + checkers.append(checker) + else: + for nic in nics: + checker = scheduler.TaskRunner( + server.interface_attach, + nic['port-id'], None, None) + checkers.append(checker) - def check_update_complete(self, checker): - return checker.step() if checker is not None else True + if checkers: + checkers[0].start() + return checkers + + def check_update_complete(self, checkers): + '''Push all checkers to completion in list order.''' + for checker in checkers: + if not checker.started(): + checker.start() + if not checker.step(): + return False + return True def metadata_update(self, new_metadata=None): ''' diff --git a/heat/tests/test_instance.py b/heat/tests/test_instance.py index 3e0de13b23..62fa1bba99 100644 --- a/heat/tests/test_instance.py +++ b/heat/tests/test_instance.py @@ -382,6 +382,270 @@ class InstancesTest(HeatTestCase): self.assertEqual((instance.UPDATE, instance.FAILED), instance.state) self.m.VerifyAll() + def create_fake_iface(self, port, net, ip): + class fake_interface(): + def __init__(self, port_id, net_id, fixed_ip): + self.port_id = port_id + self.net_id = net_id + self.fixed_ips = [{'ip_address': fixed_ip}] + + return fake_interface(port, net, ip) + + def test_instance_update_network_interfaces(self): + """ + Instance.handle_update supports changing the NetworkInterfaces, + and makes the change making a resize API call against Nova. + """ + return_server = self.fc.servers.list()[1] + return_server.id = 1234 + instance = self._create_test_instance(return_server, + 'ud_network_interfaces') + # if new overlaps with old, detach the different ones in old, and + # attach the different ones in new + old_interfaces = [ + {'NetworkInterfaceId': 'ea29f957-cd35-4364-98fb-57ce9732c10d', + 'DeviceIndex': '2'}, + {'NetworkInterfaceId': 'd1e9c73c-04fe-4e9e-983c-d5ef94cd1a46', + 'DeviceIndex': '1'}] + new_interfaces = [ + {'NetworkInterfaceId': 'ea29f957-cd35-4364-98fb-57ce9732c10d', + 'DeviceIndex': '2'}, + {'NetworkInterfaceId': '34b752ec-14de-416a-8722-9531015e04a5', + 'DeviceIndex': '3'}] + + instance.t['Properties']['NetworkInterfaces'] = old_interfaces + update_template = copy.deepcopy(instance.t) + update_template['Properties']['NetworkInterfaces'] = new_interfaces + + self.m.StubOutWithMock(self.fc.servers, 'get') + self.fc.servers.get(1234).AndReturn(return_server) + self.m.StubOutWithMock(return_server, 'interface_detach') + return_server.interface_detach( + 'd1e9c73c-04fe-4e9e-983c-d5ef94cd1a46').AndReturn(None) + self.m.StubOutWithMock(return_server, 'interface_attach') + return_server.interface_attach('34b752ec-14de-416a-8722-9531015e04a5', + None, None).AndReturn(None) + self.m.ReplayAll() + + scheduler.TaskRunner(instance.update, update_template)() + self.assertEqual((instance.UPDATE, instance.COMPLETE), instance.state) + self.m.VerifyAll() + + def test_instance_update_network_interfaces_old_include_new(self): + """ + Instance.handle_update supports changing the NetworkInterfaces, + and makes the change making a resize API call against Nova. + """ + return_server = self.fc.servers.list()[1] + return_server.id = 1234 + instance = self._create_test_instance(return_server, + 'ud_network_interfaces') + # if old include new, just detach the different ones in old + old_interfaces = [ + {'NetworkInterfaceId': 'ea29f957-cd35-4364-98fb-57ce9732c10d', + 'DeviceIndex': '2'}, + {'NetworkInterfaceId': 'd1e9c73c-04fe-4e9e-983c-d5ef94cd1a46', + 'DeviceIndex': '1'}] + new_interfaces = [ + {'NetworkInterfaceId': 'ea29f957-cd35-4364-98fb-57ce9732c10d', + 'DeviceIndex': '2'}] + + instance.t['Properties']['NetworkInterfaces'] = old_interfaces + update_template = copy.deepcopy(instance.t) + update_template['Properties']['NetworkInterfaces'] = new_interfaces + + self.m.StubOutWithMock(self.fc.servers, 'get') + self.fc.servers.get(1234).AndReturn(return_server) + self.m.StubOutWithMock(return_server, 'interface_detach') + return_server.interface_detach( + 'd1e9c73c-04fe-4e9e-983c-d5ef94cd1a46').AndReturn(None) + + self.m.ReplayAll() + + scheduler.TaskRunner(instance.update, update_template)() + self.assertEqual((instance.UPDATE, instance.COMPLETE), instance.state) + + def test_instance_update_network_interfaces_new_include_old(self): + """ + Instance.handle_update supports changing the NetworkInterfaces, + and makes the change making a resize API call against Nova. + """ + return_server = self.fc.servers.list()[1] + return_server.id = 1234 + instance = self._create_test_instance(return_server, + 'ud_network_interfaces') + # if new include old, just attach the different ones in new + old_interfaces = [ + {'NetworkInterfaceId': 'ea29f957-cd35-4364-98fb-57ce9732c10d', + 'DeviceIndex': '2'}] + new_interfaces = [ + {'NetworkInterfaceId': 'ea29f957-cd35-4364-98fb-57ce9732c10d', + 'DeviceIndex': '2'}, + {'NetworkInterfaceId': 'd1e9c73c-04fe-4e9e-983c-d5ef94cd1a46', + 'DeviceIndex': '1'}] + + instance.t['Properties']['NetworkInterfaces'] = old_interfaces + update_template = copy.deepcopy(instance.t) + update_template['Properties']['NetworkInterfaces'] = new_interfaces + + self.m.StubOutWithMock(self.fc.servers, 'get') + self.fc.servers.get(1234).AndReturn(return_server) + self.m.StubOutWithMock(return_server, 'interface_attach') + return_server.interface_attach('d1e9c73c-04fe-4e9e-983c-d5ef94cd1a46', + None, None).AndReturn(None) + + self.m.ReplayAll() + + scheduler.TaskRunner(instance.update, update_template)() + self.assertEqual((instance.UPDATE, instance.COMPLETE), instance.state) + + def test_instance_update_network_interfaces_new_old_all_different(self): + """ + Instance.handle_update supports changing the NetworkInterfaces, + and makes the change making a resize API call against Nova. + """ + return_server = self.fc.servers.list()[1] + return_server.id = 1234 + instance = self._create_test_instance(return_server, + 'ud_network_interfaces') + # if different, detach the old ones and attach the new ones + old_interfaces = [ + {'NetworkInterfaceId': 'ea29f957-cd35-4364-98fb-57ce9732c10d', + 'DeviceIndex': '2'}] + new_interfaces = [ + {'NetworkInterfaceId': '34b752ec-14de-416a-8722-9531015e04a5', + 'DeviceIndex': '3'}, + {'NetworkInterfaceId': 'd1e9c73c-04fe-4e9e-983c-d5ef94cd1a46', + 'DeviceIndex': '1'}] + + instance.t['Properties']['NetworkInterfaces'] = old_interfaces + update_template = copy.deepcopy(instance.t) + update_template['Properties']['NetworkInterfaces'] = new_interfaces + + self.m.StubOutWithMock(self.fc.servers, 'get') + self.fc.servers.get(1234).AndReturn(return_server) + self.m.StubOutWithMock(return_server, 'interface_detach') + return_server.interface_detach( + 'ea29f957-cd35-4364-98fb-57ce9732c10d').AndReturn(None) + self.m.StubOutWithMock(return_server, 'interface_attach') + return_server.interface_attach('d1e9c73c-04fe-4e9e-983c-d5ef94cd1a46', + None, None).AndReturn(None) + return_server.interface_attach('34b752ec-14de-416a-8722-9531015e04a5', + None, None).AndReturn(None) + + self.m.ReplayAll() + + scheduler.TaskRunner(instance.update, update_template)() + self.assertEqual((instance.UPDATE, instance.COMPLETE), instance.state) + + def test_instance_update_network_interfaces_no_old(self): + """ + Instance.handle_update supports changing the NetworkInterfaces, + and makes the change making a resize API call against Nova. + """ + return_server = self.fc.servers.list()[1] + return_server.id = 1234 + instance = self._create_test_instance(return_server, + 'ud_network_interfaces') + new_interfaces = [ + {'NetworkInterfaceId': 'ea29f957-cd35-4364-98fb-57ce9732c10d', + 'DeviceIndex': '2'}, + {'NetworkInterfaceId': '34b752ec-14de-416a-8722-9531015e04a5', + 'DeviceIndex': '3'}] + iface = self.create_fake_iface('d1e9c73c-04fe-4e9e-983c-d5ef94cd1a46', + 'c4485ba1-283a-4f5f-8868-0cd46cdda52f', + '10.0.0.4') + + update_template = copy.deepcopy(instance.t) + update_template['Properties']['NetworkInterfaces'] = new_interfaces + + self.m.StubOutWithMock(self.fc.servers, 'get') + self.fc.servers.get(1234).AndReturn(return_server) + self.m.StubOutWithMock(return_server, 'interface_list') + return_server.interface_list().AndReturn([iface]) + self.m.StubOutWithMock(return_server, 'interface_detach') + return_server.interface_detach( + 'd1e9c73c-04fe-4e9e-983c-d5ef94cd1a46').AndReturn(None) + self.m.StubOutWithMock(return_server, 'interface_attach') + return_server.interface_attach('ea29f957-cd35-4364-98fb-57ce9732c10d', + None, None).AndReturn(None) + return_server.interface_attach('34b752ec-14de-416a-8722-9531015e04a5', + None, None).AndReturn(None) + + self.m.ReplayAll() + + scheduler.TaskRunner(instance.update, update_template)() + self.assertEqual((instance.UPDATE, instance.COMPLETE), instance.state) + self.m.VerifyAll() + + def test_instance_update_network_interfaces_no_old_no_new(self): + """ + Instance.handle_update supports changing the NetworkInterfaces, + and makes the change making a resize API call against Nova. + """ + return_server = self.fc.servers.list()[1] + return_server.id = 1234 + instance = self._create_test_instance(return_server, + 'ud_network_interfaces') + iface = self.create_fake_iface('d1e9c73c-04fe-4e9e-983c-d5ef94cd1a46', + 'c4485ba1-283a-4f5f-8868-0cd46cdda52f', + '10.0.0.4') + + update_template = copy.deepcopy(instance.t) + update_template['Properties']['NetworkInterfaces'] = [] + + self.m.StubOutWithMock(self.fc.servers, 'get') + self.fc.servers.get(1234).AndReturn(return_server) + self.m.StubOutWithMock(return_server, 'interface_list') + return_server.interface_list().AndReturn([iface]) + self.m.StubOutWithMock(return_server, 'interface_detach') + return_server.interface_detach( + 'd1e9c73c-04fe-4e9e-983c-d5ef94cd1a46').AndReturn(None) + self.m.StubOutWithMock(return_server, 'interface_attach') + return_server.interface_attach(None, None, None).AndReturn(None) + self.m.ReplayAll() + + scheduler.TaskRunner(instance.update, update_template)() + self.assertEqual((instance.UPDATE, instance.COMPLETE), instance.state) + self.m.VerifyAll() + + def test_instance_update_network_interfaces_no_old_new_with_subnet(self): + """ + Instance.handle_update supports changing the NetworkInterfaces, + and makes the change making a resize API call against Nova. + """ + return_server = self.fc.servers.list()[1] + return_server.id = 1234 + instance = self._create_test_instance(return_server, + 'ud_network_interfaces') + iface = self.create_fake_iface('d1e9c73c-04fe-4e9e-983c-d5ef94cd1a46', + 'c4485ba1-283a-4f5f-8868-0cd46cdda52f', + '10.0.0.4') + subnet_id = '8c1aaddf-e49e-4f28-93ea-ca9f0b3c6240' + nics = [{'port-id': 'ea29f957-cd35-4364-98fb-57ce9732c10d'}] + update_template = copy.deepcopy(instance.t) + update_template['Properties']['NetworkInterfaces'] = [] + update_template['Properties']['SubnetId'] = subnet_id + + self.m.StubOutWithMock(self.fc.servers, 'get') + self.fc.servers.get(1234).AndReturn(return_server) + self.m.StubOutWithMock(return_server, 'interface_list') + return_server.interface_list().AndReturn([iface]) + self.m.StubOutWithMock(return_server, 'interface_detach') + return_server.interface_detach( + 'd1e9c73c-04fe-4e9e-983c-d5ef94cd1a46').AndReturn(None) + self.m.StubOutWithMock(instance, '_build_nics') + instance._build_nics([], security_groups=None, + subnet_id=subnet_id).AndReturn(nics) + self.m.StubOutWithMock(return_server, 'interface_attach') + return_server.interface_attach('ea29f957-cd35-4364-98fb-57ce9732c10d', + None, None).AndReturn(None) + self.m.ReplayAll() + + scheduler.TaskRunner(instance.update, update_template)() + self.assertEqual((instance.UPDATE, instance.COMPLETE), instance.state) + self.m.VerifyAll() + def test_instance_update_replace(self): return_server = self.fc.servers.list()[1] instance = self._create_test_instance(return_server,