Add PowerVM compute driver and unit tests.
part of bp:powervm-compute-driver Change-Id: I9e55964b5260417d15b5def814ee305fce28c40b
This commit is contained in:
parent
9deb489a8b
commit
e1df9606c1
165
nova/tests/test_powervm.py
Normal file
165
nova/tests/test_powervm.py
Normal file
@ -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)
|
29
nova/virt/powervm/__init__.py
Normal file
29
nova/virt/powervm/__init__.py
Normal file
@ -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. <http://www.redbooks.ibm.com/abstracts/sg247940.html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
from nova.virt.powervm.driver import PowerVMDriver
|
90
nova/virt/powervm/command.py
Normal file
90
nova/virt/powervm/command.py
Normal file
@ -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)
|
112
nova/virt/powervm/common.py
Normal file
112
nova/virt/powervm/common.py
Normal file
@ -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)
|
35
nova/virt/powervm/constants.py
Normal file
35
nova/virt/powervm/constants.py
Normal file
@ -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
|
214
nova/virt/powervm/driver.py
Normal file
214
nova/virt/powervm/driver.py
Normal file
@ -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
|
59
nova/virt/powervm/exception.py
Normal file
59
nova/virt/powervm/exception.py
Normal file
@ -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")
|
158
nova/virt/powervm/lpar.py
Normal file
158
nova/virt/powervm/lpar.py
Normal file
@ -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)
|
631
nova/virt/powervm/operator.py
Normal file
631
nova/virt/powervm/operator.py
Normal file
@ -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)
|
Loading…
Reference in New Issue
Block a user