From 23191308ff1bb151e441e398d69e46fb845f4e65 Mon Sep 17 00:00:00 2001 From: Sahid Orentino Ferdjaoui Date: Wed, 30 Mar 2016 11:44:55 -0400 Subject: [PATCH] libvirt: introduces module to handle domain xml migration This commit adds module to handle domain migration it also cleans the driver in several places to avoid duplicate code. Change-Id: I19612e0ec003212e12547a36243573531c5c5d20 Implements: blueprint libvirt-clean-driver --- nova/tests/unit/virt/libvirt/test_driver.py | 68 ++++--- .../tests/unit/virt/libvirt/test_migration.py | 181 ++++++++++++++++++ nova/virt/libvirt/driver.py | 124 +----------- nova/virt/libvirt/migration.py | 122 ++++++++++++ 4 files changed, 359 insertions(+), 136 deletions(-) create mode 100644 nova/tests/unit/virt/libvirt/test_migration.py create mode 100644 nova/virt/libvirt/migration.py diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index 07c54308db58..8c62feb3facf 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -96,6 +96,7 @@ from nova.virt.libvirt import firewall from nova.virt.libvirt import guest as libvirt_guest from nova.virt.libvirt import host from nova.virt.libvirt import imagebackend +from nova.virt.libvirt import migration as libvirt_migrate from nova.virt.libvirt.storage import dmcrypt from nova.virt.libvirt.storage import lvm from nova.virt.libvirt.storage import rbd_utils @@ -6998,7 +6999,8 @@ class LibvirtConnTestCase(test.NoDBTestCase): mget_info,\ mock.patch.object(drvr._host, 'get_domain') as mget_domain,\ mock.patch.object(fakelibvirt.virDomain, 'migrateToURI2'),\ - mock.patch.object(drvr, '_update_xml') as mupdate: + mock.patch.object( + libvirt_migrate, 'get_updated_guest_xml') as mupdate: mget_info.side_effect = exception.InstanceNotFound( instance_id='foo') @@ -7007,8 +7009,10 @@ class LibvirtConnTestCase(test.NoDBTestCase): self.assertFalse(drvr._live_migration_operation( self.context, instance_ref, 'dest', False, migrate_data, test_mock, [])) - mupdate.assert_called_once_with(target_xml, migrate_data.bdms, - {}, '') + # guest object is created under this method we do not have + # other choice than using mock.ANY + mupdate.assert_called_once_with( + mock.ANY, migrate_data, mock.ANY) def test_live_migration_with_valid_target_connect_addr(self): self.compute = importutils.import_object(CONF.compute_manager) @@ -7046,7 +7050,8 @@ class LibvirtConnTestCase(test.NoDBTestCase): drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) test_mock = mock.MagicMock() - with mock.patch.object(drvr, '_update_xml') as mupdate: + with mock.patch.object(libvirt_migrate, + 'get_updated_guest_xml') as mupdate: test_mock.XMLDesc.return_value = target_xml drvr._live_migration_operation(self.context, instance_ref, @@ -7093,13 +7098,20 @@ class LibvirtConnTestCase(test.NoDBTestCase): conf.source_type = "block" conf.source_path = bdmi.connection_info['data'].get('device_path') - with mock.patch.object(drvr, '_get_volume_config', - return_value=conf): + guest = libvirt_guest.Guest(mock.MagicMock()) + with test.nested( + mock.patch.object(drvr, '_get_volume_config', + return_value=conf), + mock.patch.object(guest, 'get_xml_desc', + return_value=initial_xml)): + config = libvirt_migrate.get_updated_guest_xml(guest, + objects.LibvirtLiveMigrateData(bdms=[bdmi]), + drvr._get_volume_config) parser = etree.XMLParser(remove_blank_text=True) - xml_doc = etree.fromstring(initial_xml, parser) - config = drvr._update_volume_xml(xml_doc, [bdmi]) - xml_doc = etree.fromstring(target_xml, parser) - self.assertEqual(etree.tostring(xml_doc), etree.tostring(config)) + config = etree.fromstring(config, parser) + target_xml = etree.fromstring(target_xml, parser) + self.assertEqual(etree.tostring(target_xml), + etree.tostring(config)) def test_live_migration_uri(self): hypervisor_uri_map = ( @@ -7188,11 +7200,16 @@ class LibvirtConnTestCase(test.NoDBTestCase): conf.source_type = "block" conf.source_path = bdmi.connection_info['data'].get('device_path') - with mock.patch.object(drvr, '_get_volume_config', - return_value=conf): - xml_doc = etree.fromstring(initial_xml) - config = drvr._update_volume_xml(xml_doc, [bdmi]) - self.assertEqual(target_xml, etree.tostring(config)) + guest = libvirt_guest.Guest(mock.MagicMock()) + with test.nested( + mock.patch.object(drvr, '_get_volume_config', + return_value=conf), + mock.patch.object(guest, 'get_xml_desc', + return_value=initial_xml)): + config = libvirt_migrate.get_updated_guest_xml(guest, + objects.LibvirtLiveMigrateData(bdms=[bdmi]), + drvr._get_volume_config) + self.assertEqual(target_xml, config) def test_update_volume_xml_no_connection_info(self): drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) @@ -7213,11 +7230,17 @@ class LibvirtConnTestCase(test.NoDBTestCase): format='qcow') bdmi.connection_info = {} conf = vconfig.LibvirtConfigGuestDisk() - with mock.patch.object(drvr, '_get_volume_config', - return_value=conf): - xml_doc = etree.fromstring(initial_xml) - config = drvr._update_volume_xml(xml_doc, [bdmi]) - self.assertEqual(target_xml, etree.tostring(config)) + guest = libvirt_guest.Guest(mock.MagicMock()) + with test.nested( + mock.patch.object(drvr, '_get_volume_config', + return_value=conf), + mock.patch.object(guest, 'get_xml_desc', + return_value=initial_xml)): + config = libvirt_migrate.get_updated_guest_xml( + guest, + objects.LibvirtLiveMigrateData(bdms=[bdmi]), + drvr._get_volume_config) + self.assertEqual(target_xml, config) @mock.patch.object(fakelibvirt.virDomain, "migrateToURI2") @mock.patch.object(fakelibvirt.virDomain, "XMLDesc") @@ -7334,9 +7357,10 @@ class LibvirtConnTestCase(test.NoDBTestCase): @mock.patch.object(host.Host, 'has_min_version', return_value=True) @mock.patch.object(fakelibvirt.virDomain, "migrateToURI3") - @mock.patch('nova.virt.libvirt.driver.LibvirtDriver._update_xml', + @mock.patch('nova.virt.libvirt.migration.get_updated_guest_xml', return_value='') - @mock.patch('nova.virt.libvirt.guest.Guest.get_xml_desc', return_value='') + @mock.patch('nova.virt.libvirt.guest.Guest.get_xml_desc', + return_value='') def test_live_migration_uses_migrateToURI3( self, mock_old_xml, mock_new_xml, mock_migrateToURI3, mock_min_version): diff --git a/nova/tests/unit/virt/libvirt/test_migration.py b/nova/tests/unit/virt/libvirt/test_migration.py new file mode 100644 index 000000000000..15f8696efabc --- /dev/null +++ b/nova/tests/unit/virt/libvirt/test_migration.py @@ -0,0 +1,181 @@ +# Copyright (c) 2016 Red Hat, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from lxml import etree +import mock + +import six + +from nova import objects +from nova import test +from nova.virt.libvirt import config as vconfig +from nova.virt.libvirt import guest as libvirt_guest +from nova.virt.libvirt import migration + + +class UtilityMigrationTestCase(test.NoDBTestCase): + + def test_graphics_listen_addrs(self): + data = objects.LibvirtLiveMigrateData( + graphics_listen_addr_vnc='127.0.0.1', + graphics_listen_addr_spice='127.0.0.2') + addrs = migration.graphics_listen_addrs(data) + self.assertEqual({ + 'vnc': '127.0.0.1', + 'spice': '127.0.0.2'}, addrs) + + def test_graphics_listen_addrs_empty(self): + data = objects.LibvirtLiveMigrateData() + addrs = migration.graphics_listen_addrs(data) + self.assertIsNone(None, addrs) + + def test_graphics_listen_addrs_spice(self): + data = objects.LibvirtLiveMigrateData( + graphics_listen_addr_spice='127.0.0.2') + addrs = migration.graphics_listen_addrs(data) + self.assertEqual({ + 'vnc': None, + 'spice': '127.0.0.2'}, addrs) + + def test_graphics_listen_addrs_vnc(self): + data = objects.LibvirtLiveMigrateData( + graphics_listen_addr_vnc='127.0.0.1') + addrs = migration.graphics_listen_addrs(data) + self.assertEqual({ + 'vnc': '127.0.0.1', + 'spice': None}, addrs) + + def test_serial_listen_addr(self): + data = objects.LibvirtLiveMigrateData( + serial_listen_addr='127.0.0.1') + addr = migration.serial_listen_addr(data) + self.assertEqual('127.0.0.1', addr) + + def test_serial_listen_addr_emtpy(self): + data = objects.LibvirtLiveMigrateData() + addr = migration.serial_listen_addr(data) + self.assertIsNone(addr) + + @mock.patch('lxml.etree.tostring') + @mock.patch.object(migration, '_update_graphics_xml') + @mock.patch.object(migration, '_update_serial_xml') + @mock.patch.object(migration, '_update_volume_xml') + def test_get_updated_guest_xml( + self, mock_volume, mock_serial, mock_graphics, + mock_tostring): + data = objects.LibvirtLiveMigrateData() + mock_guest = mock.Mock(spec=libvirt_guest.Guest) + get_volume_config = mock.MagicMock() + mock_guest.get_xml_desc.return_value = '' + + migration.get_updated_guest_xml(mock_guest, data, get_volume_config) + mock_graphics.assert_called_once_with(mock.ANY, data) + mock_serial.assert_called_once_with(mock.ANY, data) + mock_volume.assert_called_once_with(mock.ANY, data, get_volume_config) + self.assertEqual(1, mock_tostring.called) + + def test_update_serial_xml_serial(self): + data = objects.LibvirtLiveMigrateData( + serial_listen_addr='127.0.0.100') + xml = """ + + + + + +""" + doc = etree.fromstring(xml) + res = etree.tostring(migration._update_serial_xml(doc, data)) + self.assertIn('127.0.0.100', six.text_type(res)) + + def test_update_serial_xml_console(self): + data = objects.LibvirtLiveMigrateData( + serial_listen_addr='127.0.0.100') + xml = """ + + + + + +""" + doc = etree.fromstring(xml) + res = etree.tostring(migration._update_serial_xml(doc, data)) + self.assertIn('127.0.0.100', six.text_type(res)) + + def test_update_graphics(self): + data = objects.LibvirtLiveMigrateData( + graphics_listen_addr_vnc='127.0.0.100', + graphics_listen_addr_spice='127.0.0.200') + xml = """ + + + + + + + + +""" + doc = etree.fromstring(xml) + res = etree.tostring(migration._update_graphics_xml(doc, data)) + self.assertIn('127.0.0.100', six.text_type(res)) + self.assertIn('127.0.0.200', six.text_type(res)) + + def test_update_volume_xml(self): + connection_info = { + 'driver_volume_type': 'iscsi', + 'serial': '58a84f6d-3f0c-4e19-a0af-eb657b790657', + 'data': { + 'access_mode': 'rw', + 'target_discovered': False, + 'target_iqn': 'ip-1.2.3.4:3260-iqn.cde.67890.opst-lun-Z', + 'volume_id': '58a84f6d-3f0c-4e19-a0af-eb657b790657', + 'device_path': + '/dev/disk/by-path/ip-1.2.3.4:3260-iqn.cde.67890.opst-lun-Z'}} + bdm = objects.LibvirtLiveMigrateBDMInfo( + serial='58a84f6d-3f0c-4e19-a0af-eb657b790657', + bus='virtio', type='disk', dev='vdb', + connection_info=connection_info) + data = objects.LibvirtLiveMigrateData( + target_connect_addr='127.0.0.1', + bdms=[bdm], + block_migration=False) + xml = """ + + + + + + 58a84f6d-3f0c-4e19-a0af-eb657b790657 +
+ + +""" + conf = vconfig.LibvirtConfigGuestDisk() + conf.source_device = bdm.type + conf.driver_name = "qemu" + conf.driver_format = "raw" + conf.driver_cache = "none" + conf.target_dev = bdm.dev + conf.target_bus = bdm.bus + conf.serial = bdm.connection_info.get('serial') + conf.source_type = "block" + conf.source_path = bdm.connection_info['data'].get('device_path') + + get_volume_config = mock.MagicMock(return_value=conf) + doc = etree.fromstring(xml) + res = etree.tostring(migration._update_volume_xml( + doc, data, get_volume_config)) + self.assertIn('ip-1.2.3.4:3260-iqn.cde.67890.opst-lun-Z', + six.text_type(res)) diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 5f644057ac90..4635544d6c03 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -102,6 +102,7 @@ from nova.virt.libvirt import host from nova.virt.libvirt import imagebackend from nova.virt.libvirt import imagecache from nova.virt.libvirt import instancejobtracker +from nova.virt.libvirt import migration as libvirt_migrate from nova.virt.libvirt.storage import dmcrypt from nova.virt.libvirt.storage import lvm from nova.virt.libvirt.storage import rbd_utils @@ -5506,22 +5507,8 @@ class LibvirtDriver(driver.ComputeDriver): md_obj.from_legacy_dict(dest_check_data) dest_check_data = md_obj - listen_addrs = None - # We are building listen_addrs of vnc/spice from - # LibvirtLiveMigrateData; in some certains (e.g. an old-code - # destination host) those fields may have not been set and we - # want to avoid any unfortunates exceptions raised. - # TODO(sahid): The method - # _check_graphics_addresses_can_live_migrate_should to take an - # object LibvirtLiveMigrate itself. - if (dest_check_data.obj_attr_is_set('graphics_listen_addr_vnc') - or dest_check_data.obj_attr_is_set('graphics_listen_addr_spice')): - listen_addrs = {'vnc': None, 'spice': None} - if dest_check_data.obj_attr_is_set('graphics_listen_addr_vnc'): - listen_addrs['vnc'] = dest_check_data.graphics_listen_addr_vnc - if dest_check_data.obj_attr_is_set('graphics_listen_addr_spice'): - listen_addrs['spice'] = dest_check_data.graphics_listen_addr_spice - + listen_addrs = libvirt_migrate.graphics_listen_addrs( + dest_check_data) migratable_flag = self._host.is_migratable_xml_flag() if not migratable_flag or not listen_addrs: # In this context want to ensure we do not have to migrate @@ -5844,90 +5831,6 @@ class LibvirtDriver(driver.ComputeDriver): e, instance=instance) raise - def _update_xml(self, xml_str, migrate_bdm_info, listen_addrs, - serial_listen_addr): - xml_doc = etree.fromstring(xml_str) - - if migrate_bdm_info: - xml_doc = self._update_volume_xml(xml_doc, migrate_bdm_info) - if listen_addrs: - xml_doc = self._update_graphics_xml(xml_doc, listen_addrs) - else: - self._check_graphics_addresses_can_live_migrate(listen_addrs) - if serial_listen_addr: - xml_doc = self._update_serial_xml(xml_doc, serial_listen_addr) - else: - self._verify_serial_console_is_disabled() - - return etree.tostring(xml_doc) - - def _update_graphics_xml(self, xml_doc, listen_addrs): - - # change over listen addresses - for dev in xml_doc.findall('./devices/graphics'): - gr_type = dev.get('type') - listen_tag = dev.find('listen') - if gr_type in ('vnc', 'spice'): - if listen_tag is not None: - listen_tag.set('address', listen_addrs[gr_type]) - if dev.get('listen') is not None: - dev.set('listen', listen_addrs[gr_type]) - - return xml_doc - - def _update_volume_xml(self, xml_doc, migrate_bdm_info): - """Update XML using device information of destination host.""" - - # Update volume xml - parser = etree.XMLParser(remove_blank_text=True) - disk_nodes = xml_doc.findall('./devices/disk') - - bdm_info_by_serial = {x.serial: x for x in migrate_bdm_info} - for pos, disk_dev in enumerate(disk_nodes): - serial_source = disk_dev.findtext('serial') - bdm_info = bdm_info_by_serial.get(serial_source) - if (serial_source is None or - not bdm_info or not bdm_info.connection_info or - serial_source not in bdm_info_by_serial): - continue - conf = self._get_volume_config( - bdm_info.connection_info, bdm_info.as_disk_info()) - xml_doc2 = etree.XML(conf.to_xml(), parser) - serial_dest = xml_doc2.findtext('serial') - - # Compare source serial and destination serial number. - # If these serial numbers match, continue the process. - if (serial_dest and (serial_source == serial_dest)): - LOG.debug("Find same serial number: pos=%(pos)s, " - "serial=%(num)s", - {'pos': pos, 'num': serial_source}) - for cnt, item_src in enumerate(disk_dev): - # If source and destination have same item, update - # the item using destination value. - for item_dst in xml_doc2.findall(item_src.tag): - disk_dev.remove(item_src) - item_dst.tail = None - disk_dev.insert(cnt, item_dst) - - # If destination has additional items, thses items should be - # added here. - for item_dst in list(xml_doc2): - item_dst.tail = None - disk_dev.insert(cnt, item_dst) - - return xml_doc - - def _update_serial_xml(self, xml_doc, listen_addr): - for dev in xml_doc.findall("./devices/serial[@type='tcp']/source"): - if dev.get('host') is not None: - dev.set('host', listen_addr) - - for dev in xml_doc.findall("./devices/console[@type='tcp']/source"): - if dev.get('host') is not None: - dev.set('host', listen_addr) - - return xml_doc - def _check_graphics_addresses_can_live_migrate(self, listen_addrs): LOCAL_ADDRS = ('0.0.0.0', '127.0.0.1', '::', '::1') @@ -6008,15 +5911,8 @@ class LibvirtDriver(driver.ComputeDriver): else: migration_flags = self._live_migration_flags - listen_addrs = {} - if 'graphics_listen_addr_vnc' in migrate_data: - listen_addrs['vnc'] = str( - migrate_data.graphics_listen_addr_vnc) - if 'graphics_listen_addr_spice' in migrate_data: - listen_addrs['spice'] = str( - migrate_data.graphics_listen_addr_spice) - serial_listen_addr = (migrate_data.serial_listen_addr if - 'serial_listen_addr' in migrate_data else None) + listen_addrs = libvirt_migrate.graphics_listen_addrs( + migrate_data) if ('target_connect_addr' in migrate_data and migrate_data.target_connect_addr is not None): dest = migrate_data.target_connect_addr @@ -6028,11 +5924,11 @@ class LibvirtDriver(driver.ComputeDriver): None, CONF.libvirt.live_migration_bandwidth) else: - old_xml_str = guest.get_xml_desc(dump_migratable=True) - new_xml_str = self._update_xml(old_xml_str, - migrate_data.bdms, - listen_addrs, - serial_listen_addr) + new_xml_str = libvirt_migrate.get_updated_guest_xml( + # TODO(sahid): It's not a really well idea to pass + # the method _get_volume_config and we should to find + # a way to avoid this in future. + guest, migrate_data, self._get_volume_config) if self._host.has_min_version( MIN_LIBVIRT_BLOCK_LM_WITH_VOLUMES_VERSION): params = { diff --git a/nova/virt/libvirt/migration.py b/nova/virt/libvirt/migration.py new file mode 100644 index 000000000000..50f6cb3d0f1e --- /dev/null +++ b/nova/virt/libvirt/migration.py @@ -0,0 +1,122 @@ +# Copyright (c) 2016 Red Hat, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +"""Utility methods to manage guests migration + +""" + +from lxml import etree +from oslo_log import log as logging + +LOG = logging.getLogger(__name__) + + +def graphics_listen_addrs(migrate_data): + """Returns listen addresses of vnc/spice from a LibvirtLiveMigrateData""" + listen_addrs = None + if (migrate_data.obj_attr_is_set('graphics_listen_addr_vnc') + or migrate_data.obj_attr_is_set('graphics_listen_addr_spice')): + listen_addrs = {'vnc': None, 'spice': None} + if migrate_data.obj_attr_is_set('graphics_listen_addr_vnc'): + listen_addrs['vnc'] = str(migrate_data.graphics_listen_addr_vnc) + if migrate_data.obj_attr_is_set('graphics_listen_addr_spice'): + listen_addrs['spice'] = str( + migrate_data.graphics_listen_addr_spice) + return listen_addrs + + +def serial_listen_addr(migrate_data): + """Returns listen address serial from a LibvirtLiveMigrateData""" + listen_addr = None + if migrate_data.obj_attr_is_set('serial_listen_addr'): + listen_addr = str(migrate_data.serial_listen_addr) + return listen_addr + + +def get_updated_guest_xml(guest, migrate_data, get_volume_config): + xml_doc = etree.fromstring(guest.get_xml_desc(dump_migratable=True)) + xml_doc = _update_graphics_xml(xml_doc, migrate_data) + xml_doc = _update_serial_xml(xml_doc, migrate_data) + xml_doc = _update_volume_xml(xml_doc, migrate_data, get_volume_config) + return etree.tostring(xml_doc) + + +def _update_graphics_xml(xml_doc, migrate_data): + listen_addrs = graphics_listen_addrs(migrate_data) + + # change over listen addresses + for dev in xml_doc.findall('./devices/graphics'): + gr_type = dev.get('type') + listen_tag = dev.find('listen') + if gr_type in ('vnc', 'spice'): + if listen_tag is not None: + listen_tag.set('address', listen_addrs[gr_type]) + if dev.get('listen') is not None: + dev.set('listen', listen_addrs[gr_type]) + return xml_doc + + +def _update_serial_xml(xml_doc, migrate_data): + listen_addr = serial_listen_addr(migrate_data) + for dev in xml_doc.findall("./devices/serial[@type='tcp']/source"): + if dev.get('host') is not None: + dev.set('host', listen_addr) + for dev in xml_doc.findall("./devices/console[@type='tcp']/source"): + if dev.get('host') is not None: + dev.set('host', listen_addr) + return xml_doc + + +def _update_volume_xml(xml_doc, migrate_data, get_volume_config): + """Update XML using device information of destination host.""" + migrate_bdm_info = migrate_data.bdms + + # Update volume xml + parser = etree.XMLParser(remove_blank_text=True) + disk_nodes = xml_doc.findall('./devices/disk') + + bdm_info_by_serial = {x.serial: x for x in migrate_bdm_info} + for pos, disk_dev in enumerate(disk_nodes): + serial_source = disk_dev.findtext('serial') + bdm_info = bdm_info_by_serial.get(serial_source) + if (serial_source is None or + not bdm_info or not bdm_info.connection_info or + serial_source not in bdm_info_by_serial): + continue + conf = get_volume_config( + bdm_info.connection_info, bdm_info.as_disk_info()) + xml_doc2 = etree.XML(conf.to_xml(), parser) + serial_dest = xml_doc2.findtext('serial') + + # Compare source serial and destination serial number. + # If these serial numbers match, continue the process. + if (serial_dest and (serial_source == serial_dest)): + LOG.debug("Find same serial number: pos=%(pos)s, " + "serial=%(num)s", + {'pos': pos, 'num': serial_source}) + for cnt, item_src in enumerate(disk_dev): + # If source and destination have same item, update + # the item using destination value. + for item_dst in xml_doc2.findall(item_src.tag): + disk_dev.remove(item_src) + item_dst.tail = None + disk_dev.insert(cnt, item_dst) + + # If destination has additional items, thses items should be + # added here. + for item_dst in list(xml_doc2): + item_dst.tail = None + disk_dev.insert(cnt, item_dst) + return xml_doc