diff options
| author | Lance Bragstad <ldbragst@us.ibm.com> | 2012-11-01 23:02:34 +0800 |
|---|---|---|
| committer | Lance Bragstad <ldbragst@us.ibm.com> | 2013-02-13 15:28:25 +0000 |
| commit | b0e99bedfff609c330a01378d6ddf9cbb704fd0e (patch) | |
| tree | b5c5ae07810dcddbe9ecfe1c17300b88498cdaed /nova | |
| parent | 99ddc0d2ad7f2f9c27deaac08559eb794845afc3 (diff) | |
| download | nova-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
Diffstat (limited to 'nova')
| -rw-r--r-- | nova/virt/powervm/blockdev.py | 43 | ||||
| -rw-r--r-- | nova/virt/powervm/command.py | 3 | ||||
| -rw-r--r-- | nova/virt/powervm/common.py | 68 | ||||
| -rwxr-xr-x | nova/virt/powervm/driver.py | 102 | ||||
| -rw-r--r-- | nova/virt/powervm/lpar.py | 6 | ||||
| -rw-r--r-- | nova/virt/powervm/operator.py | 206 |
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. |
