322 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			322 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
#!/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
 | 
						|
 | 
						|
# Do not monkey_patch in unittest
 | 
						|
if __name__ == '__main__':
 | 
						|
    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()
 |