diff --git a/makerpm b/makerpm index e48de76d5..2266e78b5 100755 --- a/makerpm +++ b/makerpm @@ -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 diff --git a/xCAT-OpenStack-ironic/ironic_baremetal/MANIFEST.in b/xCAT-OpenStack-ironic/ironic_baremetal/MANIFEST.in new file mode 100644 index 000000000..c978a52da --- /dev/null +++ b/xCAT-OpenStack-ironic/ironic_baremetal/MANIFEST.in @@ -0,0 +1,6 @@ +include AUTHORS +include ChangeLog +exclude .gitignore +exclude .gitreview + +global-exclude *.pyc diff --git a/xCAT-OpenStack-ironic/ironic_baremetal/README.rst b/xCAT-OpenStack-ironic/ironic_baremetal/README.rst new file mode 100644 index 000000000..9fad8f287 --- /dev/null +++ b/xCAT-OpenStack-ironic/ironic_baremetal/README.rst @@ -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 + +$ nova boot --flavor baremetal --image testing --nic net-id= diff --git a/xCAT-OpenStack-ironic/ironic_baremetal/ironic/drivers/modules/xcat_exception.py b/xCAT-OpenStack-ironic/ironic_baremetal/ironic/drivers/modules/xcat_exception.py new file mode 100644 index 000000000..2e2f4a187 --- /dev/null +++ b/xCAT-OpenStack-ironic/ironic_baremetal/ironic/drivers/modules/xcat_exception.py @@ -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.") \ No newline at end of file diff --git a/xCAT-OpenStack-ironic/ironic_baremetal/ironic/drivers/modules/xcat_neutron.py b/xCAT-OpenStack-ironic/ironic_baremetal/ironic/drivers/modules/xcat_neutron.py new file mode 100644 index 000000000..389e563de --- /dev/null +++ b/xCAT-OpenStack-ironic/ironic_baremetal/ironic/drivers/modules/xcat_neutron.py @@ -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 + diff --git a/xCAT-OpenStack-ironic/ironic_baremetal/ironic/drivers/modules/xcat_pxe.py b/xCAT-OpenStack-ironic/ironic_baremetal/ironic/drivers/modules/xcat_pxe.py new file mode 100644 index 000000000..37a06f4c3 --- /dev/null +++ b/xCAT-OpenStack-ironic/ironic_baremetal/ironic/drivers/modules/xcat_pxe.py @@ -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]) + + diff --git a/xCAT-OpenStack-ironic/ironic_baremetal/ironic/drivers/modules/xcat_rpower.py b/xCAT-OpenStack-ironic/ironic_baremetal/ironic/drivers/modules/xcat_rpower.py new file mode 100644 index 000000000..f5f2a76fc --- /dev/null +++ b/xCAT-OpenStack-ironic/ironic_baremetal/ironic/drivers/modules/xcat_rpower.py @@ -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} diff --git a/xCAT-OpenStack-ironic/ironic_baremetal/ironic/drivers/modules/xcat_util.py b/xCAT-OpenStack-ironic/ironic_baremetal/ironic/drivers/modules/xcat_util.py new file mode 100644 index 000000000..738d6aef0 --- /dev/null +++ b/xCAT-OpenStack-ironic/ironic_baremetal/ironic/drivers/modules/xcat_util.py @@ -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 \ No newline at end of file diff --git a/xCAT-OpenStack-ironic/ironic_baremetal/ironic/drivers/xcat.py b/xCAT-OpenStack-ironic/ironic_baremetal/ironic/drivers/xcat.py new file mode 100644 index 000000000..0a12d9ebb --- /dev/null +++ b/xCAT-OpenStack-ironic/ironic_baremetal/ironic/drivers/xcat.py @@ -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) \ No newline at end of file diff --git a/xCAT-OpenStack-ironic/ironic_baremetal/openstack-common.conf b/xCAT-OpenStack-ironic/ironic_baremetal/openstack-common.conf new file mode 100644 index 000000000..dd10dded5 --- /dev/null +++ b/xCAT-OpenStack-ironic/ironic_baremetal/openstack-common.conf @@ -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 diff --git a/xCAT-OpenStack-ironic/ironic_baremetal/setup.cfg b/xCAT-OpenStack-ironic/ironic_baremetal/setup.cfg new file mode 100644 index 000000000..b6d40b84d --- /dev/null +++ b/xCAT-OpenStack-ironic/ironic_baremetal/setup.cfg @@ -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 diff --git a/xCAT-OpenStack-ironic/ironic_baremetal/setup.py b/xCAT-OpenStack-ironic/ironic_baremetal/setup.py new file mode 100644 index 000000000..736375744 --- /dev/null +++ b/xCAT-OpenStack-ironic/ironic_baremetal/setup.py @@ -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) diff --git a/xCAT-OpenStack-ironic/ironic_baremetal/test-requirements.txt b/xCAT-OpenStack-ironic/ironic_baremetal/test-requirements.txt new file mode 100644 index 000000000..56285cf97 --- /dev/null +++ b/xCAT-OpenStack-ironic/ironic_baremetal/test-requirements.txt @@ -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