summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLance Bragstad <ldbragst@us.ibm.com>2012-11-01 23:02:34 +0800
committerLance Bragstad <ldbragst@us.ibm.com>2013-02-13 15:28:25 +0000
commitb0e99bedfff609c330a01378d6ddf9cbb704fd0e (patch)
treeb5c5ae07810dcddbe9ecfe1c17300b88498cdaed
parent99ddc0d2ad7f2f9c27deaac08559eb794845afc3 (diff)
downloadnova-b0e99bedfff609c330a01378d6ddf9cbb704fd0e.tar.gz
nova-b0e99bedfff609c330a01378d6ddf9cbb704fd0e.tar.xz
nova-b0e99bedfff609c330a01378d6ddf9cbb704fd0e.zip
Resize/Migrate functions for PowerVM driver
Added resize and migrate functionality to PowerVM driver. Allows resizing of instance and cold migration of an instance to different host. bp powervm-compute-resize-migration Change-Id: I723456266d3ceb4bd4a7ce4273008201b450f533
-rw-r--r--nova/virt/powervm/blockdev.py43
-rw-r--r--nova/virt/powervm/command.py3
-rw-r--r--nova/virt/powervm/common.py68
-rwxr-xr-xnova/virt/powervm/driver.py102
-rw-r--r--nova/virt/powervm/lpar.py6
-rw-r--r--nova/virt/powervm/operator.py206
6 files changed, 418 insertions, 10 deletions
diff --git a/nova/virt/powervm/blockdev.py b/nova/virt/powervm/blockdev.py
index 76caca1b9..dc539814e 100644
--- a/nova/virt/powervm/blockdev.py
+++ b/nova/virt/powervm/blockdev.py
@@ -164,8 +164,47 @@ class PowerVMLocalVolumeAdapter(PowerVMDiskAdapter):
LOG.warn(_("Failed to clean up snapshot file "
"%(snapshot_file_path)s") % locals())
- def migrate_volume(self):
- raise NotImplementedError()
+ def migrate_volume(self, lv_name, src_host, dest, image_path,
+ instance_name=None):
+ """Copy a logical volume to file, compress, and transfer
+
+ :param lv_name: logical volume device name
+ :param dest: destination IP or DNS name
+ :param image_path: path to remote image storage directory
+ :param instance_name: name of instance that is being migrated
+ :returns: file path on destination of image file that was moved
+ """
+ if instance_name:
+ file_name = ''.join([instance_name, '_rsz'])
+ else:
+ file_name = ''.join([lv_name, '_rsz'])
+ file_path = os.path.join(image_path, file_name)
+ self._copy_device_to_file(lv_name, file_path)
+ cmds = 'gzip %s' % file_path
+ self.run_vios_command_as_root(cmds)
+ file_path = file_path + '.gz'
+ # If destination is not same host
+ # transfer file to destination VIOS system
+ if (src_host != dest):
+ with common.vios_to_vios_auth(self.connection_data.host,
+ dest,
+ self.connection_data) as key_name:
+ cmd = ''.join(['scp -o "StrictHostKeyChecking no"',
+ ('-i %s' % key_name),
+ file_path,
+ '%s@%s:%s' % (self.connection_data.username,
+ dest,
+ image_path)
+ ])
+ # do the remote copy
+ self.run_vios_command(cmd)
+
+ # cleanup local file only if transferring to remote system
+ # otherwise keep the file to boot from locally and clean up later
+ cleanup_cmd = 'rm %s' % file_path
+ self.run_vios_command_as_root(cleanup_cmd)
+
+ return file_path
def attach_volume_to_host(self, *args, **kargs):
pass
diff --git a/nova/virt/powervm/command.py b/nova/virt/powervm/command.py
index 3e51c933c..25cc2c6cd 100644
--- a/nova/virt/powervm/command.py
+++ b/nova/virt/powervm/command.py
@@ -65,6 +65,9 @@ class BaseCommand(object):
def vhost_by_instance_id(self, instance_id_hex):
pass
+ def chsyscfg(self, args=''):
+ return 'chsyscfg %s' % args
+
class IVMCommand(BaseCommand):
diff --git a/nova/virt/powervm/common.py b/nova/virt/powervm/common.py
index bf69be84e..d98d4ae89 100644
--- a/nova/virt/powervm/common.py
+++ b/nova/virt/powervm/common.py
@@ -14,13 +14,16 @@
# License for the specific language governing permissions and limitations
# under the License.
+import contextlib
import ftplib
import os
+import uuid
import paramiko
from nova import exception as nova_exception
from nova.openstack.common import log as logging
+from nova import utils
from nova.virt.powervm import exception
LOG = logging.getLogger(__name__)
@@ -85,7 +88,7 @@ def ssh_command_as_root(ssh_connection, cmd, check_exit_code=True):
raise nova_exception.ProcessExecutionError(exit_code=exit_status,
stdout=stdout,
stderr=stderr,
- cmd=' '.join(cmd))
+ cmd=''.join(cmd))
return (stdout, stderr)
@@ -154,3 +157,66 @@ def aix_path_join(path_one, path_two):
final_path = path_one + '/' + path_two
return final_path
+
+
+@contextlib.contextmanager
+def vios_to_vios_auth(source, dest, conn_info):
+ """Context allowing for SSH between VIOS partitions
+
+ This context will build an SSH key on the source host, put the key
+ into the authorized_keys on the destination host, and make the
+ private key file name available within the context.
+ The key files and key inserted into authorized_keys will be
+ removed when the context exits.
+
+ :param source: source IP or DNS name
+ :param dest: destination IP or DNS name
+ :param conn_info: dictionary object with SSH connection
+ information for both hosts
+ """
+ KEY_BASE_NAME = "os-%s" % uuid.uuid4().hex
+ keypair_uuid = uuid.uuid4()
+ src_conn_obj = ssh_connect(conn_info)
+
+ dest_conn_info = Connection(dest, conn_info.username,
+ conn_info.password)
+ dest_conn_obj = ssh_connect(dest_conn_info)
+
+ def run_command(conn_obj, cmd):
+ stdout, stderr = utils.ssh_execute(conn_obj, cmd)
+ return stdout.strip().splitlines()
+
+ def build_keypair_on_source():
+ mkkey = ('ssh-keygen -f %s -N "" -C %s' %
+ (KEY_BASE_NAME, keypair_uuid.hex))
+ ssh_command_as_root(src_conn_obj, mkkey)
+
+ chown_key = ('chown %s %s*' % (conn_info.username, KEY_BASE_NAME))
+ ssh_command_as_root(src_conn_obj, chown_key)
+
+ cat_key = ('cat %s.pub' % KEY_BASE_NAME)
+ pubkey = run_command(src_conn_obj, cat_key)
+
+ return pubkey[0]
+
+ def cleanup_key_on_source():
+ rmkey = 'rm %s*' % KEY_BASE_NAME
+ run_command(src_conn_obj, rmkey)
+
+ def insert_into_authorized_keys(public_key):
+ echo_key = 'echo "%s" >> .ssh/authorized_keys' % public_key
+ ssh_command_as_root(dest_conn_obj, echo_key)
+
+ def remove_from_authorized_keys():
+ rmkey = ('sed /%s/d .ssh/authorized_keys > .ssh/authorized_keys' %
+ keypair_uuid.hex)
+ ssh_command_as_root(dest_conn_obj, rmkey)
+
+ public_key = build_keypair_on_source()
+ insert_into_authorized_keys(public_key)
+
+ try:
+ yield KEY_BASE_NAME
+ finally:
+ remove_from_authorized_keys()
+ cleanup_key_on_source()
diff --git a/nova/virt/powervm/driver.py b/nova/virt/powervm/driver.py
index dd0f473a6..9cd6453ab 100755
--- a/nova/virt/powervm/driver.py
+++ b/nova/virt/powervm/driver.py
@@ -14,6 +14,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+import socket
import time
from nova.image import glance
@@ -44,7 +45,7 @@ powervm_opts = [
help='PowerVM image remote path'),
cfg.StrOpt('powervm_img_local_path',
default=None,
- help='Local directory to download glance images to'),
+ help='Local directory to download glance images to')
]
CONF = cfg.CONF
@@ -113,10 +114,15 @@ class PowerVMDriver(driver.ComputeDriver):
pass
def get_host_ip_addr(self):
- """
- Retrieves the IP address of the dom0
- """
- pass
+ """Retrieves the IP address of the hypervisor host."""
+ LOG.debug(_("In get_host_ip_addr"))
+ # TODO(mrodden): use operator get_hostname instead
+ hostname = CONF.powervm_mgr
+ LOG.debug(_("Attempting to resolve %s") % hostname)
+ ip_addr = socket.gethostbyname(hostname)
+ LOG.debug(_("%(hostname)s was successfully resolved to %(ip_addr)s") %
+ {'hostname': hostname, 'ip_addr': ip_addr})
+ return ip_addr
def snapshot(self, context, instance, image_id):
"""Snapshots the specified instance.
@@ -208,3 +214,89 @@ class PowerVMDriver(driver.ComputeDriver):
the cache and remove images which are no longer of interest.
"""
pass
+
+ def migrate_disk_and_power_off(self, context, instance, dest,
+ instance_type, network_info,
+ block_device_info=None):
+ """Transfers the disk of a running instance in multiple phases, turning
+ off the instance before the end.
+
+ :returns: disk_info dictionary that is passed as the
+ disk_info parameter to finish_migration
+ on the destination nova-compute host
+ """
+ src_host = self.get_host_ip_addr()
+ pvm_op = self._powervm._operator
+ lpar_obj = pvm_op.get_lpar(instance['name'])
+ vhost = pvm_op.get_vhost_by_instance_id(lpar_obj['lpar_id'])
+ diskname = pvm_op.get_disk_name_by_vhost(vhost)
+
+ self._powervm.power_off(instance['name'], timeout=120)
+
+ disk_info = self._powervm.migrate_disk(
+ diskname, src_host, dest, CONF.powervm_img_remote_path,
+ instance['name'])
+ disk_info['old_lv_size'] = pvm_op.get_logical_vol_size(diskname)
+ new_name = self._get_resize_name(instance['name'])
+ pvm_op.rename_lpar(instance['name'], new_name)
+ return disk_info
+
+ def _get_resize_name(self, instance_name):
+ """Rename the instance to be migrated to avoid naming conflicts
+
+ :param instance_name: name of instance to be migrated
+ :returns: the new instance name
+ """
+ name_tag = 'rsz_'
+
+ # if the current name would overflow with new tag
+ if ((len(instance_name) + len(name_tag)) > 31):
+ # remove enough chars for the tag to fit
+ num_chars = len(name_tag)
+ old_name = instance_name[num_chars:]
+ else:
+ old_name = instance_name
+
+ return ''.join([name_tag, old_name])
+
+ def finish_migration(self, context, migration, instance, disk_info,
+ network_info, image_meta, resize_instance,
+ block_device_info=None):
+ """Completes a resize, turning on the migrated instance
+
+ :param network_info:
+ :py:meth:`~nova.network.manager.NetworkManager.get_instance_nw_info`
+ :param image_meta: image object returned by nova.image.glance that
+ defines the image from which this instance
+ was created
+ """
+ lpar_obj = self._powervm._create_lpar_instance(instance)
+
+ new_lv_size = instance['instance_type']['root_gb']
+ old_lv_size = disk_info['old_lv_size']
+ if 'root_disk_file' in disk_info:
+ disk_size = max(int(new_lv_size), int(old_lv_size))
+ disk_size_bytes = disk_size * 1024 * 1024 * 1024
+ self._powervm.deploy_from_migrated_file(
+ lpar_obj, disk_info['root_disk_file'], disk_size_bytes)
+ else:
+ # this shouldn't get hit unless someone forgot to handle
+ # a certain migration type
+ raise Exception(
+ _('Unrecognized root disk information: %s') %
+ disk_info)
+
+ def confirm_migration(self, migration, instance, network_info):
+ """Confirms a resize, destroying the source VM."""
+
+ new_name = self._get_resize_name(instance['name'])
+ self._powervm.destroy(new_name)
+
+ def finish_revert_migration(self, instance, network_info,
+ block_device_info=None):
+ """Finish reverting a resize, powering back on the instance."""
+
+ # undo instance rename and start
+ new_name = self._get_resize_name(instance['name'])
+ self._powervm._operator.rename_lpar(new_name, instance['name'])
+ self._powervm.power_on(instance['name'])
diff --git a/nova/virt/powervm/lpar.py b/nova/virt/powervm/lpar.py
index 10e8c8e37..907818ca8 100644
--- a/nova/virt/powervm/lpar.py
+++ b/nova/virt/powervm/lpar.py
@@ -49,7 +49,11 @@ def load_from_conf_data(conf_data):
attribs = dict(item.split("=") for item in list(cf_splitter))
lpar = LPAR()
for (key, value) in attribs.items():
- lpar[key] = value
+ try:
+ lpar[key] = value
+ except exception.PowerVMLPARAttributeNotFound, e:
+ LOG.info(_('Encountered unknown LPAR attribute: %s\n'
+ 'Continuing without storing') % key)
return lpar
diff --git a/nova/virt/powervm/operator.py b/nova/virt/powervm/operator.py
index f534acc15..c623c39aa 100644
--- a/nova/virt/powervm/operator.py
+++ b/nova/virt/powervm/operator.py
@@ -266,7 +266,16 @@ class PowerVMOperator(object):
spawn_start = time.time()
try:
- _create_lpar_instance(instance)
+ try:
+ host_stats = self.get_host_stats(refresh=True)
+ lpar_inst = self._create_lpar_instance(instance, host_stats)
+ self._operator.create_lpar(lpar_inst)
+ LOG.debug(_("Creating LPAR instance '%s'") % instance['name'])
+ except nova_exception.ProcessExecutionError:
+ LOG.exception(_("LPAR instance '%s' creation failed") %
+ instance['name'])
+ raise exception.PowerVMLPARCreationFailed()
+
_create_image(context, instance, image_id)
LOG.debug(_("Activating the LPAR instance '%s'")
% instance['name'])
@@ -373,6 +382,118 @@ class PowerVMOperator(object):
def macs_for_instance(self, instance):
return self._operator.macs_for_instance(instance)
+ def _create_lpar_instance(self, instance, host_stats=None):
+ inst_name = instance['name']
+
+ # CPU/Memory min and max can be configurable. Lets assume
+ # some default values for now.
+
+ # Memory
+ mem = instance['memory_mb']
+ if host_stats and mem > host_stats['host_memory_free']:
+ LOG.error(_('Not enough free memory in the host'))
+ raise exception.PowerVMInsufficientFreeMemory(
+ instance_name=instance['name'])
+ mem_min = min(mem, constants.POWERVM_MIN_MEM)
+ mem_max = mem + constants.POWERVM_MAX_MEM
+
+ # CPU
+ cpus = instance['vcpus']
+ if host_stats:
+ avail_cpus = host_stats['vcpus'] - host_stats['vcpus_used']
+ if cpus > avail_cpus:
+ LOG.error(_('Insufficient available CPU on PowerVM'))
+ raise exception.PowerVMInsufficientCPU(
+ instance_name=instance['name'])
+ cpus_min = min(cpus, constants.POWERVM_MIN_CPUS)
+ cpus_max = cpus + constants.POWERVM_MAX_CPUS
+ cpus_units_min = decimal.Decimal(cpus_min) / decimal.Decimal(10)
+ cpus_units = decimal.Decimal(cpus) / decimal.Decimal(10)
+
+ # Network
+ eth_id = self._operator.get_virtual_eth_adapter_id()
+
+ # LPAR configuration data
+ lpar_inst = LPAR.LPAR(
+ name=inst_name, lpar_env='aixlinux',
+ min_mem=mem_min, desired_mem=mem,
+ max_mem=mem_max, proc_mode='shared',
+ sharing_mode='uncap', min_procs=cpus_min,
+ desired_procs=cpus, max_procs=cpus_max,
+ min_proc_units=cpus_units_min,
+ desired_proc_units=cpus_units,
+ max_proc_units=cpus_max,
+ virtual_eth_adapters='4/0/%s//0/0' % eth_id)
+ return lpar_inst
+
+ def _check_host_resources(self, instance, vcpus, mem, host_stats):
+ """Checks resources on host for resize, migrate, and spawn
+ :param vcpus: CPUs to be used
+ :param mem: memory requested by instance
+ :param disk: size of disk to be expanded or created
+ """
+ if mem > host_stats['host_memory_free']:
+ LOG.exception(_('Not enough free memory in the host'))
+ raise exception.PowerVMInsufficientFreeMemory(
+ instance_name=instance['name'])
+
+ avail_cpus = host_stats['vcpus'] - host_stats['vcpus_used']
+ if vcpus > avail_cpus:
+ LOG.exception(_('Insufficient available CPU on PowerVM'))
+ raise exception.PowerVMInsufficientCPU(
+ instance_name=instance['name'])
+
+ def migrate_disk(self, device_name, src_host, dest, image_path,
+ instance_name=None):
+ """Migrates SVC or Logical Volume based disks
+
+ :param device_name: disk device name in /dev/
+ :param dest: IP or DNS name of destination host/VIOS
+ :param image_path: path on source and destination to directory
+ for storing image files
+ :param instance_name: name of instance being migrated
+ :returns: disk_info dictionary object describing root volume
+ information used for locating/mounting the volume
+ """
+ dest_file_path = self._disk_adapter.migrate_volume(
+ device_name, src_host, dest, image_path, instance_name)
+ disk_info = {}
+ disk_info['root_disk_file'] = dest_file_path
+ return disk_info
+
+ def deploy_from_migrated_file(self, lpar, file_path, size):
+ # decompress file
+ gzip_ending = '.gz'
+ if file_path.endswith(gzip_ending):
+ raw_file_path = file_path[:-len(gzip_ending)]
+ else:
+ raw_file_path = file_path
+
+ self._operator._decompress_image_file(file_path, raw_file_path)
+
+ try:
+ # deploy lpar from file
+ self._deploy_from_vios_file(lpar, raw_file_path, size)
+ finally:
+ # cleanup migrated file
+ self._operator._remove_file(raw_file_path)
+
+ def _deploy_from_vios_file(self, lpar, file_path, size):
+ self._operator.create_lpar(lpar)
+ lpar = self._operator.get_lpar(lpar['name'])
+ instance_id = lpar['lpar_id']
+ vhost = self._operator.get_vhost_by_instance_id(instance_id)
+
+ # Create logical volume on IVM
+ diskName = self._disk_adapter._create_logical_volume(size)
+ # Attach the disk to LPAR
+ self._operator.attach_disk_to_vhost(diskName, vhost)
+
+ # Copy file to device
+ self._disk_adapter._copy_file_to_device(file_path, diskName)
+
+ self._operator.start_lpar(lpar['name'])
+
class BaseOperator(object):
"""Base operator for IVM and HMC managed systems."""
@@ -604,6 +725,89 @@ class BaseOperator(object):
def macs_for_instance(self, instance):
pass
+ def update_lpar(self, lpar_info):
+ """Resizing an LPAR
+
+ :param lpar_info: dictionary of LPAR information
+ """
+ configuration_data = ('name=%s,min_mem=%s,desired_mem=%s,'
+ 'max_mem=%s,min_procs=%s,desired_procs=%s,'
+ 'max_procs=%s,min_proc_units=%s,'
+ 'desired_proc_units=%s,max_proc_units=%s' %
+ (lpar_info['name'], lpar_info['min_mem'],
+ lpar_info['desired_mem'],
+ lpar_info['max_mem'],
+ lpar_info['min_procs'],
+ lpar_info['desired_procs'],
+ lpar_info['max_procs'],
+ lpar_info['min_proc_units'],
+ lpar_info['desired_proc_units'],
+ lpar_info['max_proc_units']))
+
+ self.run_vios_command(self.command.chsyscfg('-r prof -i "%s"' %
+ configuration_data))
+
+ def get_logical_vol_size(self, diskname):
+ """Finds and calculates the logical volume size in GB
+
+ :param diskname: name of the logical volume
+ :returns: size of logical volume in GB
+ """
+ configuration_data = ("ioscli lslv %s -fmt : -field pps ppsize" %
+ diskname)
+ output = self.run_vios_command(configuration_data)
+ pps, ppsize = output[0].split(':')
+ ppsize = re.findall(r'\d+', ppsize)
+ ppsize = int(ppsize[0])
+ pps = int(pps)
+ lv_size = ((pps * ppsize) / 1024)
+
+ return lv_size
+
+ def rename_lpar(self, instance_name, new_name):
+ """Rename LPAR given by instance_name to new_name
+
+ Note: For IVM based deployments, the name is
+ limited to 31 characters and will be trimmed
+ to meet this requirement
+
+ :param instance_name: name of LPAR to be renamed
+ :param new_name: desired new name of LPAR
+ :returns: new name of renamed LPAR trimmed to 31 characters
+ if necessary
+ """
+
+ # grab first 31 characters of new name
+ new_name_trimmed = new_name[:31]
+
+ cmd = ''.join(['chsyscfg -r lpar -i ',
+ '"',
+ 'name=%s,' % instance_name,
+ 'new_name=%s' % new_name_trimmed,
+ '"'])
+
+ self.run_vios_command(cmd)
+
+ return new_name_trimmed
+
+ def _decompress_image_file(self, file_path, outfile_path):
+ command = "/usr/bin/gunzip -c %s > %s" % (file_path, outfile_path)
+ output = self.run_vios_command_as_root(command)
+
+ # Remove compressed image file
+ command = "/usr/bin/rm %s" % file_path
+ output = self.run_vios_command_as_root(command)
+
+ return outfile_path
+
+ def _remove_file(self, file_path):
+ """Removes a file on the VIOS partition
+
+ :param file_path: absolute path to file to be removed
+ """
+ command = 'rm %s' % file_path
+ self.run_vios_command_as_root(command)
+
class IVMOperator(BaseOperator):
"""Integrated Virtualization Manager (IVM) Operator.