diff options
author | Lance Bragstad <ldbragst@us.ibm.com> | 2013-01-07 21:30:09 +0000 |
---|---|---|
committer | Lance Bragstad <ldbragst@us.ibm.com> | 2013-01-07 22:49:50 +0000 |
commit | 3bf5e88931b96381b1768c926b97064985cf9f96 (patch) | |
tree | 05a70de847ca7626eb711c2e352d9e2c1e164772 | |
parent | e1c7b18c7f3c8d97ba7b2cccf27b968ad4710735 (diff) | |
download | nova-3bf5e88931b96381b1768c926b97064985cf9f96.tar.gz nova-3bf5e88931b96381b1768c926b97064985cf9f96.tar.xz nova-3bf5e88931b96381b1768c926b97064985cf9f96.zip |
powervm: Implement snapshot for local volumes
Snapshot local volumes by copying the logical volume on VIOS,
gzip the file, transfer to compute service node, and
finally upload to glance. PEP8 and string concatenation fixes.
Added utility to powervm.common for constructing UNIX file paths.
Added unit tests for new utility.
bp powervm-compute-enhancements
Change-Id: Ie6bafbeef75a9464457d78d2997be78346d3ab24
-rw-r--r-- | nova/tests/test_powervm.py | 29 | ||||
-rw-r--r-- | nova/virt/powervm/blockdev.py | 145 | ||||
-rw-r--r-- | nova/virt/powervm/common.py | 48 | ||||
-rw-r--r-- | nova/virt/powervm/driver.py | 52 | ||||
-rw-r--r-- | nova/virt/powervm/exception.py | 6 | ||||
-rw-r--r-- | nova/virt/powervm/operator.py | 30 |
6 files changed, 296 insertions, 14 deletions
diff --git a/nova/tests/test_powervm.py b/nova/tests/test_powervm.py index 02d3a5a3f..3c944e170 100644 --- a/nova/tests/test_powervm.py +++ b/nova/tests/test_powervm.py @@ -26,6 +26,7 @@ from nova.compute import power_state from nova.openstack.common import log as logging from nova.virt import images from nova.virt.powervm import blockdev as powervm_blockdev +from nova.virt.powervm import common from nova.virt.powervm import driver as powervm_driver from nova.virt.powervm import exception from nova.virt.powervm import lpar @@ -195,3 +196,31 @@ class PowerVMDriverTestCase(test.TestCase): self.assertEqual(info['mem'], 1024) self.assertEqual(info['num_cpu'], 2) self.assertEqual(info['cpu_time'], 939395) + + def test_remote_utility_1(self): + path_one = '/some/file/' + path_two = '/path/filename' + joined_path = common.aix_path_join(path_one, path_two) + expected_path = '/some/file/path/filename' + self.assertEqual(joined_path, expected_path) + + def test_remote_utility_2(self): + path_one = '/some/file/' + path_two = 'path/filename' + joined_path = common.aix_path_join(path_one, path_two) + expected_path = '/some/file/path/filename' + self.assertEqual(joined_path, expected_path) + + def test_remote_utility_3(self): + path_one = '/some/file' + path_two = '/path/filename' + joined_path = common.aix_path_join(path_one, path_two) + expected_path = '/some/file/path/filename' + self.assertEqual(joined_path, expected_path) + + def test_remote_utility_4(self): + path_one = '/some/file' + path_two = 'path/filename' + joined_path = common.aix_path_join(path_one, path_two) + expected_path = '/some/file/path/filename' + self.assertEqual(joined_path, expected_path) diff --git a/nova/virt/powervm/blockdev.py b/nova/virt/powervm/blockdev.py index b359716ff..fb3a0210c 100644 --- a/nova/virt/powervm/blockdev.py +++ b/nova/virt/powervm/blockdev.py @@ -18,11 +18,16 @@ import hashlib import os import re +from eventlet import greenthread + from nova import utils +from nova.image import glance + from nova.openstack.common import cfg from nova.openstack.common import excutils from nova.openstack.common import log as logging + from nova.virt import images from nova.virt.powervm import command from nova.virt.powervm import common @@ -78,7 +83,7 @@ class PowerVMLocalVolumeAdapter(PowerVMDiskAdapter): :param context: nova context used to retrieve image from glance :param instance: instance to create the volume for - :image_id: image_id reference used to locate image in glance + :param image_id: image_id reference used to locate image in glance :returns: dictionary with the name of the created Logical Volume device in 'device_name' key """ @@ -125,8 +130,44 @@ class PowerVMLocalVolumeAdapter(PowerVMDiskAdapter): return {'device_name': disk_name} - def create_image_from_volume(self): - raise NotImplementedError() + def create_image_from_volume(self, device_name, context, + image_id, image_meta): + """Capture the contents of a volume and upload to glance + + :param device_name: device in /dev/ to capture + :param context: nova context for operation + :param image_id: image reference to pre-created image in glance + :param image_meta: metadata for new image + """ + + # do the disk copy + dest_file_path = common.aix_path_join(CONF.powervm_img_remote_path, + image_id) + self._copy_device_to_file(device_name, dest_file_path) + + # compress and copy the file back to the nova-compute host + snapshot_file_path = self._copy_image_file_from_host( + dest_file_path, CONF.powervm_img_local_path, + compress=True) + + # get glance service + glance_service, image_id = glance.get_remote_image_service( + context, image_id) + + # upload snapshot file to glance + with open(snapshot_file_path, 'r') as img_file: + glance_service.update(context, + image_id, + image_meta, + img_file) + LOG.debug(_("Snapshot added to glance.")) + + # clean up local image file + try: + os.remove(snapshot_file_path) + except OSError as ose: + LOG.warn(_("Failed to clean up snapshot file " + "%(snapshot_file_path)s") % locals()) def migrate_volume(self): raise NotImplementedError() @@ -202,6 +243,25 @@ class PowerVMLocalVolumeAdapter(PowerVMDiskAdapter): cmd = 'dd if=%s of=/dev/%s bs=1024k' % (source_path, device) self.run_vios_command_as_root(cmd) + def _copy_device_to_file(self, device_name, file_path): + """Copy a device to a file using dd + + :param device_name: device name to copy from + :param file_path: output file path + """ + cmd = 'dd if=/dev/%s of=%s bs=1024k' % (device_name, file_path) + self.run_vios_command_as_root(cmd) + + def _md5sum_remote_file(self, remote_path): + # AIX6/VIOS cannot md5sum files with sizes greater than ~2GB + cmd = ("perl -MDigest::MD5 -e 'my $file = \"%s\"; open(FILE, $file); " + "binmode(FILE); " + "print Digest::MD5->new->addfile(*FILE)->hexdigest, " + "\" $file\n\";'" % remote_path) + + output = self.run_vios_command_as_root(cmd) + return output[0] + def _copy_image_file(self, source_path, remote_path, decompress=False): """Copy file to VIOS, decompress it, and return its new size and name. @@ -225,26 +285,24 @@ class PowerVMLocalVolumeAdapter(PowerVMDiskAdapter): if not decompress: final_path = comp_path else: - final_path = "%s.%s" % (uncomp_path, source_cksum) + final_path = uncomp_path # Check whether the image is already on IVM output = self.run_vios_command("ls %s" % final_path, check_exit_code=False) # If the image does not exist already - if not len(output): + if not output: # Copy file to IVM common.ftp_put_command(self.connection_data, source_path, remote_path) # Verify image file checksums match - cmd = ("/usr/bin/csum -h MD5 %s |" - "/usr/bin/awk '{print $1}'" % comp_path) - output = self.run_vios_command_as_root(cmd) - if not len(output): + output = self._md5sum_remote_file(final_path) + if not output: LOG.error(_("Unable to get checksum")) raise exception.PowerVMFileTransferFailed() - if source_cksum != output[0]: + if source_cksum != output.split(' ')[0]: LOG.error(_("Image checksums do not match")) raise exception.PowerVMFileTransferFailed() @@ -271,7 +329,7 @@ class PowerVMLocalVolumeAdapter(PowerVMDiskAdapter): # Calculate file size in multiples of 512 bytes output = self.run_vios_command("ls -o %s|awk '{print $4}'" % final_path, check_exit_code=False) - if len(output): + if output: size = int(output[0]) else: LOG.error(_("Uncompressed image file not found")) @@ -281,6 +339,71 @@ class PowerVMLocalVolumeAdapter(PowerVMDiskAdapter): return final_path, size + def _copy_image_file_from_host(self, remote_source_path, local_dest_dir, + compress=False): + """ + Copy a file from IVM to the nova-compute host, + and return the location of the copy + + :param remote_source_path remote source file path + :param local_dest_dir local destination directory + :param compress: if True, compress the file before transfer; + if False (default), copy the file as is + """ + + temp_str = common.aix_path_join(local_dest_dir, + os.path.basename(remote_source_path)) + local_file_path = temp_str + '.gz' + + if compress: + copy_from_path = remote_source_path + '.gz' + else: + copy_from_path = remote_source_path + + if compress: + # Gzip the file + cmd = "/usr/bin/gzip %s" % remote_source_path + self.run_vios_command_as_root(cmd) + + # Cleanup uncompressed remote file + cmd = "/usr/bin/rm -f %s" % remote_source_path + self.run_vios_command_as_root(cmd) + + # Get file checksum + output = self._md5sum_remote_file(copy_from_path) + if not output: + LOG.error(_("Unable to get checksum")) + msg_args = {'file_path': copy_from_path} + raise exception.PowerVMFileTransferFailed(**msg_args) + else: + source_chksum = output.split(' ')[0] + + # Copy file to host + common.ftp_get_command(self.connection_data, + copy_from_path, + local_file_path) + + # Calculate copied image checksum + with open(local_file_path, 'r') as image_file: + hasher = hashlib.md5() + block_size = 0x10000 + buf = image_file.read(block_size) + while len(buf) > 0: + hasher.update(buf) + buf = image_file.read(block_size) + dest_chksum = hasher.hexdigest() + + # do comparison + if source_chksum and dest_chksum != source_chksum: + LOG.error(_("Image checksums do not match")) + raise exception.PowerVMFileTransferFailed() + + # Cleanup transferred remote file + cmd = "/usr/bin/rm -f %s" % copy_from_path + output = self.run_vios_command_as_root(cmd) + + return local_file_path + def run_vios_command(self, cmd, check_exit_code=True): """Run a remote command using an active ssh connection. diff --git a/nova/virt/powervm/common.py b/nova/virt/powervm/common.py index 179bd7f14..bf69be84e 100644 --- a/nova/virt/powervm/common.py +++ b/nova/virt/powervm/common.py @@ -63,6 +63,7 @@ def ssh_command_as_root(ssh_connection, cmd, check_exit_code=True): :returns: Tuple -- a tuple of (stdout, stderr) :raises: nova.exception.ProcessExecutionError """ + LOG.debug(_('Running cmd (SSH-as-root): %s') % cmd) chan = ssh_connection._transport.open_session() # This command is required to be executed # in order to become root. @@ -108,5 +109,48 @@ def ftp_put_command(connection, local_path, remote_dir): f.close() ftp.close() except Exception: - LOG.exception(_('File transfer to PowerVM manager failed')) - raise exception.PowerVMFileTransferFailed(file_path=local_path) + LOG.error(_('File transfer to PowerVM manager failed')) + raise exception.PowerVMFTPTransferFailed(ftp_cmd='PUT', + source_path=local_path, dest_path=remote_dir) + + +def ftp_get_command(connection, remote_path, local_path): + """Retrieve a file via FTP + + :param connection: a Connection object. + :param remote_path: path to the remote file + :param local_path: path to local destination + :raises: PowerVMFileTransferFailed + """ + try: + ftp = ftplib.FTP(host=connection.host, + user=connection.username, + passwd=connection.password) + ftp.cwd(os.path.dirname(remote_path)) + name = os.path.basename(remote_path) + LOG.debug(_("ftp GET %(remote_path)s to: %(local_path)s") % locals()) + with open(local_path, 'w') as ftpfile: + ftpcmd = 'RETR %s' % name + ftp.retrbinary(ftpcmd, ftpfile.write) + ftp.close() + except Exception: + LOG.error(_("File transfer from PowerVM manager failed")) + raise exception.PowerVMFTPTransferFailed(ftp_cmd='GET', + source_path=remote_path, dest_path=local_path) + + +def aix_path_join(path_one, path_two): + """Ensures file path is built correctly for remote UNIX system + + :param path_one: string of the first file path + :param path_two: string of the second file path + :returns: a uniform path constructed from both strings + """ + if path_one.endswith('/'): + path_one = path_one.rstrip('/') + + if path_two.startswith('/'): + path_two = path_two.lstrip('/') + + final_path = path_one + '/' + path_two + return final_path diff --git a/nova/virt/powervm/driver.py b/nova/virt/powervm/driver.py index 0821d4d84..b9acc685c 100644 --- a/nova/virt/powervm/driver.py +++ b/nova/virt/powervm/driver.py @@ -14,6 +14,16 @@ # License for the specific language governing permissions and limitations # under the License. +import os +import time + +from nova.compute import task_states +from nova.compute import vm_states + +from nova import context as nova_context + +from nova.image import glance + from nova.openstack.common import cfg from nova.openstack.common import log as logging @@ -111,6 +121,48 @@ class PowerVMDriver(driver.ComputeDriver): """ pass + def snapshot(self, context, instance, image_id): + """Snapshots the specified instance. + + :param context: security context + :param instance: Instance object as returned by DB layer. + :param image_id: Reference to a pre-created image that will + hold the snapshot. + """ + snapshot_start = time.time() + + # get current image info + glance_service, old_image_id = glance.get_remote_image_service( + context, instance['image_ref']) + image_meta = glance_service.show(context, old_image_id) + img_props = image_meta['properties'] + + # build updated snapshot metadata + snapshot_meta = glance_service.show(context, image_id) + new_snapshot_meta = {'is_public': False, + 'name': snapshot_meta['name'], + 'status': 'active', + 'properties': {'image_location': 'snapshot', + 'image_state': 'available', + 'owner_id': instance['project_id'] + }, + 'disk_format': image_meta['disk_format'], + 'container_format': image_meta['container_format'] + } + + if 'architecture' in image_meta['properties']: + arch = image_meta['properties']['architecture'] + new_snapshot_meta['properties']['architecture'] = arch + + # disk capture and glance upload + self._powervm.capture_image(context, instance, image_id, + new_snapshot_meta) + + snapshot_time = time.time() - snapshot_start + inst_name = instance['name'] + LOG.info(_("%(inst_name)s captured in %(snapshot_time)s seconds") % + locals()) + def pause(self, instance): """Pause the specified instance.""" pass diff --git a/nova/virt/powervm/exception.py b/nova/virt/powervm/exception.py index 2a8cf4771..50e08eaea 100644 --- a/nova/virt/powervm/exception.py +++ b/nova/virt/powervm/exception.py @@ -22,7 +22,11 @@ class PowerVMConnectionFailed(exception.NovaException): class PowerVMFileTransferFailed(exception.NovaException): - message = _("File '%(file_path)' transfer to PowerVM manager failed") + message = _("File '%(file_path)s' transfer to PowerVM manager failed") + + +class PowerVMFTPTransferFailed(PowerVMFileTransferFailed): + message = _("FTP %(ftp_cmd)s from %(source_path)s to %(dest_path)s failed") class PowerVMLPARInstanceNotFound(exception.InstanceNotFound): diff --git a/nova/virt/powervm/operator.py b/nova/virt/powervm/operator.py index ad6b17035..46ead80d5 100644 --- a/nova/virt/powervm/operator.py +++ b/nova/virt/powervm/operator.py @@ -287,6 +287,36 @@ class PowerVMOperator(object): LOG.warn(_("During destroy, LPAR instance '%s' was not found on " "PowerVM system.") % instance_name) + def capture_image(self, context, instance, image_id, image_meta): + """Capture the root disk for a snapshot + + :param context: nova context for this operation + :param instance: instance information to capture the image from + :param image_id: uuid of pre-created snapshot image + :param image_meta: metadata to upload with captured image + """ + lpar = self._operator.get_lpar(instance['name']) + previous_state = lpar['state'] + + # stop the instance if it is running + if previous_state == 'Running': + LOG.debug(_("Stopping instance %s for snapshot.") % + instance['name']) + # wait up to 2 minutes for shutdown + self.power_off(instance['name'], timeout=120) + + # get disk_name + vhost = self._operator.get_vhost_by_instance_id(lpar['lpar_id']) + disk_name = self._operator.get_disk_name_by_vhost(vhost) + + # do capture and upload + self._disk_adapter.create_image_from_volume( + disk_name, context, image_id, image_meta) + + # restart instance if it was running before + if previous_state == 'Running': + self.power_on(instance['name']) + def _cleanup(self, instance_name): lpar_id = self._get_instance(instance_name)['lpar_id'] try: |