summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMikyung Kang <mkkang@isi.edu>2012-11-10 08:07:13 +0900
committerRobert Collins <robertc@robertcollins.net>2013-01-08 08:55:02 +1300
commitabe1db6f88a6fb58586d4ed8d272cf7acb57b588 (patch)
treea801930f273cf0c97fc0b08a0580e9a5c46885ad
parente1c7b18c7f3c8d97ba7b2cccf27b968ad4710735 (diff)
downloadnova-abe1db6f88a6fb58586d4ed8d272cf7acb57b588.tar.gz
nova-abe1db6f88a6fb58586d4ed8d272cf7acb57b588.tar.xz
nova-abe1db6f88a6fb58586d4ed8d272cf7acb57b588.zip
PXE bare-metal provisioning helper server
a part of blueprint general-bare-metal-provisioning-framework. Implement nova-baremetal-deploy-helper. This service listens for HTTP requests from baremetal deploy ramdisk, formats the remote disk and writes an image to it, as part of baremetal PXE provisioning. blueprint improve-baremetal-pxe-deploy shows how we plan to improve this process. Change-Id: I0a1b020cc5f81d49559acd4dcc781397a58e2c01 Co-authored-by: Mikyung Kang <mkkang@isi.edu> Co-authored-by: David Kang <dkang@isi.edu> Co-authored-by: Ken Igarashi <igarashik@nttdocomo.co.jp> Co-authored-by: Arata Notsu <notsu@virtualtech.jp> Co-authored-by: Devananda van der Veen <devananda.vdv@gmail.com>
-rwxr-xr-xbin/nova-baremetal-deploy-helper318
-rw-r--r--doc/source/man/nova-baremetal-deploy-helper.rst52
-rw-r--r--etc/nova/rootwrap.d/baremetal-deploy-helper.filters10
-rw-r--r--setup.py1
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
diff --git a/setup.py b/setup.py
index e13ae4f64..b04ac2b4a 100644
--- a/setup.py
+++ b/setup.py
@@ -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',