summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--nova/tests/test_powervm.py29
-rw-r--r--nova/virt/powervm/blockdev.py145
-rw-r--r--nova/virt/powervm/common.py48
-rw-r--r--nova/virt/powervm/driver.py52
-rw-r--r--nova/virt/powervm/exception.py6
-rw-r--r--nova/virt/powervm/operator.py30
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: