diff options
-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: |