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>
This commit is contained in:
parent
e1c7b18c7f
commit
abe1db6f88
318
bin/nova-baremetal-deploy-helper
Executable file
318
bin/nova-baremetal-deploy-helper
Executable file
@ -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()
|
52
doc/source/man/nova-baremetal-deploy-helper.rst
Normal file
52
doc/source/man/nova-baremetal-deploy-helper.rst
Normal file
@ -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>`__
|
10
etc/nova/rootwrap.d/baremetal-deploy-helper.filters
Normal file
10
etc/nova/rootwrap.d/baremetal-deploy-helper.filters
Normal file
@ -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
|
1
setup.py
1
setup.py
@ -49,6 +49,7 @@ setuptools.setup(name='nova',
|
|||||||
'bin/nova-api-ec2',
|
'bin/nova-api-ec2',
|
||||||
'bin/nova-api-metadata',
|
'bin/nova-api-metadata',
|
||||||
'bin/nova-api-os-compute',
|
'bin/nova-api-os-compute',
|
||||||
|
'bin/nova-baremetal-deploy-helper',
|
||||||
'bin/nova-rpc-zmq-receiver',
|
'bin/nova-rpc-zmq-receiver',
|
||||||
'bin/nova-cells',
|
'bin/nova-cells',
|
||||||
'bin/nova-cert',
|
'bin/nova-cert',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user