From e1df9606c1cb54db6f9e91fb59f0f6c7636b334c Mon Sep 17 00:00:00 2001 From: Tiago Mello Date: Wed, 11 Jul 2012 17:21:13 -0300 Subject: [PATCH] Add PowerVM compute driver and unit tests. part of bp:powervm-compute-driver Change-Id: I9e55964b5260417d15b5def814ee305fce28c40b --- nova/tests/test_powervm.py | 165 +++++++++ nova/virt/powervm/__init__.py | 29 ++ nova/virt/powervm/command.py | 90 +++++ nova/virt/powervm/common.py | 112 ++++++ nova/virt/powervm/constants.py | 35 ++ nova/virt/powervm/driver.py | 214 +++++++++++ nova/virt/powervm/exception.py | 59 +++ nova/virt/powervm/lpar.py | 158 +++++++++ nova/virt/powervm/operator.py | 631 +++++++++++++++++++++++++++++++++ 9 files changed, 1493 insertions(+) create mode 100644 nova/tests/test_powervm.py create mode 100644 nova/virt/powervm/__init__.py create mode 100644 nova/virt/powervm/command.py create mode 100644 nova/virt/powervm/common.py create mode 100644 nova/virt/powervm/constants.py create mode 100644 nova/virt/powervm/driver.py create mode 100644 nova/virt/powervm/exception.py create mode 100644 nova/virt/powervm/lpar.py create mode 100644 nova/virt/powervm/operator.py diff --git a/nova/tests/test_powervm.py b/nova/tests/test_powervm.py new file mode 100644 index 000000000000..13d9cdd587c9 --- /dev/null +++ b/nova/tests/test_powervm.py @@ -0,0 +1,165 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 IBM +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Test suite for PowerVMDriver. +""" + +from nova.compute import power_state +from nova import context +from nova import db +from nova import flags +from nova import test + +from nova.openstack.common import log as logging +from nova.virt import images +from nova.virt.powervm import driver as powervm_driver +from nova.virt.powervm import lpar +from nova.virt.powervm import operator + + +FLAGS = flags.FLAGS +LOG = logging.getLogger(__name__) + + +def fake_lpar(instance_name): + return lpar.LPAR(name=instance_name, + lpar_id=1, desired_mem=1024, + max_mem=2048, max_procs=2, + uptime=939395, state='Running') + + +class FakeIVMOperator(object): + + def get_lpar(self, instance_name, resource_type='lpar'): + return fake_lpar(instance_name) + + def list_lpar_instances(self): + return ['instance-00000001', 'instance-00000002'] + + def create_lpar(self, lpar): + pass + + def start_lpar(self, instance_name): + pass + + def stop_lpar(self, instance_name): + pass + + def remove_lpar(self, instance_name): + pass + + def get_vhost_by_instance_id(self, instance_id): + return 'vhostfake' + + def get_virtual_eth_adapter_id(self): + return 1 + + def get_disk_name_by_vhost(self, vhost): + return 'lvfake01' + + def remove_disk(self, disk_name): + pass + + def create_logical_volume(self, size): + return 'lvfake01' + + def remove_logical_volume(self, lv_name): + pass + + def copy_file_to_device(self, sourcePath, device): + pass + + def copy_image_file(self, sourcePath, remotePath): + finalPath = '/home/images/rhel62.raw.7e358754160433febd6f3318b7c9e335' + size = 4294967296 + return finalPath, size + + def run_cfg_dev(self, device_name): + pass + + def attach_disk_to_vhost(self, disk, vhost): + pass + + def get_memory_info(self): + return {'total_mem': 65536, 'avail_mem': 46336} + + def get_cpu_info(self): + return {'total_procs': 8.0, 'avail_procs': 6.3} + + def get_disk_info(self): + return {'disk_total': 10168, + 'disk_used': 0, + 'disk_avail': 10168} + + +def fake_get_powervm_operator(): + return FakeIVMOperator() + + +class PowerVMDriverTestCase(test.TestCase): + """Unit tests for PowerVM connection calls.""" + + def setUp(self): + super(PowerVMDriverTestCase, self).setUp() + self.stubs.Set(operator, 'get_powervm_operator', + fake_get_powervm_operator) + self.powervm_connection = powervm_driver.PowerVMDriver() + self.instance = self._create_instance() + + def _create_instance(self): + return db.instance_create(context.get_admin_context(), + {'user_id': 'fake', + 'project_id': 'fake', + 'instance_type_id': 1, + 'memory_mb': 1024, + 'vcpus': 2}) + + def test_list_instances(self): + instances = self.powervm_connection.list_instances() + self.assertTrue('instance-00000001' in instances) + self.assertTrue('instance-00000002' in instances) + + def test_instance_exists(self): + name = self.instance['name'] + self.assertTrue(self.powervm_connection.instance_exists(name)) + + def test_spawn(self): + def fake_image_fetch_to_raw(context, image_id, file_path, + user_id, project_id): + pass + self.flags(powervm_img_local_path='/images/') + self.stubs.Set(images, 'fetch_to_raw', fake_image_fetch_to_raw) + image_meta = {} + image_meta['id'] = '666' + self.powervm_connection.spawn(context.get_admin_context(), + self.instance, image_meta) + state = self.powervm_connection.get_info(self.instance)['state'] + self.assertEqual(state, power_state.RUNNING) + + def test_destroy(self): + self.powervm_connection.destroy(self.instance, None) + self.stubs.Set(FakeIVMOperator, 'get_lpar', lambda x, y: None) + name = self.instance['name'] + self.assertFalse(self.powervm_connection.instance_exists(name)) + + def test_get_info(self): + info = self.powervm_connection.get_info(self.instance) + self.assertEqual(info['state'], power_state.RUNNING) + self.assertEqual(info['max_mem'], 2048) + self.assertEqual(info['mem'], 1024) + self.assertEqual(info['num_cpu'], 2) + self.assertEqual(info['cpu_time'], 939395) diff --git a/nova/virt/powervm/__init__.py b/nova/virt/powervm/__init__.py new file mode 100644 index 000000000000..83bbcd289938 --- /dev/null +++ b/nova/virt/powervm/__init__.py @@ -0,0 +1,29 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 IBM +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""A connection to an IBM PowerVM Virtualization system. + +The driver connects to an Integrated Virtualization Manager (IVM) to +perform PowerVM Logical Partition (LPAR) deployment and management. + +For more detailed information about PowerVM virtualization, +refer to the IBM Redbook[1] publication. + +[1] IBM Redbook. IBM PowerVM Virtualization Introduction and Configuration. + May 2011. +""" + +from nova.virt.powervm.driver import PowerVMDriver diff --git a/nova/virt/powervm/command.py b/nova/virt/powervm/command.py new file mode 100644 index 000000000000..8cef5b72862d --- /dev/null +++ b/nova/virt/powervm/command.py @@ -0,0 +1,90 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 IBM +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +"""PowerVM manager commands.""" + + +class BaseCommand(object): + + def lsvg(self, args=''): + return 'lsvg %s' % args + + def mklv(self, args=''): + return 'mklv %s' % args + + def rmdev(self, args=''): + return 'rmdev %s' % args + + def rmvdev(self, args=''): + return 'rmvdev %s' % args + + def lsmap(self, args=''): + return 'lsmap %s' % args + + def lsdev(self, args=''): + return 'lsdev %s' % args + + def rmsyscfg(self, args=''): + return 'rmsyscfg %s' % args + + def chsysstate(self, args=''): + return 'chsysstate %s' % args + + def mksyscfg(self, args=''): + return 'mksyscfg %s' % args + + def lssyscfg(self, args=''): + return 'lssyscfg %s' % args + + def cfgdev(self, args=''): + return 'cfgdev %s' % args + + def mkvdev(self, args=''): + return 'mkvdev %s' % args + + def lshwres(self, args=''): + return 'lshwres %s' % args + + def vhost_by_instance_id(self, instance_id_hex): + pass + + +class IVMCommand(BaseCommand): + + def lsvg(self, args=''): + return 'ioscli ' + BaseCommand.lsvg(self, args) + + def mklv(self, args=''): + return 'ioscli ' + BaseCommand.mklv(self, args) + + def rmdev(self, args=''): + return 'ioscli ' + BaseCommand.rmdev(self, args) + + def rmvdev(self, args=''): + return 'ioscli ' + BaseCommand.rmvdev(self, args=args) + + def lsmap(self, args=''): + return 'ioscli ' + BaseCommand.lsmap(self, args) + + def lsdev(self, args=''): + return 'ioscli ' + BaseCommand.lsdev(self, args) + + def cfgdev(self, args=''): + return 'ioscli ' + BaseCommand.cfgdev(self, args=args) + + def mkvdev(self, args=''): + return 'ioscli ' + BaseCommand.mkvdev(self, args=args) diff --git a/nova/virt/powervm/common.py b/nova/virt/powervm/common.py new file mode 100644 index 000000000000..179bd7f14e1b --- /dev/null +++ b/nova/virt/powervm/common.py @@ -0,0 +1,112 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 IBM +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import ftplib +import os + +import paramiko + +from nova import exception as nova_exception +from nova.openstack.common import log as logging +from nova.virt.powervm import exception + +LOG = logging.getLogger(__name__) + + +class Connection(object): + + def __init__(self, host, username, password, port=22): + self.host = host + self.username = username + self.password = password + self.port = port + + +def ssh_connect(connection): + """Method to connect to remote system using ssh protocol. + + :param connection: a Connection object. + :returns: paramiko.SSHClient -- an active ssh connection. + :raises: PowerVMConnectionFailed + """ + try: + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(connection.host, + username=connection.username, + password=connection.password, + port=connection.port) + return ssh + except Exception: + LOG.exception(_('Connection error connecting PowerVM manager')) + raise exception.PowerVMConnectionFailed() + + +def ssh_command_as_root(ssh_connection, cmd, check_exit_code=True): + """Method to execute remote command as root. + + :param connection: an active paramiko.SSHClient connection. + :param command: string containing the command to run. + :returns: Tuple -- a tuple of (stdout, stderr) + :raises: nova.exception.ProcessExecutionError + """ + chan = ssh_connection._transport.open_session() + # This command is required to be executed + # in order to become root. + chan.exec_command('ioscli oem_setup_env') + bufsize = -1 + stdin = chan.makefile('wb', bufsize) + stdout = chan.makefile('rb', bufsize) + stderr = chan.makefile_stderr('rb', bufsize) + # We run the command and then call 'exit' to exit from + # super user environment. + stdin.write('%s\n%s\n' % (cmd, 'exit')) + stdin.flush() + exit_status = chan.recv_exit_status() + + # Lets handle the error just like nova.utils.ssh_execute does. + if exit_status != -1: + LOG.debug(_('Result was %s') % exit_status) + if check_exit_code and exit_status != 0: + raise nova_exception.ProcessExecutionError(exit_code=exit_status, + stdout=stdout, + stderr=stderr, + cmd=' '.join(cmd)) + + return (stdout, stderr) + + +def ftp_put_command(connection, local_path, remote_dir): + """Method to transfer a file via ftp. + + :param connection: a Connection object. + :param local_path: path to the local file + :param remote_dir: path to remote destination + :raises: PowerVMFileTransferFailed + """ + try: + ftp = ftplib.FTP(host=connection.host, + user=connection.username, + passwd=connection.password) + ftp.cwd(remote_dir) + name = os.path.split(local_path)[1] + f = open(local_path, "rb") + ftp.storbinary("STOR " + name, f) + f.close() + ftp.close() + except Exception: + LOG.exception(_('File transfer to PowerVM manager failed')) + raise exception.PowerVMFileTransferFailed(file_path=local_path) diff --git a/nova/virt/powervm/constants.py b/nova/virt/powervm/constants.py new file mode 100644 index 000000000000..1990ec5a50ad --- /dev/null +++ b/nova/virt/powervm/constants.py @@ -0,0 +1,35 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 IBM +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova.compute import power_state + +POWERVM_NOSTATE = '' +POWERVM_RUNNING = 'Running' +POWERVM_SHUTDOWN = 'Not Activated' +POWERVM_POWER_STATE = { + POWERVM_NOSTATE: power_state.NOSTATE, + POWERVM_RUNNING: power_state.RUNNING, + POWERVM_SHUTDOWN: power_state.SHUTDOWN, +} + +POWERVM_CPU_INFO = ('ppc64', 'powervm', '3940') +POWERVM_HYPERVISOR_TYPE = 'powervm' +POWERVM_HYPERVISOR_VERSION = '7.1' + +POWERVM_MIN_MEM = 512 +POWERVM_MAX_MEM = 1024 +POWERVM_MAX_CPUS = 1 +POWERVM_MIN_CPUS = 1 diff --git a/nova/virt/powervm/driver.py b/nova/virt/powervm/driver.py new file mode 100644 index 000000000000..66fd8929eb41 --- /dev/null +++ b/nova/virt/powervm/driver.py @@ -0,0 +1,214 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 IBM +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova.compute import task_states +from nova.compute import vm_states + +from nova import context as nova_context +from nova import db +from nova import flags + +from nova.openstack.common import cfg +from nova.openstack.common import log as logging + +from nova.virt import driver +from nova.virt.powervm import operator + + +LOG = logging.getLogger(__name__) + +powervm_opts = [ + cfg.StrOpt('powervm_mgr_type', + default='ivm', + help='PowerVM manager type (ivm, hmc)'), + cfg.StrOpt('powervm_mgr', + default=None, + help='PowerVM manager host or ip'), + cfg.StrOpt('powervm_vios', + default='powervm_mgr', + help='PowerVM vios host or ip if different from manager'), + cfg.StrOpt('powervm_mgr_user', + default=None, + help='PowerVM manager user name'), + cfg.StrOpt('powervm_mgr_passwd', + default=None, + help='PowerVM manager user password'), + cfg.StrOpt('powervm_img_remote_path', + default=None, + help='PowerVM image remote path'), + cfg.StrOpt('powervm_img_local_path', + default=None, + help='Local directory to download glance images to'), + ] + +FLAGS = flags.FLAGS +FLAGS.register_opts(powervm_opts) + + +class PowerVMDriver(driver.ComputeDriver): + + """PowerVM Implementation of Compute Driver.""" + + def __init__(self): + super(PowerVMDriver, self).__init__() + self._powervm = operator.PowerVMOperator() + + @property + def host_state(self): + pass + + def init_host(self, host): + """Initialize anything that is necessary for the driver to function, + including catching up with currently running VM's on the given host.""" + context = nova_context.get_admin_context() + instances = db.instance_get_all_by_host(context, host) + powervm_instances = self.list_instances() + # Looks for db instances that don't exist on the host side + # and cleanup the inconsistencies. + for db_instance in instances: + task_state = db_instance['task_state'] + if db_instance['name'] in powervm_instances: + continue + if task_state in [task_states.DELETING, task_states.SPAWNING]: + db.instance_update(context, db_instance['uuid'], + {'vm_state': vm_states.DELETED, + 'task_state': None}) + db.instance_destroy(context, db_instance['uuid']) + + def get_info(self, instance): + """Get the current status of an instance.""" + return self._powervm.get_info(instance['name']) + + def get_num_instances(self): + return len(self.list_instances()) + + def instance_exists(self, instance_name): + return self._powervm.instance_exists(instance_name) + + def list_instances(self): + return self._powervm.list_instances() + + def list_instances_detail(self): + """Return a list of InstanceInfo for all registered VMs""" + infos = [] + for instance_name in self.list_instances(): + state = self._powervm.get_info(instance_name)['state'] + infos.append(driver.InstanceInfo(instance_name, state)) + return infos + + def get_host_stats(self, refresh=False): + """Return currently known host stats""" + return self._powervm.get_host_stats(refresh=refresh) + + def plug_vifs(self, instance, network_info): + pass + + def spawn(self, context, instance, image_meta, + network_info=None, block_device_info=None): + """ + Create a new instance/VM/domain on powerVM. + + :param context: security context + :param instance: Instance object as returned by DB layer. + This function should use the data there to guide + the creation of the new instance. + :param image_meta: image object returned by nova.image.glance that + defines the image from which to boot this instance + :param network_info: + :py:meth:`~nova.network.manager.NetworkManager.get_instance_nw_info` + :param block_device_info: Information about block devices to be + attached to the instance. + """ + self._powervm.spawn(context, instance, image_meta['id']) + + def destroy(self, instance, network_info, block_device_info=None): + """Destroy (shutdown and delete) the specified instance.""" + self._powervm.destroy(instance['name']) + + def reboot(self, instance, network_info, reboot_type): + """Reboot the specified instance. + + :param instance: Instance object as returned by DB layer. + :param network_info: + :py:meth:`~nova.network.manager.NetworkManager.get_instance_nw_info` + :param reboot_type: Either a HARD or SOFT reboot + """ + # TODO(Vek): Need to pass context in for access to auth_token + pass + + def get_host_ip_addr(self): + """ + Retrieves the IP address of the dom0 + """ + pass + + def pause(self, instance): + """Pause the specified instance.""" + pass + + def unpause(self, instance): + """Unpause paused VM instance""" + pass + + def suspend(self, instance): + """suspend the specified instance""" + pass + + def resume(self, instance): + """resume the specified instance""" + pass + + def power_off(self, instance): + """Power off the specified instance.""" + self._powervm.power_off(instance['name']) + + def power_on(self, instance): + """Power on the specified instance""" + self._powervm.power_on(instance['name']) + + def update_available_resource(self, ctxt, host): + """Updates compute manager resource info on ComputeNode table. + + This method is called when nova-compute launches, and + whenever admin executes "nova-manage service update_resource". + + :param ctxt: security context + :param host: hostname that compute manager is currently running + + """ + pass + + def host_power_action(self, host, action): + """Reboots, shuts down or powers up the host.""" + pass + + def legacy_nwinfo(self): + """ + Indicate if the driver requires the legacy network_info format. + """ + # TODO(tr3buchet): update all subclasses and remove this + return False + + def manage_image_cache(self, context): + """ + Manage the driver's local image cache. + + Some drivers chose to cache images for instances on disk. This method + is an opportunity to do management of that cache which isn't directly + related to other calls into the driver. The prime example is to clean + the cache and remove images which are no longer of interest. + """ + pass diff --git a/nova/virt/powervm/exception.py b/nova/virt/powervm/exception.py new file mode 100644 index 000000000000..1ced07cfe278 --- /dev/null +++ b/nova/virt/powervm/exception.py @@ -0,0 +1,59 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 IBM +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova import exception + + +class PowerVMConnectionFailed(exception.NovaException): + message = _('Connection to PowerVM manager failed') + + +class PowerVMFileTransferFailed(exception.NovaException): + message = _("File '%(file_path)' transfer to PowerVM manager failed") + + +class PowerVMLPARInstanceNotFound(exception.NovaException): + message = _("LPAR instance '%(instance_name)s' could not be found") + + +class PowerVMLPARCreationFailed(exception.NovaException): + message = _("LPAR instance '%(instance_name)s' creation failed") + + +class PowerVMNoSpaceLeftOnVolumeGroup(exception.NovaException): + message = _("No space left on any volume group") + + +class PowerVMLPARAttributeNotFound(exception.NovaException): + pass + + +class PowerVMImageCreationFailed(exception.NovaException): + message = _("Image creation failed on PowerVM") + + +class PowerVMInsufficientFreeMemory(exception.NovaException): + message = _("Insufficient free memory on PowerVM system to spawn instance " + "'%(instance_name)s'") + + +class PowerVMInsufficientCPU(exception.NovaException): + message = _("Insufficient available CPUs on PowerVM system to spawn " + "instance '%(instance_name)s'") + + +class PowerVMLPARInstanceCleanupFailed(exception.NovaException): + message = _("PowerVM LPAR instance '%(instance_name)s' cleanup failed") diff --git a/nova/virt/powervm/lpar.py b/nova/virt/powervm/lpar.py new file mode 100644 index 000000000000..10e8c8e37648 --- /dev/null +++ b/nova/virt/powervm/lpar.py @@ -0,0 +1,158 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 IBM +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""PowerVM Logical Partition (LPAR) + +PowerVM LPAR configuration attributes. +""" + +import shlex + +from nova.openstack.common import log as logging +from nova.virt.powervm import exception + +LOG = logging.getLogger(__name__) + + +def load_from_conf_data(conf_data): + """LPAR configuration data parser. + + The configuration data is a string representation of + the attributes of a Logical Partition. The attributes + consists of name/value pairs, which are in command separated + value format. + Example format: name=lpar_name,lpar_id=1,lpar_env=aixlinux + + :param conf_data: string containing the LPAR configuration data. + :returns: LPAR -- LPAR object. + """ + # config_data can contain comma separated values within + # double quotes, example: virtual_serial_adapters + # and virtual_scsi_adapters attributes. So can't simply + # split them by ','. + cf_splitter = shlex.shlex(conf_data, posix=True) + cf_splitter.whitespace = ',' + cf_splitter.whitespace_split = True + attribs = dict(item.split("=") for item in list(cf_splitter)) + lpar = LPAR() + for (key, value) in attribs.items(): + lpar[key] = value + return lpar + + +class LPAR(object): + + """ + Simple class representing a logical partition and the attributes + for the partition and/or its selected profile. + """ + + # Attributes for all logical partitions + LPAR_ATTRS = ( + 'name', + 'lpar_id', + 'lpar_env', + 'state', + 'resource_config', + 'os_version', + 'logical_serial_num', + 'default_profile', + 'profile_name', + 'curr_profile', + 'work_group_id', + 'allow_perf_collection', + 'power_ctrl_lpar_ids', + 'boot_mode', + 'lpar_keylock', + 'auto_start', + 'uptime', + 'lpar_avail_priority', + 'desired_lpar_proc_compat_mode', + 'curr_lpar_proc_compat_mode', + 'virtual_eth_mac_base_value', + 'rmc_ipaddr' + ) + + # Logical partitions may contain one or more profiles, which + # may have the following attributes + LPAR_PROFILE_ATTRS = ( + 'name', + 'lpar_name', + 'lpar_id', + 'os_type', + 'all_resources', + 'mem_mode', + 'min_mem', + 'desired_mem', + 'max_mem', + 'proc_mode', + 'min_proc_units', + 'desired_proc_units', + 'max_proc_units', + 'min_procs', + 'desired_procs', + 'max_procs', + 'sharing_mode', + 'uncap_weight', + 'io_slots', + 'lpar_io_pool_ids', + 'max_virtual_slots', + 'virtual_serial_adapters', + 'virtual_scsi_adapters', + 'virtual_eth_adapters', + 'boot_mode', + 'conn_monitoring', + 'auto_start', + 'power_ctrl_lpar_ids', + 'lhea_logical_ports', + 'lhea_capabilities', + 'lpar_proc_compat_mode', + 'virtual_fc_adapters' + ) + + def __init__(self, **kwargs): + self.attributes = dict([k, None] for k in self.LPAR_ATTRS) + self.profile_attributes = dict([k, None] for k + in self.LPAR_PROFILE_ATTRS) + self.attributes.update(kwargs) + self.profile_attributes.update(kwargs) + self.all_attrs = dict(self.attributes.items() + + self.profile_attributes.items()) + + def __getitem__(self, key): + if key not in self.all_attrs.keys(): + raise exception.PowerVMLPARAttributeNotFound(key) + return self.all_attrs.get(key) + + def __setitem__(self, key, value): + if key not in self.all_attrs.keys(): + raise exception.PowerVMLPARAttributeNotFound(key) + self.all_attrs[key] = value + + def __delitem__(self, key): + if key not in self.all_attrs.keys(): + raise exception.PowerVMLPARAttributeNotFound(key) + # We set to None instead of removing the key... + self.all_attrs[key] = None + + def to_string(self, exclude_attribs=[]): + conf_data = [] + for (key, value) in self.all_attrs.items(): + if key in exclude_attribs or value is None: + continue + conf_data.append('%s=%s' % (key, value)) + + return ','.join(conf_data) diff --git a/nova/virt/powervm/operator.py b/nova/virt/powervm/operator.py new file mode 100644 index 000000000000..110fae4fb1a2 --- /dev/null +++ b/nova/virt/powervm/operator.py @@ -0,0 +1,631 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 IBM +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import decimal +import hashlib +import os +import re +import time + +from nova import exception as nova_exception +from nova import flags +from nova import utils + +from nova.compute import power_state +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 +from nova.virt.powervm import constants +from nova.virt.powervm import exception +from nova.virt.powervm import lpar as LPAR + + +LOG = logging.getLogger(__name__) +FLAGS = flags.FLAGS + + +def get_powervm_operator(): + if FLAGS.powervm_mgr_type == 'ivm': + return IVMOperator(common.Connection(FLAGS.powervm_mgr, + FLAGS.powervm_mgr_user, + FLAGS.powervm_mgr_passwd)) + + +class PowerVMOperator(object): + """PowerVM main operator. + + The PowerVMOperator is intented to wrapper all operations + from the driver and handle either IVM or HMC managed systems. + """ + + def __init__(self): + self._operator = get_powervm_operator() + self._host_stats = {} + self._update_host_stats() + + def get_info(self, instance_name): + """Get the current status of an LPAR instance. + + Returns a dict containing: + + :state: the running state, one of the power_state codes + :max_mem: (int) the maximum memory in KBytes allowed + :mem: (int) the memory in KBytes used by the domain + :num_cpu: (int) the number of virtual CPUs for the domain + :cpu_time: (int) the CPU time used in nanoseconds + + :raises: PowerVMLPARInstanceNotFound + """ + lpar_instance = self._get_instance(instance_name) + + state = constants.POWERVM_POWER_STATE[lpar_instance['state']] + return {'state': state, + 'max_mem': lpar_instance['max_mem'], + 'mem': lpar_instance['desired_mem'], + 'num_cpu': lpar_instance['max_procs'], + 'cpu_time': lpar_instance['uptime']} + + def instance_exists(self, instance_name): + lpar_instance = self._operator.get_lpar(instance_name) + return True if lpar_instance else False + + def _get_instance(self, instance_name): + """Check whether or not the LPAR instance exists and return it.""" + lpar_instance = self._operator.get_lpar(instance_name) + + if lpar_instance is None: + LOG.exception(_("LPAR instance '%s' not found") % instance_name) + raise exception.PowerVMLPARInstanceNotFound( + instance_name=instance_name) + return lpar_instance + + def list_instances(self): + """ + Return the names of all the instances known to the virtualization + layer, as a list. + """ + lpar_instances = self._operator.list_lpar_instances() + # We filter out instances that haven't been created + # via Openstack. Notice that this is fragile and it can + # be improved later. + instances = [instance for instance in lpar_instances + if re.search(r'^instance-[0-9]{8}$', instance)] + return instances + + def get_host_stats(self, refresh=False): + """Return currently known host stats""" + if refresh: + self._update_host_stats() + return self._host_stats + + def _update_host_stats(self): + memory_info = self._operator.get_memory_info() + cpu_info = self._operator.get_cpu_info() + + # Note: disk avail information is not accurate. The value + # is a sum of all Volume Groups and the result cannot + # represent the real possibility. Example: consider two + # VGs both 10G, the avail disk will be 20G however, + # a 15G image does not fit in any VG. This can be improved + # later on. + disk_info = self._operator.get_disk_info() + + data = {} + data['vcpus'] = cpu_info['total_procs'] + data['vcpus_used'] = cpu_info['total_procs'] - cpu_info['avail_procs'] + data['cpu_info'] = constants.POWERVM_CPU_INFO + data['disk_total'] = disk_info['disk_total'] + data['disk_used'] = disk_info['disk_used'] + data['disk_available'] = disk_info['disk_avail'] + data['host_memory_total'] = memory_info['total_mem'] + data['host_memory_free'] = memory_info['avail_mem'] + data['hypervisor_type'] = constants.POWERVM_HYPERVISOR_TYPE + data['hypervisor_version'] = constants.POWERVM_HYPERVISOR_VERSION + data['extres'] = '' + + self._host_stats = data + + def spawn(self, context, instance, image_id): + def _create_lpar_instance(instance): + host_stats = self.get_host_stats(refresh=True) + 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 mem > host_stats['host_memory_free']: + LOG.exception(_('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'] + avail_cpus = host_stats['vcpus'] - host_stats['vcpus_used'] + if cpus > avail_cpus: + LOG.exception(_('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) + + try: + # 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) + + LOG.debug(_("Creating LPAR instance '%s'") % instance['name']) + self._operator.create_lpar(lpar_inst) + except nova_exception.ProcessExecutionError: + LOG.exception(_("LPAR instance '%s' creation failed") % + instance['name']) + raise exception.PowerVMLPARCreationFailed() + + def _create_image(context, instance, image_id): + """Fetch image from glance and copy it to the remote system.""" + try: + file_name = '.'.join([image_id, 'gz']) + file_path = os.path.join(FLAGS.powervm_img_local_path, + file_name) + LOG.debug(_("Fetching image '%s' from glance") % image_id) + images.fetch_to_raw(context, image_id, file_path, + instance['user_id'], + project_id=instance['project_id']) + LOG.debug(_("Copying image '%s' to IVM") % file_path) + remote_path = FLAGS.powervm_img_remote_path + remote_file_name, size = self._operator.copy_image_file( + file_path, remote_path) + # Logical volume + LOG.debug(_("Creating logical volume")) + lpar_id = self._operator.get_lpar(instance['name'])['lpar_id'] + vhost = self._operator.get_vhost_by_instance_id(lpar_id) + disk_name = self._operator.create_logical_volume(size) + self._operator.attach_disk_to_vhost(disk_name, vhost) + LOG.debug(_("Copying image to the device '%s'") % disk_name) + self._operator.copy_file_to_device(remote_file_name, disk_name) + except Exception, e: + LOG.exception(_("PowerVM image creation failed: %s") % str(e)) + raise exception.PowerVMImageCreationFailed() + + try: + _create_lpar_instance(instance) + _create_image(context, instance, image_id) + LOG.debug(_("Activating the LPAR instance '%s'") + % instance['name']) + self._operator.start_lpar(instance['name']) + + # Wait for boot + timeout_count = range(10) + while timeout_count: + state = self.get_info(instance['name'])['state'] + if state == power_state.RUNNING: + LOG.info(_("Instance spawned successfully."), + instance=instance) + break + timeout_count.pop() + if len(timeout_count) == 0: + LOG.error(_("Instance '%s' failed to boot") % + instance['name']) + self._cleanup(instance['name']) + break + time.sleep(1) + + except exception.PowerVMImageCreationFailed: + self._cleanup(instance['name']) + + def destroy(self, instance_name): + """Destroy (shutdown and delete) the specified instance. + + :param instance_name: Instance name. + """ + try: + self._cleanup(instance_name) + except exception.PowerVMLPARInstanceNotFound: + LOG.warn(_("During destroy, LPAR instance '%s' was not found on " + "PowerVM system.") % instance_name) + + def _cleanup(self, instance_name): + try: + lpar_id = self._get_instance(instance_name)['lpar_id'] + vhost = self._operator.get_vhost_by_instance_id(lpar_id) + disk_name = self._operator.get_disk_name_by_vhost(vhost) + + LOG.debug(_("Shutting down the instance '%s'") % instance_name) + self._operator.stop_lpar(instance_name) + + if disk_name: + LOG.debug(_("Removing the logical volume '%s'") % disk_name) + self._operator.remove_logical_volume(disk_name) + + LOG.debug(_("Deleting the LPAR instance '%s'") % instance_name) + self._operator.remove_lpar(instance_name) + except Exception: + LOG.exception(_("PowerVM instance cleanup failed")) + raise exception.PowerVMLPARInstanceCleanupFailed( + instance_name=instance_name) + + def power_off(self, instance_name): + self._operator.stop(instance_name) + + def power_on(self, instance_name): + self._operator.start(instance_name) + + +class BaseOperator(object): + """Base operator for IVM and HMC managed systems.""" + + def __init__(self, connection): + """Constructor. + + :param connection: common.Connection object with the + information to connect to the remote + ssh. + """ + self._connection = common.ssh_connect(connection) + self.connection_data = connection + + def get_lpar(self, instance_name, resource_type='lpar'): + """Return a LPAR object by its instance name. + + :param instance_name: LPAR instance name + :param resource_type: the type of resources to list + :returns: LPAR object + """ + cmd = self.command.lssyscfg('-r %s --filter "lpar_names=%s"' + % (resource_type, instance_name)) + output = self.run_command(cmd) + if not output: + return None + lpar = LPAR.load_from_conf_data(output[0]) + return lpar + + def list_lpar_instances(self): + """List all existent LPAR instances names. + + :returns: list -- list with instances names. + """ + lpar_names = self.run_command(self.command.lssyscfg('-r lpar -F name')) + if not lpar_names: + return [] + return lpar_names + + def create_lpar(self, lpar): + """Receives a LPAR data object and creates a LPAR instance. + + :param lpar: LPAR object + """ + conf_data = lpar.to_string() + self.run_command(self.command.mksyscfg('-r lpar -i "%s"' % conf_data)) + + def start_lpar(self, instance_name): + """Start a LPAR instance. + + :param instance_name: LPAR instance name + """ + self.run_command(self.command.chsysstate('-r lpar -o on -n %s' + % instance_name)) + + def stop_lpar(self, instance_name): + """Stop a running LPAR. + + :param instance_name: LPAR instance name + """ + cmd = self.command.chsysstate('-r lpar -o shutdown --immed -n %s' + % instance_name) + self.run_command(cmd) + + def remove_lpar(self, instance_name): + """Removes a LPAR. + + :param instance_name: LPAR instance name + """ + self.run_command(self.command.rmsyscfg('-r lpar -n %s' + % instance_name)) + + def get_vhost_by_instance_id(self, instance_id): + """Return the vhost name by the instance id. + + :param instance_id: LPAR instance id + :returns: string -- vhost name or None in case none is found + """ + instance_hex_id = '%#010x' % int(instance_id) + cmd = self.command.lsmap('-all -field clientid svsa -fmt :') + output = self.run_command(cmd) + vhosts = dict(item.split(':') for item in list(output)) + + if instance_hex_id in vhosts: + return vhosts[instance_hex_id] + + return None + + def get_virtual_eth_adapter_id(self): + """Virtual ethernet adapter id. + + Searches for the shared ethernet adapter and returns + its id. + + :returns: id of the virtual ethernet adapter. + """ + cmd = self.command.lsmap('-all -net -field sea -fmt :') + output = self.run_command(cmd) + sea = output[0] + cmd = self.command.lsdev('-dev %s -attr pvid' % sea) + output = self.run_command(cmd) + # Returned output looks like this: ['value', '', '1'] + if output: + return output[2] + + return None + + def get_disk_name_by_vhost(self, vhost): + """Returns the disk name attached to a vhost. + + :param vhost: a vhost name + :returns: string -- disk name + """ + cmd = self.command.lsmap('-vadapter %s -field backing -fmt :' + % vhost) + output = self.run_command(cmd) + if output: + return output[0] + + return None + + def remove_disk(self, disk_name): + """Removes a disk. + + :param disk: a disk name + """ + self.run_command(self.command.rmdev('-dev %s' % disk_name)) + + def create_logical_volume(self, size): + """Creates a logical volume with a minimum size. + + :param size: size of the logical volume in bytes + :returns: string -- the name of the new logical volume. + :raises: PowerVMNoSpaceLeftOnVolumeGroup + """ + vgs = self.run_command(self.command.lsvg()) + cmd = self.command.lsvg('%s -field vgname freepps -fmt :' + % ' '.join(vgs)) + output = self.run_command(cmd) + found_vg = None + + # If it's not a multiple of 1MB we get the next + # multiple and use it as the megabyte_size. + megabyte = 1024 * 1024 + if (size % megabyte) != 0: + megabyte_size = int(size / megabyte) + 1 + else: + megabyte_size = size / megabyte + + # Search for a volume group with enough free space for + # the new logical volume. + for vg in output: + # Returned output example: 'rootvg:396 (25344 megabytes)' + match = re.search(r'^(\w+):\d+\s\((\d+).+$', vg) + if match is None: + continue + vg_name, avail_size = match.groups() + if megabyte_size <= int(avail_size): + found_vg = vg_name + break + + if not found_vg: + LOG.exception(_('Could not create logical volume. ' + 'No space left on any volume group.')) + raise exception.PowerVMNoSpaceLeftOnVolumeGroup() + + cmd = self.command.mklv('%s %sB' % (found_vg, size / 512)) + lv_name, = self.run_command(cmd) + return lv_name + + def remove_logical_volume(self, lv_name): + """Removes the lv and the connection between its associated vscsi. + + :param lv_name: a logical volume name + """ + cmd = self.command.rmvdev('-vdev %s -rmlv' % lv_name) + self.run_command(cmd) + + def copy_file_to_device(self, source_path, device): + """Copy file to device. + + :param source_path: path to input source file + :param device: output device name + """ + cmd = 'dd if=%s of=/dev/%s bs=1024k' % (source_path, device) + self.run_command_as_root(cmd) + + def copy_image_file(self, source_path, remote_path): + """Copy file to VIOS, decompress it, and return its new size and name. + + :param source_path: source file path + :param remote_path remote file path + """ + # Calculate source image checksum + hasher = hashlib.md5() + block_size = 0x10000 + img_file = file(source_path, 'r') + buf = img_file.read(block_size) + while len(buf) > 0: + hasher.update(buf) + buf = img_file.read(block_size) + source_cksum = hasher.hexdigest() + + comp_path = remote_path + os.path.basename(source_path) + uncomp_path = comp_path.rstrip(".gz") + final_path = "%s.%s" % (uncomp_path, source_cksum) + + # Check whether the uncompressed image is already on IVM + output = self.run_command("ls %s" % final_path, check_exit_code=False) + + # If the image does not exist already + if not len(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_command_as_root(cmd) + if not len(output): + LOG.exception("Unable to get checksum") + raise exception.PowerVMFileTransferFailed() + if source_cksum != output[0]: + LOG.exception("Image checksums do not match") + raise exception.PowerVMFileTransferFailed() + + # Unzip the image + cmd = "/usr/bin/gunzip %s" % comp_path + output = self.run_command_as_root(cmd) + + # Remove existing image file + cmd = "/usr/bin/rm -f %s.*" % uncomp_path + output = self.run_command_as_root(cmd) + + # Rename unzipped image + cmd = "/usr/bin/mv %s %s" % (uncomp_path, final_path) + output = self.run_command_as_root(cmd) + + # Remove compressed image file + cmd = "/usr/bin/rm -f %s" % comp_path + output = self.run_command_as_root(cmd) + + # Calculate file size in multiples of 512 bytes + output = self.run_command("ls -o %s|awk '{print $4}'" + % final_path, check_exit_code=False) + if len(output): + size = int(output[0]) + else: + LOG.exception("Uncompressed image file not found") + raise exception.PowerVMFileTransferFailed() + if (size % 512 != 0): + size = (int(size / 512) + 1) * 512 + + return final_path, size + + def run_cfg_dev(self, device_name): + """Run cfgdev command for a specific device. + + :param device_name: device name the cfgdev command will run. + """ + cmd = self.command.cfgdev('-dev %s' % device_name) + self.run_command(cmd) + + def attach_disk_to_vhost(self, disk, vhost): + """Attach disk name to a specific vhost. + + :param disk: the disk name + :param vhost: the vhost name + """ + cmd = self.command.mkvdev('-vdev %s -vadapter %s') % (disk, vhost) + self.run_command(cmd) + + def get_memory_info(self): + """Get memory info. + + :returns: tuple - memory info (total_mem, avail_mem) + """ + cmd = self.command.lshwres( + '-r mem --level sys -F configurable_sys_mem,curr_avail_sys_mem') + output = self.run_command(cmd) + total_mem, avail_mem = output[0].split(',') + return {'total_mem': int(total_mem), + 'avail_mem': int(avail_mem)} + + def get_cpu_info(self): + """Get CPU info. + + :returns: tuple - cpu info (total_procs, avail_procs) + """ + cmd = self.command.lshwres( + '-r proc --level sys -F ' + 'configurable_sys_proc_units,curr_avail_sys_proc_units') + output = self.run_command(cmd) + total_procs, avail_procs = output[0].split(',') + return {'total_procs': float(total_procs), + 'avail_procs': float(avail_procs)} + + def get_disk_info(self): + """Get the disk usage information. + + :returns: tuple - disk info (disk_total, disk_used, disk_avail) + """ + vgs = self.run_command(self.command.lsvg()) + (disk_total, disk_used, disk_avail) = [0, 0, 0] + for vg in vgs: + cmd = self.command.lsvg('%s -field totalpps usedpps freepps -fmt :' + % vg) + output = self.run_command(cmd) + # Output example: + # 1271 (10168 megabytes):0 (0 megabytes):1271 (10168 megabytes) + (d_total, d_used, d_avail) = re.findall(r'(\d+) megabytes', + output[0]) + disk_total += int(d_total) + disk_used += int(d_used) + disk_avail += int(d_avail) + + return {'disk_total': disk_total, + 'disk_used': disk_used, + 'disk_avail': disk_avail} + + def run_command(self, cmd, check_exit_code=True): + """Run a remote command using an active ssh connection. + + :param command: String with the command to run. + """ + stdout, stderr = utils.ssh_execute(self._connection, cmd, + check_exit_code=check_exit_code) + return stdout.strip().splitlines() + + def run_command_as_root(self, command, check_exit_code=True): + """Run a remote command as root using an active ssh connection. + + :param command: List of commands. + """ + stdout, stderr = common.ssh_command_as_root( + self._connection, command, check_exit_code=check_exit_code) + return stdout.read().splitlines() + + +class IVMOperator(BaseOperator): + """Integrated Virtualization Manager (IVM) Operator. + + Runs specific commands on an IVM managed system. + """ + + def __init__(self, ivm_connection): + self.command = command.IVMCommand() + BaseOperator.__init__(self, ivm_connection)