add ironic baremetal driver

This commit is contained in:
chenglch 2014-08-01 05:06:02 -04:00
parent 48641cf945
commit b5fca17987
13 changed files with 1326 additions and 2 deletions

21
makerpm
View File

@ -106,7 +106,23 @@ function makexcat {
fi
}
# make ironic rpm for ironic baremetal driver
function makeironic {
RPMNAME="$1"
ARCH="$2"
cd `dirname $0`/$RPMNAME
cp -rf ironic_baremetal /tmp/
cd /tmp/ironic_baremetal
git init
git add *
git commit -a -m "generate rpm"
python setup.py bdist_rpm
rm -rf $RPMROOT/RPMS/$ARCH/
mkdir -p $RPMROOT/RPMS/$ARCH/
cp -rf dist/*.rpm $RPMROOT/RPMS/$ARCH/
rm -rf /tmp/ironic_baremetal
}
# Make the xCAT-nbroot-core rpm
function makenbroot {
@ -206,7 +222,6 @@ else # linux
fi
fi
if [ "$1" = "xCAT" -o "$1" = "xCATsn" -o "$1" = "xCAT-buildkit" -o "$1" = "xCAT-OpenStack" ]; then
exportEmbed $3
makexcat $1 $2
@ -219,6 +234,8 @@ elif [ "$1" = "xCAT-genesis-builder" ]; then
elif [ "$1" = "xCAT-genesis-scripts" ]; then
exportEmbed $3
makegenesisscripts $1 $2
elif [ "$1" = "xCAT-OpenStack-ironic" ]; then
makeironic $1 $2
else # must be one of the noarch rpms
exportEmbed $2
makenoarch $1

View File

@ -0,0 +1,6 @@
include AUTHORS
include ChangeLog
exclude .gitignore
exclude .gitreview
global-exclude *.pyc

View File

@ -0,0 +1,44 @@
xCAT Driver for ironic x86/64 machine
==================================
xCAT is a Extreme Cluster/Cloud Administration Toolkit. We can use xcat
to do :
1 hardward discoveery
2 remote hardware control
3 remote sonsole
4 hardware inventory
5 firmware flashing
Ironic is a project in Openstack, it will replace the nova-baremetal in juno release. Ironic's design is very flexable, we can add driver to extend function
without change any code in Openstack. Ironic xCAT driver takes the advantage of xcat and openstack, we can use it to deploy the baremetal machine very easily.
Before using this driver, we must setup the openstack environment at least for two nodes( ironic conductor and neutron network node can't setup on the same node)
Ironic conductor and the baremetal node( waiting for deploy) must in the same vlan
Add the follows in the ironic egg-info entry_points.txt file (ironic.drivers section)
pxe_xcat = ironic.drivers.xcat:XCATBaremetalDriver
When the openstack with ironic is ready, just execute command in the ironic_xcat directory as follows:
$ python setup.py install
Restart the ironic-conductor process
Initialize the xcat environment according to http://sourceforge.net/p/xcat/wiki/XCAT_iDataPlex_Cluster_Quick_Start/
Using xCAT baremetal driver need config site table and run copycds to generate image. The node definition is not requirement.
Ironic use neutron as the network service.
Check the openvswitch config on the network node ,make sure brbm bridge connect to the baremetal node.
==================================================================================
Some Example to use the xCAT baremetal driver.
$touch /tmp/rhelhpc6.5-x86_64-install-compute.qcow2;glance image-create --name rhelhpc6.5-x86_64-install-compute --public --disk-format qcow2 --container-format bare --property xcat_image_name='rhels6.4-x86_64-install-compute' < /tmp/rhelhpc6.5-x86_64-install-compute.qcow2
--name rhelhpc6.5-x86_64-install-compute is the image name in xcat. You can use lsdef -t osimage on the ironic-conductor node which xcat is installed.
$ ironic node-create --driver pxe_xcat -i ipmi_address=xxx.xxx.xxx.xxx -i ipmi_username=userid -i ipmi_password=password -i xcat_node=x3550m4n02 -i xcatmaster=10.1.0.241 -i netboot=xnba -i ipmi_terminal_port=0 -p memory_mb=2048 -p cpus=8
$ ironic port-create --address ff:ff:ff:ff:ff:ff --node_uuid <ironic node uuid>
$ nova boot --flavor baremetal --image <image-id> testing --nic net-id=<internal network id>

View File

@ -0,0 +1,25 @@
"""xCAT baremtal exceptions.
"""
from oslo.config import cfg
import six
from ironic.openstack.common.gettextutils import _
from ironic.openstack.common import log as logging
from ironic.common.exception import IronicException
LOG = logging.getLogger(__name__)
class xCATCmdFailure(IronicException):
message = _("xcat call failed: %(cmd)s %(node)s %(args)s.")
class xCATDeploymentFailure(IronicException):
message = _("xCAT node deployment failed for node %(node)s:%(error)s")
class GetNetworkFixedIPFailure(IronicException):
message = _("get fixed ip failed for mac %(mac_address)s")
class GetNetworkIdFailure(IronicException):
message = _("get node network in failed for mac %(mac_address)s")
class FailedToGetInfoOnPort(IronicException):
message = _("Show info on port: %(port_id)s failed.")

View File

@ -0,0 +1,41 @@
"""
Get the network from neutron
This is a xcat patch for the ironic/common/neutron.py
"""
from neutronclient.common import exceptions as neutron_client_exc
from ironic.common import exception
from ironic.openstack.common import log as logging
from ironic.common import neutron
from ironic.drivers.modules import xcat_exception
LOG = logging.getLogger(__name__)
def get_vif_port_info(task, port_id):
""" Get detail port info from neutron with a given port id """
api = neutron.NeutronAPI(task.context)
try:
port_info = api.client.show_port(port_id)
except neutron_client_exc.NeutronClientException:
LOG.exception(_("Failed to get port info %s."), port_id)
raise exception.FailedToGetInfoOnPort(port_id=port_id)
return port_info
def get_ports_info_from_neutron(task):
""" Get neutron port info from neutron about this task """
vifs = neutron.get_node_vif_ids(task)
if not vifs:
LOG.warning(_("No VIFs found for node %(node)s when attempting to "
"update Neutron DHCP BOOT options."),
{'node': task.node.uuid})
return
failures = []
vif_ports_info = {}
for port_id, port_vif in vifs.iteritems():
try:
vif_ports_info[port_id] = get_vif_port_info(task,port_vif)
except xcat_exception.FailedToGetInfoOnPort(port_id=port_vif):
failures.append(port_vif)
return vif_ports_info

View File

@ -0,0 +1,453 @@
"""
pxe procedure for the xcat baremetal driver
use xcat to config dhcp and tftp
"""
import os
import time
import paramiko
import datetime
from oslo.config import cfg
from ironic.common import exception
from ironic.common import image_service as service
from ironic.common import keystone
from ironic.common import states
from ironic.common import utils
from ironic.conductor import task_manager
from ironic.conductor import utils as manager_utils
from ironic.drivers import base
from ironic.drivers import utils as driver_utils
from ironic.openstack.common import log as logging
from ironic.openstack.common import strutils
from ironic.drivers.modules import xcat_neutron
from ironic.drivers.modules import xcat_util
from ironic.openstack.common import loopingcall
from nova.openstack.common import timeutils
from ironic.openstack.common import lockutils
from ironic.drivers.modules import xcat_exception
pxe_opts = [
cfg.StrOpt('pxe_append_params',
default='nofb nomodeset vga=normal',
help='Additional append parameters for baremetal PXE boot.'),
cfg.StrOpt('default_ephemeral_format',
default='ext4',
help='Default file system format for ephemeral partition, '
'if one is created.'),
]
xcat_opts = [
cfg.StrOpt('network_node_ip',
default='127.0.0.1',
help='IP address of neutron network node'),
cfg.StrOpt('ssh_user',
default='root',
help='Username of neutron network node.'),
cfg.StrOpt('ssh_password',
default='cluster',
help='Password of neutron network node'),
cfg.IntOpt('ssh_port',
default=22,
help='ssh connection port for the neutron '),
cfg.StrOpt('host_filepath',
default='/etc/hosts',
help='host file of server'),
cfg.IntOpt('deploy_timeout',
default=3600,
help='max depolyment time(seconds) for the xcat driver'),
cfg.IntOpt('deploy_checking_interval',
default=30,
help='interval time(seconds) to check the xcat deploy state'),
]
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
CONF.register_opts(pxe_opts, group='pxe')
CONF.register_opts(xcat_opts, group='xcat')
CONF.import_opt('use_ipv6', 'ironic.netconf')
EM_SEMAPHORE = 'xcat_pxe'
def _check_for_missing_params(info_dict, param_prefix=''):
missing_info = []
for label, value in info_dict.items():
if not value:
missing_info.append(param_prefix + label)
if missing_info:
raise exception.InvalidParameterValue(_(
"Can not validate PXE bootloader. The following parameters "
"were not passed to ironic: %s") % missing_info)
def _parse_driver_info(node):
"""Gets the driver specific Node deployment info.
This method validates whether the 'driver_info' property of the
supplied node contains the required information for this driver to
deploy images to the node.
:param node: a single Node.
:returns: A dict with the driver_info values.
"""
info = node.driver_info
d_info = {}
d_info['xcat_node'] = info.get('xcat_node')
return d_info
def _parse_instance_info(node):
"""Gets the instance specific Node deployment info.
This method validates whether the 'instance_info' property of the
supplied node contains the required information for this driver to
deploy images to the node.
:param node: a single Node.
:returns: A dict with the instance_info values.
"""
info = node.instance_info
i_info = {}
i_info['image_source'] = info.get('image_source')
i_info['root_gb'] = info.get('root_gb')
i_info['image_file'] = i_info['image_source']
_check_for_missing_params(i_info)
# Internal use only
i_info['deploy_key'] = info.get('deploy_key')
i_info['swap_mb'] = info.get('swap_mb', 0)
i_info['ephemeral_gb'] = info.get('ephemeral_gb', 0)
i_info['ephemeral_format'] = info.get('ephemeral_format')
err_msg_invalid = _("Can not validate PXE bootloader. Invalid parameter "
"%(param)s. Reason: %(reason)s")
for param in ('root_gb', 'swap_mb', 'ephemeral_gb'):
try:
int(i_info[param])
except ValueError:
reason = _("'%s' is not an integer value.") % i_info[param]
raise exception.InvalidParameterValue(err_msg_invalid %
{'param': param, 'reason': reason})
if i_info['ephemeral_gb'] and not i_info['ephemeral_format']:
i_info['ephemeral_format'] = CONF.pxe.default_ephemeral_format
preserve_ephemeral = info.get('preserve_ephemeral', False)
try:
i_info['preserve_ephemeral'] = strutils.bool_from_string(
preserve_ephemeral, strict=True)
except ValueError as e:
raise exception.InvalidParameterValue(err_msg_invalid %
{'param': 'preserve_ephemeral', 'reason': e})
return i_info
def _parse_deploy_info(node):
"""Gets the instance and driver specific Node deployment info.
This method validates whether the 'instance_info' and 'driver_info'
property of the supplied node contains the required information for
this driver to deploy images to the node.
:param node: a single Node.
:returns: A dict with the instance_info and driver_info values.
"""
info = {}
info.update(_parse_instance_info(node))
info.update(_parse_driver_info(node))
return info
def _validate_glance_image(ctx, deploy_info):
"""Validate the image in Glance.
Check if the image exist in Glance and if it contains the
'kernel_id' and 'ramdisk_id' properties.
:raises: InvalidParameterValue.
"""
image_id = deploy_info['image_source']
if not image_id:
raise exception.ImageNotFound
class PXEDeploy(base.DeployInterface):
"""PXE Deploy Interface: just a stub until the real driver is ported."""
def validate(self, task):
"""Validate the deployment information for the task's node.
:param task: a TaskManager instance containing the node to act on.
:raises: InvalidParameterValue.
"""
node = task.node
if not driver_utils.get_node_mac_addresses(task):
raise exception.InvalidParameterValue(_("Node %s does not have "
"any port associated with it.") % node.uuid)
d_info = _parse_deploy_info(node)
# Try to get the URL of the Ironic API
try:
# TODO(lucasagomes): Validate the format of the URL
CONF.conductor.api_url or keystone.get_service_url()
except (exception.CatalogFailure,
exception.CatalogNotFound,
exception.CatalogUnauthorized):
raise exception.InvalidParameterValue(_(
"Couldn't get the URL of the Ironic API service from the "
"configuration file or keystone catalog."))
_validate_glance_image(task.context, d_info)
@task_manager.require_exclusive_lock
def deploy(self, task):
"""Start deployment of the task's node'.
Config host file and xcat dhcp, generate image info for xcat
and issues a reboot request to the power driver.
This causes the node to boot into the deployment ramdisk and triggers
the next phase of PXE-based deployment via
VendorPassthru._continue_deploy().
:param task: a TaskManager instance containing the node to act on.
:returns: deploy state DEPLOYDONE.
"""
d_info = _parse_deploy_info(task.node)
if not task.node.instance_info.get('fixed_ip_address') or not task.node.instance_info.get('image_name'):
raise exception.InvalidParameterValue
self._config_host_file(d_info,task.node.instance_info.get('fixed_ip_address'))
self._make_dhcp()
self._nodeset_osimage(d_info,task.node.instance_info.get('image_name'))
manager_utils.node_set_boot_device(task, 'pxe', persistent=True)
manager_utils.node_power_action(task, states.REBOOT)
try:
self._wait_for_node_deploy(task)
except xcat_exception.xCATDeploymentFailure:
LOG.info(_("xcat deployment failed"))
return states.ERROR
return states.DEPLOYDONE
@task_manager.require_exclusive_lock
def tear_down(self, task):
"""Tear down a previous deployment on the task's node.
Power off the node. All actual clean-up is done in the clean_up()
method which should be called separately.
:param task: a TaskManager instance containing the node to act on.
:returns: deploy state DELETED.
"""
manager_utils.node_power_action(task, states.POWER_OFF)
return states.DELETED
def prepare(self, task):
"""Prepare the deployment environment for this task's node.
Get the image info from glance, config the mac for the xcat
use ssh and iptables to disable dhcp on network node
:param task: a TaskManager instance containing the node to act on.
"""
# TODO(deva): optimize this if rerun on existing files
d_info = _parse_deploy_info(task.node)
i_info = task.node.instance_info
image_id = d_info['image_source']
try:
glance_service = service.Service(version=1, context=task.context)
image_name = glance_service.show(image_id)['name']
i_info['image_name'] = image_name
except (exception.GlanceConnectionFailed,
exception.ImageNotAuthorized,
exception.Invalid):
LOG.warning(_("Failed to connect to Glance to get the properties "
"of the image %s") % image_id)
node_mac_addresses = driver_utils.get_node_mac_addresses(task)
vif_ports_info = xcat_neutron.get_ports_info_from_neutron(task)
try:
network_info = self._get_deploy_network_info(vif_ports_info, node_mac_addresses)
except (xcat_exception.GetNetworkFixedIPFailure,xcat_exception.GetNetworkIdFailure):
LOG.error(_("Failed to get network info"))
return
if not network_info:
LOG.error(_("Failed to get network info"))
return
fixed_ip_address = network_info['fixed_ip_address']
deploy_mac_address = network_info['mac_address']
network_id = network_info['network_id']
i_info['fixed_ip_address'] = fixed_ip_address
i_info['network_id'] = network_id
i_info['deploy_mac_address'] = deploy_mac_address
# use iptables to drop the dhcp mac of baremetal machine
self._ssh_append_dhcp_rule(CONF.xcat.network_node_ip,CONF.xcat.ssh_port,CONF.xcat.ssh_user,
CONF.xcat.ssh_password,network_id,deploy_mac_address)
self._chdef_node_mac_address(d_info,deploy_mac_address)
def clean_up(self, task):
"""Clean up the deployment environment for the task's node.
Unlinks TFTP and instance images and triggers image cache cleanup.
Removes the TFTP configuration files for this node. As a precaution,
this method also ensures the keystone auth token file was removed.
:param task: a TaskManager instance containing the node to act on.
"""
pass
def take_over(self, task):
pass
def _get_deploy_network_info(self, vif_ports_info, valid_node_mac_addrsses):
"""Get network info from mac address of ironic node.
:param vif_ports_info: info collection from neutron ports
:param valid_node_mac_addrsses: mac address from ironic node
:raises: GetNetworkFixedIpFailure if search the fixed ip from mac address failure
:raises: GetNetworkIdFailure if search the network id from mac address failure
"""
network_info = {}
for port_info in vif_ports_info.values():
if(port_info['port']['mac_address'] in valid_node_mac_addrsses ):
network_info['fixed_ip_address'] = port_info['port']['fixed_ips'][0]['ip_address']
if not network_info['fixed_ip_address']:
raise xcat_exception.GetNetworkFixedIPFailure(mac_address=port_info['port']['mac_address'])
network_info['mac_address'] = port_info['port']['mac_address']
network_info['network_id'] = port_info['port']['network_id']
if not network_info['network_id']:
raise xcat_exception.GetNetworkIdFailure(mac_address=port_info['port']['mac_address'])
network_info['port_id'] = port_info['port']['id']
return network_info
return network_info
def _chdef_node_mac_address(self, driver_info, deploy_mac):
""" run chdef command to set mac address"""
cmd = 'chdef'
args = 'mac='+ deploy_mac
try:
out_err = xcat_util.exec_xcatcmd(driver_info, cmd, args)
LOG.info(_("xcat chdef cmd exetute output: %(out_err)s") % {'out_err':out_err})
except xcat_exception.xCATCmdFailure as e:
LOG.warning(_("xcat chdef failed for node %(xcat_node)s with "
"error: %(error)s.")
% {'xcat_node': driver_info['xcat_node'], 'error': e})
raise exception.IPMIFailure(cmd=cmd)
@lockutils.synchronized(EM_SEMAPHORE, 'xcat-hosts-')
def _config_host_file(self, driver_info, deploy_ip):
""" append node and ip infomation to host file"""
with open(CONF.xcat.host_filepath,"r+") as f:
lines = []
for line in f:
temp = line.split('#')
if temp[0].strip():
host_name = xcat_util._tsplit(temp[0].strip(),(' ','\t'))[1]
if driver_info['xcat_node'] not in host_name:
lines.append(line)
# append a new line to host file
line = "%s\t%s\n" %(deploy_ip,driver_info['xcat_node'])
lines.append(line)
f.seek(0)
f.truncate()
for line in lines:
f.write(line)
def _nodeset_osimage(self, driver_info, image_name):
"""run nodeset command to config the image for the xcat node
:param driver_info: xcat node deploy info
:param image_name: image for the xcat deployment
"""
cmd = 'nodeset'
args = 'osimage='+ image_name
try:
xcat_util.exec_xcatcmd(driver_info, cmd, args)
except xcat_exception.xCATCmdFailure as e:
LOG.warning(_("xcat nodeset failed for node %(xcat_node)s with "
"error: %(error)s.")
% {'xcat_node': driver_info['xcat_node'], 'error': e})
def _make_dhcp(self):
"""run makedhcp command to setup dhcp environment for the xcat node"""
cmd = ['makedhcp',
'-n'
]
try:
out, err = utils.execute(*cmd)
LOG.info(_(" excute cmd: %(cmd)s \n output: %(out)s \n. Error: %(err)s \n"),
{'cmd':cmd,'out': out, 'err': err})
except Exception as e:
LOG.error(_("Unable to execute %(cmd)s. Exception: %(exception)s"),
{'cmd': cmd, 'exception': e})
# makedhcp -a
cmd = ['makedhcp',
'-a'
]
try:
out, err = utils.execute(*cmd)
LOG.info(_(" excute cmd: %(cmd)s \n output: %(out)s \n. Error: %(err)s \n"),
{'cmd':cmd,'out': out, 'err': err})
except Exception as e:
LOG.error(_("Unable to execute %(cmd)s. Exception: %(exception)s"),
{'cmd': cmd, 'exception': e})
def _ssh_append_dhcp_rule(self,ip,port,username,password,network_id,mac_address):
""" drop the dhcp package in network node to avoid of confilct of dhcp """
netns = 'qdhcp-%s' %network_id
append_cmd = 'sudo ip netns exec %s iptables -A INPUT -m mac --mac-source %s -j DROP' % \
(netns,mac_address)
cmd = [append_cmd]
xcat_util.xcat_ssh(ip,port,username,password,cmd)
def _ssh_delete_dhcp_rule(self,ip,port,username,password,network_id,mac_address):
""" delete the iptable rule on network node to recover the environment"""
netns = 'qdhcp-%s' %network_id
cancel_cmd = 'sudo ip netns exec %s iptables -D INPUT -m mac --mac-source %s -j DROP' % \
(netns,mac_address)
cmd = [cancel_cmd]
xcat_util.xcat_ssh(ip,port,username,password,cmd)
def _wait_for_node_deploy(self, task):
"""Wait for xCAT node deployment to complete."""
locals = {'errstr':''}
driver_info = _parse_deploy_info(task.node)
node_mac_addrsses = driver_utils.get_node_mac_addresses(task)
i_info = task.node.instance_info
def _wait_for_deploy():
out,err = xcat_util.exec_xcatcmd(driver_info,'nodels','nodelist.status')
if err:
locals['errstr'] = _("Error returned when quering node status"
" for node %s:%s") % (driver_info['xcat_node'], err)
LOG.warning(locals['errstr'])
raise loopingcall.LoopingCallDone()
if out:
node,status = out.split(": ")
status = status.strip()
if status == "booted":
LOG.info(_("Deployment for node %s completed.")
% driver_info['xcat_node'])
raise loopingcall.LoopingCallDone()
if (CONF.xcat.deploy_timeout and
timeutils.utcnow() > expiration):
locals['errstr'] = _("Timeout while waiting for"
" deployment of node %s.") % driver_info['xcat_node']
LOG.warning(locals['errstr'])
raise loopingcall.LoopingCallDone()
expiration = timeutils.utcnow() + datetime.timedelta(
seconds=CONF.xcat.deploy_timeout)
timer = loopingcall.FixedIntervalLoopingCall(_wait_for_deploy)
# default check every 10 seconds
timer.start(interval=CONF.xcat.deploy_checking_interval).wait()
if locals['errstr']:
raise xcat_exception.xCATDeploymentFailure(locals['errstr'])
# deploy end, delete the dhcp rule for xcat
self._ssh_delete_dhcp_rule(CONF.xcat.network_node_ip,CONF.xcat.ssh_port,CONF.xcat.ssh_user,
CONF.xcat.ssh_password,i_info['network_id'],node_mac_addrsses[0])

View File

@ -0,0 +1,479 @@
"""
IPMI power manager driver.
"""
import contextlib
import os
import stat
import tempfile
import time
from oslo.config import cfg
from ironic.common import exception
from ironic.common import states
from ironic.common import utils
from ironic.conductor import task_manager
from ironic.drivers import base
from ironic.drivers.modules import console_utils
from ironic.openstack.common import excutils
from ironic.openstack.common import log as logging
from ironic.openstack.common import loopingcall
from ironic.openstack.common import processutils
from ironic.drivers.modules import xcat_exception
from ironic.drivers.modules import xcat_util
CONF = cfg.CONF
CONF.import_opt('retry_timeout',
'ironic.drivers.modules.ipminative',
group='ipmi')
CONF.import_opt('min_command_interval',
'ironic.drivers.modules.ipminative',
group='ipmi')
LOG = logging.getLogger(__name__)
VALID_BOOT_DEVICES = ['net', 'hd', 'cd', 'floppy', 'def', 'stat']
VALID_PRIV_LEVELS = ['ADMINISTRATOR', 'CALLBACK', 'OPERATOR', 'USER']
TIMING_SUPPORT = None
def _is_timing_supported(is_supported=None):
# shim to allow module variable to be mocked in unit tests
global TIMING_SUPPORT
if (TIMING_SUPPORT is None) and (is_supported is not None):
TIMING_SUPPORT = is_supported
return TIMING_SUPPORT
def check_timing_support():
"""Check the installed version of ipmitool for -N -R option support.
Support was added in 1.8.12 for the -N -R options, which enable
more precise control over timing of ipmi packets. Prior to this,
the default behavior was to retry each command up to 18 times at
1 to 5 second intervals.
http://ipmitool.cvs.sourceforge.net/viewvc/ipmitool/ipmitool/ChangeLog?revision=1.37 # noqa
This method updates the module-level TIMING_SUPPORT variable so that
it is accessible by any driver interface class in this module. It is
intended to be called from the __init__ method of such classes only.
:returns: boolean indicating whether support for -N -R is present
:raises: OSError
"""
if _is_timing_supported() is None:
# Directly check ipmitool for support of -N and -R options. Because
# of the way ipmitool processes' command line options, if the local
# ipmitool does not support setting the timing options, the command
# below will fail.
try:
out, err = utils.execute(*['ipmitool', '-N', '0', '-R', '0', '-h'])
except processutils.ProcessExecutionError:
# the local ipmitool does not support the -N and -R options.
_is_timing_supported(False)
else:
# looks like ipmitool supports timing options.
_is_timing_supported(True)
def _console_pwfile_path(uuid):
"""Return the file path for storing the ipmi password for a console."""
file_name = "%(uuid)s.pw" % {'uuid': uuid}
return os.path.join(tempfile.gettempdir(), file_name)
def _parse_driver_info(node):
"""Gets the parameters required for ipmitool to access the node.
:param node: the Node of interest.
:returns: dictionary of parameters.
:raises: InvalidParameterValue if any required parameters are missing.
"""
info = node.driver_info or {}
address = info.get('ipmi_address')
username = info.get('ipmi_username')
password = info.get('ipmi_password')
port = info.get('ipmi_terminal_port')
priv_level = info.get('ipmi_priv_level', 'ADMINISTRATOR')
xcat_node = info.get('xcat_node')
xcatmaster = info.get('xcatmaster')
netboot = info.get('netboot')
if port:
try:
port = int(port)
except ValueError:
raise exception.InvalidParameterValue(_(
"IPMI terminal port is not an integer."))
if not address:
raise exception.InvalidParameterValue(_(
"IPMI address not supplied to xcat driver."))
if priv_level not in VALID_PRIV_LEVELS:
valid_priv_lvls = ', '.join(VALID_PRIV_LEVELS)
raise exception.InvalidParameterValue(_(
"Invalid privilege level value:%(priv_level)s, the valid value"
" can be one of %(valid_levels)s") %
{'priv_level': priv_level, 'valid_levels': valid_priv_lvls})
if not xcat_node:
raise exception.InvalidParameterValue(_(
"xcat node name not supplied to xcat driver"))
if not xcatmaster:
raise exception.InvalidParameterValue(_(
"xcatmaster not supplied to xcat driver"))
if not netboot:
raise exception.InvalidParameterValue(_(
"netboot not supplied to xcat driver"))
return {
'address': address,
'username': username,
'password': password,
'port': port,
'uuid': node.uuid,
'priv_level': priv_level,
'xcat_node': xcat_node,
'xcatmaster': xcatmaster,
'netboot': netboot
}
def chdef_node(driver_info):
"""Run the chdef command in xcat, config the node
:param driver_info: driver_info for the xcat node
"""
cmd = 'chdef'
args = 'mgt=ipmi' + \
' bmc=' + driver_info['address'] + \
' bmcusername=' + driver_info['username'] + \
' bmcpassword=' + driver_info['password'] + \
' xcatmaster=' + driver_info['xcatmaster']+ \
' netboot=' + driver_info['netboot']+ \
' primarynic=mac'+ \
' installnic=mac'+ \
' monserver=' + driver_info['xcatmaster'] + \
' nfsserver=' + driver_info['xcatmaster'] + \
' serialflow=hard'+ \
' serialspeed=115200' + \
' serialport=' + str(driver_info['port']);
try:
xcat_util.exec_xcatcmd(driver_info, cmd, args)
except xcat_exception.xCATCmdFailure as e:
LOG.warning(_("xcat chdef failed for node %(node_id)s with "
"error: %(error)s.")
% {'node_id': driver_info['uuid'], 'error': e})
def _sleep_time(iter):
"""Return the time-to-sleep for the n'th iteration of a retry loop.
This implementation increases exponentially.
:param iter: iteration number
:returns: number of seconds to sleep
"""
if iter <= 1:
return 1
return iter ** 2
def _set_and_wait(target_state, driver_info):
"""Helper function for DynamicLoopingCall.
This method changes the power state and polls the BMCuntil the desired
power state is reached, or CONF.ipmi.retry_timeout would be exceeded by the
next iteration.
This method assumes the caller knows the current power state and does not
check it prior to changing the power state. Most BMCs should be fine, but
if a driver is concerned, the state should be checked prior to calling this
method.
:param target_state: desired power state
:param driver_info: the ipmitool parameters for accessing a node.
:returns: one of ironic.common.states
:raises: IPMIFailure on an error from ipmitool (from _power_status call).
"""
if target_state == states.POWER_ON:
state_name = "on"
elif target_state == states.POWER_OFF:
state_name = "off"
def _wait(mutable):
try:
# Only issue power change command once
if mutable['iter'] < 0:
xcat_util.exec_xcatcmd(driver_info,'rpower',state_name)
else:
mutable['power'] = _power_status(driver_info)
except Exception:
# Log failures but keep trying
LOG.warning(_("xcat rpower %(state)s failed for node %(node)s."),
{'state': state_name, 'node': driver_info['uuid']})
finally:
mutable['iter'] += 1
if mutable['power'] == target_state:
raise loopingcall.LoopingCallDone()
sleep_time = _sleep_time(mutable['iter'])
if (sleep_time + mutable['total_time']) > CONF.ipmi.retry_timeout:
# Stop if the next loop would exceed maximum retry_timeout
LOG.error(_('xcat rpower %(state)s timed out after '
'%(tries)s retries on node %(node_id)s.'),
{'state': state_name, 'tries': mutable['iter'],
'node_id': driver_info['uuid']})
mutable['power'] = states.ERROR
raise loopingcall.LoopingCallDone()
else:
mutable['total_time'] += sleep_time
return sleep_time
# Use mutable objects so the looped method can change them.
# Start 'iter' from -1 so that the first two checks are one second apart.
status = {'power': None, 'iter': -1, 'total_time': 0}
timer = loopingcall.DynamicLoopingCall(_wait, status)
timer.start().wait()
return status['power']
def _power_on(driver_info):
"""Turn the power ON for this node.
:param driver_info: the xcat parameters for accessing a node.
:returns: one of ironic.common.states POWER_ON or ERROR.
:raises: IPMIFailure on an error from ipmitool (from _power_status call).
"""
return _set_and_wait(states.POWER_ON, driver_info)
def _power_off(driver_info):
"""Turn the power OFF for this node.
:param driver_info: the xcat parameters for accessing a node.
:returns: one of ironic.common.states POWER_OFF or ERROR.
:raises: IPMIFailure on an error from ipmitool (from _power_status call).
"""
return _set_and_wait(states.POWER_OFF, driver_info)
def _power_status(driver_info):
"""Get the power status for a node.
:param driver_info: the xcat access parameters for a node.
:returns: one of ironic.common.states POWER_OFF, POWER_ON or ERROR.
:raises: IPMIFailure on an error from ipmitool.
"""
cmd = "rpower"
try:
out_err = xcat_util.exec_xcatcmd(driver_info,cmd,'status')
except Exception as e:
LOG.warning(_("xcat rpower status failed for node %(node_id)s with "
"error: %(error)s.")
% {'node_id': driver_info['uuid'], 'error': e})
if out_err[0].split(' ')[1].strip() == "on":
return states.POWER_ON
elif out_err[0].split(' ')[1].strip() == "off":
return states.POWER_OFF
else:
return states.ERROR
class XcatPower(base.PowerInterface):
def __init__(self):
try:
check_timing_support()
except OSError:
raise exception.DriverLoadError(
driver=self.__class__.__name__,
reason="Unable to locate usable xcat command in "
"the system path when checking xcat version")
def validate(self, task):
"""Validate driver_info for xcat driver.
Check that node['driver_info'] contains IPMI credentials.
:param task: a TaskManager instance containing the node to act on.
:raises: InvalidParameterValue if required ipmi parameters are missing.
"""
driver_info = _parse_driver_info(task.node)
try:
chdef_node(driver_info)
except exception:
LOG.error(_("chdef xcat info error!"))
def get_power_state(self, task):
"""Get the current power state of the task's node.
:param task: a TaskManager instance containing the node to act on.
:returns: one of ironic.common.states POWER_OFF, POWER_ON or ERROR.
"""
driver_info = _parse_driver_info(task.node)
return _power_status(driver_info)
@task_manager.require_exclusive_lock
def set_power_state(self, task, pstate):
"""Turn the power on or off.
:param task: a TaskManager instance containing the node to act on.
:param pstate: The desired power state, one of ironic.common.states
POWER_ON, POWER_OFF.
:raises: InvalidParameterValue if required ipmi parameters are missing
or if an invalid power state was specified.
:raises: PowerStateFailure if the power couldn't be set to pstate.
"""
driver_info = _parse_driver_info(task.node)
if pstate == states.POWER_ON:
state = _power_on(driver_info)
elif pstate == states.POWER_OFF:
state = _power_off(driver_info)
else:
raise exception.InvalidParameterValue(_("set_power_state called "
"with invalid power state %s.") % pstate)
if state != pstate:
raise exception.PowerStateFailure(pstate=pstate)
@task_manager.require_exclusive_lock
def reboot(self, task):
"""Cycles the power to the task's node.
:param task: a TaskManager instance containing the node to act on.
:raises: InvalidParameterValue if required ipmi parameters are missing.
:raises: PowerStateFailure if the final state of the node is not
POWER_ON.
"""
driver_info = _parse_driver_info(task.node)
_power_off(driver_info)
state = _power_on(driver_info)
if state != states.POWER_ON:
raise exception.PowerStateFailure(pstate=states.POWER_ON)
class VendorPassthru(base.VendorInterface):
@task_manager.require_exclusive_lock
def _set_boot_device(self, task, device, persistent=False):
"""Set the boot device for a node.
:param task: a TaskManager instance.
:param device: Boot device. One of [net, hd, cd, floppy, def, stat].
:param persistent: Whether to set next-boot, or make the change
permanent. Default: False.
:raises: InvalidParameterValue if an invalid boot device is specified
or if required ipmi parameters are missing.
:raises: IPMIFailure on an error from ipmitool.
"""
if device not in VALID_BOOT_DEVICES:
raise exception.InvalidParameterValue(_(
"Invalid boot device %s specified.") % device)
cmd = "rsetboot"
if persistent:
cmd = cmd + " options=persistent"
driver_info = _parse_driver_info(task.node)
try:
xcat_util.exec_xcatcmd(driver_info, cmd, device)
# TODO(deva): validate (out, err) and add unit test for failure
except xcat_exception.xCATCmdFailure:
LOG.error(_("rsetboot %(node)s %(device)s"),{'node':driver_info['xcat_node]'],
'device':device})
def validate(self, task, **kwargs):
""" run chdef command to config xcat node infomation """
method = kwargs['method']
if method == 'set_boot_device':
device = kwargs.get('device')
if device not in VALID_BOOT_DEVICES:
raise exception.InvalidParameterValue(_(
"Invalid boot device %s specified.") % device)
else:
raise exception.InvalidParameterValue(_(
"Unsupported method (%s) passed to xcat driver.")
% method)
driver_info = _parse_driver_info(task.node)
chdef_node(driver_info)
def vendor_passthru(self, task, **kwargs):
method = kwargs['method']
if method == 'set_boot_device':
return self._set_boot_device(
task,
kwargs.get('device'),
kwargs.get('persistent', False))
class IPMIShellinaboxConsole(base.ConsoleInterface):
"""A ConsoleInterface that uses ipmitool and shellinabox."""
def __init__(self):
try:
check_timing_support()
except OSError:
raise exception.DriverLoadError(
driver=self.__class__.__name__,
reason="Unable to locate usable xcat command in "
"the system path when checking xcat version")
def validate(self, task):
"""Validate the Node console info.
:param task: a task from TaskManager.
:raises: InvalidParameterValue
"""
driver_info = _parse_driver_info(task.node)
if not driver_info['xcat_node']:
raise exception.InvalidParameterValue(_(
"xcat node name not supplied to xcat baremetal driver."))
if not driver_info['port']:
raise exception.InvalidParameterValue(_(
"IPMI terminal port not supplied to IPMI driver."))
def start_console(self, task):
"""Start a remote console for the node."""
driver_info = _parse_driver_info(task.node)
path = _console_pwfile_path(driver_info['uuid'])
pw_file = console_utils.make_persistent_password_file(
path, driver_info['password'])
ipmi_cmd = "/:%(uid)s:%(gid)s:HOME:ipmitool -H %(address)s" \
" -I lanplus -U %(user)s -f %(pwfile)s" \
% {'uid': os.getuid(),
'gid': os.getgid(),
'address': driver_info['address'],
'user': driver_info['username'],
'pwfile': pw_file}
if CONF.debug:
ipmi_cmd += " -v"
ipmi_cmd += " sol activate"
console_utils.start_shellinabox_console(driver_info['uuid'],
driver_info['port'],
ipmi_cmd)
def stop_console(self, task):
"""Stop the remote console session for the node."""
driver_info = _parse_driver_info(task.node)
console_utils.stop_shellinabox_console(driver_info['uuid'])
utils.unlink_without_raise(_console_pwfile_path(driver_info['uuid']))
def get_console(self, task):
"""Get the type and connection information about the console."""
driver_info = _parse_driver_info(task.node)
url = console_utils.get_shellinabox_console_url(driver_info['port'])
return {'type': 'shellinabox', 'url': url}

View File

@ -0,0 +1,110 @@
"""
util for xcat baremetal driver
exec_xcatcmd
xcat_ssh to excute remote cmd
"""
import paramiko
import time
import socket
from ironic.openstack.common import log as logging
from oslo.config import cfg
from ironic.drivers.modules import xcat_exception
from ironic.common import utils
xcat_opts = [
cfg.IntOpt('ssh_session_timeout',
default=10,
help='ssh session time'),
cfg.FloatOpt('ssh_shell_wait',
default=0.5,
help='wait time for the ssh cmd excute'),
cfg.IntOpt('ssh_buf_size',
default=65535,
help='Maximum size (in charactor) of cache for ssh, '
'including those in use'),
cfg.StrOpt('ssh_key',
default=None,
help='ssh private key to login '),
cfg.StrOpt('ssh_key_pass',
default=None,
help='Maximum size (in charactor) of cache for ssh, '
'including those in use'),
]
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
CONF.register_opts(xcat_opts, group='xcat')
LAST_CMD_TIME = {}
def xcat_ssh(ip,port,username,password,cmd):
""" exec remote command with ssh """
key =None
if CONF.xcat.ssh_key:
try:
key=paramiko.RSAKey.from_private_key_file(CONF.xcat.ssh_key)
except paramiko.PasswordRequiredException:
if not CONF.ssh_key_pass:
raise Exception.message("no pubkey password")
key = paramiko.RSAKey.from_private_key_file(
CONF.xcat.ssh_key, CONF.xcat.ssh_key.ssh_key_pass)
s = paramiko.SSHClient()
s.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
s.connect(ip,port,username=username,password=password,pkey=key,
timeout=CONF.xcat.ssh_session_timeout)
except socket.timeout as e:
LOG.error(_("Unable to connect to the ssh server Exception: %(exception)s"),
{'exception': e})
chan = s.invoke_shell()
output = chan.recv(CONF.xcat.ssh_buf_size)
while not output.rstrip().endswith('#') and not output.rstrip().endswith('$'):
output = chan.recv(CONF.xcat.ssh_buf_size)
for c in cmd :
_xcat_ssh_exec(chan,c,password)
def _xcat_ssh_exec(chan,cmd,password):
""" exec ssh command """
chan.send(cmd + '\n')
time.sleep(CONF.xcat.ssh_shell_wait)
ret = chan.recv(CONF.xcat.ssh_buf_size)
if 'password' in ret and ret.rstrip().endswith(':'):
chan.send(password + '\n')
output = chan.recv(CONF.xcat.ssh_buf_size)
while not output.rstrip().endswith('#') and not output.rstrip().endswith('$'):
output = chan.recv(CONF.xcat.ssh_buf_size)
return output
def _tsplit(string, delimiters):
""" Behaves str.split but supports multiple delimiters. """
delimiters = tuple(delimiters)
stack = [string,]
for delimiter in delimiters:
for i, substring in enumerate(stack):
substack = substring.split(delimiter)
stack.pop(i)
for j, _substring in enumerate(substack):
stack.insert(i+j, _substring)
return stack
def exec_xcatcmd(driver_info, command, args):
""" excute xcat cmd """
cmd = [command,
driver_info['xcat_node']
]
cmd.extend(args.split(" "))
# NOTE: ensure that no communications are excuted more
# often than once every min_command_interval seconds.
time_till_next_poll = CONF.ipmi.min_command_interval - (
time.time() - LAST_CMD_TIME.get(driver_info['xcat_node'], 0))
if time_till_next_poll > 0:
time.sleep(time_till_next_poll)
try:
out, err = utils.execute(*cmd)
if err:
raise xcat_exception.xCATCmdFailure(cmd=cmd,node=driver_info['xcat_node'],
args=args)
finally:
LAST_CMD_TIME[driver_info['xcat_node']] = time.time()
return out, err

View File

@ -0,0 +1,30 @@
"""
XCATBaremetalDriver
use xcat to deploy a baremetal machine
"""
from ironic.drivers import base
from ironic.drivers.modules import ipmitool
from ironic.drivers.modules import pxe
from ironic.drivers.modules import xcat_pxe
from ironic.drivers import utils
from ironic.drivers.modules import xcat_rpower
class XCATBaremetalDriver(base.BaseDriver):
"""xCAT driver
This driver implements the `core` functionality, combinding
:class:`ironic.drivers.xcat_rpower.XcatPower` for power on/off and reboot with
:class:`ironic.driver.xcat_pxe.PXEDeploy` for image deployment. Implementations are in
those respective classes; this class is merely the glue between them.
"""
def __init__(self):
self.power = xcat_rpower.XcatPower()
self.console = ipmitool.IPMIShellinaboxConsole()
self.deploy = xcat_pxe.PXEDeploy()
self.pxe_vendor = pxe.VendorPassthru()
self.ipmi_vendor = ipmitool.VendorPassthru()
self.mapping = {'pass_deploy_info': self.pxe_vendor,
'set_boot_device': self.ipmi_vendor}
self.vendor = utils.MixinVendorInterface(self.mapping)

View File

@ -0,0 +1,33 @@
[DEFAULT]
# The list of modules to copy from oslo-incubator
module=cliutils
module=config.generator
module=context
module=db
module=db.sqlalchemy
module=db.sqlalchemy.migration_cli
module=eventlet_backdoor
module=excutils
module=fileutils
module=gettextutils
module=importutils
module=jsonutils
module=local
module=lockutils
module=log
module=loopingcall
module=network_utils
module=periodic_task
module=policy
module=processutils
module=service
module=strutils
module=timeutils
module=versionutils
# Tools
# The base module to hold the copy of openstack.common
base=ironic

View File

@ -0,0 +1,34 @@
[metadata]
name = ironic
version = 2014.2
summary = OpenStack Bare Metal Provisioning
description-file =
README.rst
author = chenglch
author-email = chenglch@cn.ibm.com
home-page = http://xcat.sf.net/
classifier =
Environment :: OpenStack
Intended Audience :: Information Technology
Intended Audience :: System Administrators
License :: OSI Approved :: Apache Software License
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 2.6
[files]
packages =
ironic
[entry_points]
ironic.drivers =
pxe_xcat = ironic.drivers.xcat:XCATBaremetalDriver
[pbr]
autodoc_index_modules = True
[global]
setup-hooks =
pbr.hooks.setup_hook

View File

@ -0,0 +1,30 @@
#!/usr/bin/env python
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
# 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.
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
import setuptools
# In python < 2.7.4, a lazy loading of package `pbr` will break
# setuptools if some other modules registered functions in `atexit`.
# solution from: http://bugs.python.org/issue15881#msg170215
try:
import multiprocessing # noqa
except ImportError:
pass
setuptools.setup(
setup_requires=['pbr'],
pbr=True)

View File

@ -0,0 +1,22 @@
hacking>=0.8.0,<0.9
coverage>=3.6
discover
fixtures>=0.3.14
mock>=1.0
Babel>=1.3
MySQL-python
psycopg2
python-ironicclient
python-subunit>=0.0.18
testrepository>=0.0.18
testtools>=0.9.34
# Doc requirements
sphinx>=1.1.2,!=1.2.0,<1.3
sphinxcontrib-pecanwsme>=0.8
oslosphinx
# Required for Nova unit tests in ironic/nova/tests/ and can be removed
# once the driver code lands in Nova.
http://tarballs.openstack.org/nova/nova-master.tar.gz#egg=nova
mox>=0.5.3