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:
		 Mikyung Kang
					Mikyung Kang
				
			
				
					committed by
					
						 Robert Collins
						Robert Collins
					
				
			
			
				
	
			
			
			 Robert Collins
						Robert Collins
					
				
			
						parent
						
							ac2c2091d4
						
					
				
				
					commit
					e8f18ddf50
				
			
							
								
								
									
										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>`__ | ||||
		Reference in New Issue
	
	Block a user