diff options
-rwxr-xr-x | bin/nova-baremetal-deploy-helper | 318 | ||||
-rw-r--r-- | doc/source/man/nova-baremetal-deploy-helper.rst | 52 | ||||
-rw-r--r-- | etc/nova/rootwrap.d/baremetal-deploy-helper.filters | 10 | ||||
-rw-r--r-- | setup.py | 1 |
4 files changed, 381 insertions, 0 deletions
diff --git a/bin/nova-baremetal-deploy-helper b/bin/nova-baremetal-deploy-helper new file mode 100755 index 000000000..fa0a30d9e --- /dev/null +++ b/bin/nova-baremetal-deploy-helper @@ -0,0 +1,318 @@ +#!/usr/bin/env python + +# Copyright (c) 2012 NTT DOCOMO, INC. +# All Rights Reserved. +# +# 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. + +"""Starter script for Bare-Metal Deployment Service.""" + +import eventlet +eventlet.monkey_patch() + +import os +import sys +import threading +import time + +# If ../nova/__init__.py exists, add ../ to Python search path, so that +# it will override what happens to be installed in /usr/(local/)lib/python... +possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), + os.pardir, + os.pardir)) +if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): + sys.path.insert(0, possible_topdir) + +import cgi +import Queue +import re +import socket +import stat +from wsgiref import simple_server + +from nova import config +from nova import context as nova_context +from nova.openstack.common import log as logging +from nova import utils +from nova.virt.baremetal import db + + +LOG = logging.getLogger('nova.virt.baremetal.deploy_helper') + +QUEUE = Queue.Queue() + + +# All functions are called from deploy() directly or indirectly. +# They are split for stub-out. + +def discovery(portal_address, portal_port): + """Do iSCSI discovery on portal""" + utils.execute('iscsiadm', + '-m', 'discovery', + '-t', 'st', + '-p', '%s:%s' % (portal_address, portal_port), + run_as_root=True, + check_exit_code=[0]) + + +def login_iscsi(portal_address, portal_port, target_iqn): + """Login to an iSCSI target""" + utils.execute('iscsiadm', + '-m', 'node', + '-p', '%s:%s' % (portal_address, portal_port), + '-T', target_iqn, + '--login', + run_as_root=True, + check_exit_code=[0]) + # Ensure the login complete + time.sleep(3) + + +def logout_iscsi(portal_address, portal_port, target_iqn): + """Logout from an iSCSI target""" + utils.execute('iscsiadm', + '-m', 'node', + '-p', '%s:%s' % (portal_address, portal_port), + '-T', target_iqn, + '--logout', + run_as_root=True, + check_exit_code=[0]) + + +def make_partitions(dev, root_mb, swap_mb): + """Create partitions for root and swap on a disk device""" + commands = ['o,w', + 'n,p,1,,+%dM,t,1,83,w' % root_mb, + 'n,p,2,,+%dM,t,2,82,w' % swap_mb, + ] + for command in commands: + command = command.replace(',', '\n') + utils.execute('fdisk', dev, + process_input=command, + run_as_root=True, + check_exit_code=[0]) + # avoid "device is busy" + time.sleep(3) + + +def is_block_device(dev): + """Check whether a device is block or not""" + s = os.stat(dev) + return stat.S_ISBLK(s.st_mode) + + +def dd(src, dst): + """Execute dd from src to dst""" + utils.execute('dd', + 'if=%s' % src, + 'of=%s' % dst, + 'bs=1M', + run_as_root=True, + check_exit_code=[0]) + + +def mkswap(dev, label='swap1'): + """Execute mkswap on a device""" + utils.execute('mkswap', + '-L', label, + dev, + run_as_root=True, + check_exit_code=[0]) + + +def block_uuid(dev): + """Get UUID of a block device""" + out, _ = utils.execute('blkid', '-s', 'UUID', '-o', 'value', dev, + run_as_root=True, + check_exit_code=[0]) + return out.strip() + + +def switch_pxe_config(path, root_uuid): + """Switch a pxe config from deployment mode to service mode.""" + with open(path) as f: + lines = f.readlines() + root = 'UUID=%s' % root_uuid + rre = re.compile(r'\$\{ROOT\}') + dre = re.compile('^default .*$') + with open(path, 'w') as f: + for line in lines: + line = rre.sub(root, line) + line = dre.sub('default boot', line) + f.write(line) + + +def notify(address, port): + """Notify a node that it becomes ready to reboot.""" + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + s.connect((address, port)) + s.send('done') + finally: + s.close() + + +def get_dev(address, port, iqn, lun): + """Returns a device path for given parameters.""" + dev = "/dev/disk/by-path/ip-%s:%s-iscsi-%s-lun-%s" \ + % (address, port, iqn, lun) + return dev + + +def get_image_mb(image_path): + """Get size of an image in Megabyte.""" + mb = 1024 * 1024 + image_byte = os.path.getsize(image_path) + # round up size to MB + image_mb = int((image_byte + mb - 1) / mb) + return image_mb + + +def work_on_disk(dev, root_mb, swap_mb, image_path): + """Creates partitions and write an image to the root partition.""" + root_part = "%s-part1" % dev + swap_part = "%s-part2" % dev + + if not is_block_device(dev): + LOG.warn("parent device '%s' not found", dev) + return + make_partitions(dev, root_mb, swap_mb) + if not is_block_device(root_part): + LOG.warn("root device '%s' not found", root_part) + return + if not is_block_device(swap_part): + LOG.warn("swap device '%s' not found", swap_part) + return + dd(image_path, root_part) + mkswap(swap_part) + root_uuid = block_uuid(root_part) + return root_uuid + + +def deploy(address, port, iqn, lun, image_path, pxe_config_path, + root_mb, swap_mb): + """All-in-one function to deploy a node.""" + dev = get_dev(address, port, iqn, lun) + image_mb = get_image_mb(image_path) + if image_mb > root_mb: + root_mb = image_mb + discovery(address, port) + login_iscsi(address, port, iqn) + try: + root_uuid = work_on_disk(dev, root_mb, swap_mb, image_path) + finally: + logout_iscsi(address, port, iqn) + switch_pxe_config(pxe_config_path, root_uuid) + # Ensure the node started netcat on the port after POST the request. + time.sleep(3) + notify(address, 10000) + + +class Worker(threading.Thread): + """Thread that handles requests in queue""" + + def __init__(self): + super(Worker, self).__init__() + self.setDaemon(True) + self.stop = False + self.queue_timeout = 1 + + def run(self): + while not self.stop: + try: + # Set timeout to check self.stop periodically + (deployment_id, params) = QUEUE.get(block=True, + timeout=self.queue_timeout) + except Queue.Empty: + pass + else: + # Requests comes here from BareMetalDeploy.post() + LOG.info("start deployment: %s, %s", deployment_id, params) + try: + deploy(**params) + except Exception: + LOG.exception('deployment %s failed' % deployment_id) + else: + LOG.info("deployment %s done", deployment_id) + finally: + context = nova_context.get_admin_context() + db.bm_deployment_destroy(context, deployment_id) + + +class BareMetalDeploy(object): + """WSGI server for bare-metal deployment""" + + def __init__(self): + self.worker = Worker() + self.worker.start() + + def __call__(self, environ, start_response): + method = environ['REQUEST_METHOD'] + if method == 'POST': + return self.post(environ, start_response) + else: + start_response('501 Not Implemented', + [('Content-type', 'text/plain')]) + return 'Not Implemented' + + def post(self, environ, start_response): + LOG.info("post: environ=%s", environ) + inpt = environ['wsgi.input'] + length = int(environ.get('CONTENT_LENGTH', 0)) + + x = inpt.read(length) + q = dict(cgi.parse_qsl(x)) + try: + deployment_id = q['i'] + deployment_key = q['k'] + address = q['a'] + port = q.get('p', '3260') + iqn = q['n'] + lun = q.get('l', '1') + except KeyError as e: + start_response('400 Bad Request', [('Content-type', 'text/plain')]) + return "parameter '%s' is not defined" % e + + context = nova_context.get_admin_context() + d = db.bm_deployment_get(context, deployment_id) + + if d['key'] != deployment_key: + start_response('400 Bad Request', [('Content-type', 'text/plain')]) + return 'key is not match' + + params = {'address': address, + 'port': port, + 'iqn': iqn, + 'lun': lun, + 'image_path': d['image_path'], + 'pxe_config_path': d['pxe_config_path'], + 'root_mb': int(d['root_mb']), + 'swap_mb': int(d['swap_mb']), + } + # Restart worker, if needed + if not self.worker.isAlive(): + self.worker = Worker() + self.worker.start() + LOG.info("request is queued: %s, %s", deployment_id, params) + QUEUE.put((deployment_id, params)) + # Requests go to Worker.run() + start_response('200 OK', [('Content-type', 'text/plain')]) + return '' + + +if __name__ == '__main__': + config.parse_args(sys.argv) + logging.setup("nova") + app = BareMetalDeploy() + srv = simple_server.make_server('', 10000, app) + srv.serve_forever() diff --git a/doc/source/man/nova-baremetal-deploy-helper.rst b/doc/source/man/nova-baremetal-deploy-helper.rst new file mode 100644 index 000000000..106cb85e7 --- /dev/null +++ b/doc/source/man/nova-baremetal-deploy-helper.rst @@ -0,0 +1,52 @@ +============================ +nova-baremetal-deploy-helper +============================ + +------------------------------------------------------------------ +Writes images to a bare-metal node and switch it to instance-mode +------------------------------------------------------------------ + +:Author: openstack@lists.launchpad.net +:Date: 2012-10-17 +:Copyright: OpenStack LLC +:Version: 2013.1 +:Manual section: 1 +:Manual group: cloud computing + +SYNOPSIS +======== + + nova-baremetal-deploy-helper + +DESCRIPTION +=========== + +This is a service which should run on nova-compute host when using the +baremetal driver. During a baremetal node's first boot, +nova-baremetal-deploy-helper works in conjunction with diskimage-builder's +"deploy" ramdisk to write an image from glance onto the baremetal node's disks +using iSCSI. After that is complete, nova-baremetal-deploy-helper switches the +PXE config to reference the kernel and ramdisk which correspond to the running +image. + +OPTIONS +======= + + **General options** + +FILES +======== + +* /etc/nova/nova.conf +* /etc/nova/rootwrap.conf +* /etc/nova/rootwrap.d/ + +SEE ALSO +======== + +* `OpenStack Nova <http://nova.openstack.org>`__ + +BUGS +==== + +* Nova is sourced in Launchpad so you can view current bugs at `OpenStack Nova <http://nova.openstack.org>`__ diff --git a/etc/nova/rootwrap.d/baremetal-deploy-helper.filters b/etc/nova/rootwrap.d/baremetal-deploy-helper.filters new file mode 100644 index 000000000..65416bbf8 --- /dev/null +++ b/etc/nova/rootwrap.d/baremetal-deploy-helper.filters @@ -0,0 +1,10 @@ +# nova-rootwrap command filters for nova-baremetal-deploy-helper +# This file should be owned by (and only-writeable by) the root user + +[Filters] +# nova-baremetal-deploy-helper +iscsiadm: CommandFilter, /sbin/iscsiadm, root +fdisk: CommandFilter, /sbin/fdisk, root +dd: CommandFilter, /bin/dd, root +mkswap: CommandFilter, /sbin/mkswap, root +blkid: CommandFilter, /sbin/blkid, root @@ -49,6 +49,7 @@ setuptools.setup(name='nova', 'bin/nova-api-ec2', 'bin/nova-api-metadata', 'bin/nova-api-os-compute', + 'bin/nova-baremetal-deploy-helper', 'bin/nova-rpc-zmq-receiver', 'bin/nova-cells', 'bin/nova-cert', |