257 lines
9.0 KiB
Python
Executable File

#!/usr/bin/python
#
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# 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.
import argparse
import json
import logging
import os
import subprocess
import sys
import yaml
METADATA_NAMESPACE = 'OpenStack::ImageBuilder'
IMAGE_CONSUMING_TYPES = ['AWS::EC2::Instance',
'AWS::AutoScaling::LaunchConfiguration']
devnull = None
INCUBATOR_SCRIPTS = os.path.dirname(sys.argv[0])
class RunError(RuntimeError):
pass
class Run(object):
noop = False
@classmethod
def cmd(cls, args):
if cls.noop:
print ' '.join(args)
else:
try:
subprocess.check_call(args)
except subprocess.CalledProcessError as e:
raise RunError(e)
def read_stack(stack_path):
""" Read HEAT stack """
with open(stack_path) as stack_file:
try:
stack = json.load(stack_file)
except ValueError:
stack_file.seek(0)
try:
stack = yaml.safe_load(stack_file)
except yaml.parser.ParserError, e:
logging.error(e)
return False
# Sanity checks
if type(stack) != dict:
logging.error(
'Expecting map, instead got python type %s' % type(stack))
return False
if 'Resources' not in stack:
logging.error('Stack has no resources')
if 'Description' not in stack:
logging.warn('Stack has no description')
logging.info('Loaded %s (%s)' % (stack_path, stack.get('Description')))
return stack
def find_element_metadata(stack):
""" Find metadata with OpenStack::ImageBuilder::Elements """
launch_configs = {}
for rname, r in iter(stack.get('Resources', {}).items()):
if r.get('Type') == 'AWS::AutoScaling::LaunchConfiguration':
launch_configs[rname] = r
string_params = set()
for pname, p in iter(stack.get('Parameters', {}).items()):
if p.get('Type', 'String') == 'String':
string_params.add(pname)
found = []
for resource_key, resource in stack['Resources'].iteritems():
if resource.get('Type') not in IMAGE_CONSUMING_TYPES:
continue
resource_elements = set()
resource_packages = set()
logging.debug('Inspecting %s' % resource_key)
metadata = resource.get('Metadata', {})
for m_key, m_item in metadata.iteritems():
logging.debug('Inspecting %s' % m_key)
try:
prefix, under = m_key.rsplit('::', 1)
except ValueError:
logging.debug('Ignoring %s->%s' % (resource_key, m_key))
continue
if prefix != METADATA_NAMESPACE:
logging.debug('Ignoring %s->%s [%s]'
% (resource_key, m_key, prefix))
continue
logging.debug('Found %s' % m_key)
if prefix == METADATA_NAMESPACE and under == 'Packages':
if isinstance(m_item, list):
resource_packages.update(m_item)
else:
resource_packages.add(m_item)
else:
resource_elements.update(m_item)
# If ImageId is a single Ref{} to a parameter, we can pass it to heat
image_param = None
lc_name = resource.get('Properties', {}).get('LaunchConfigurationName')
if lc_name and lc_name in launch_configs:
lc = launch_configs[lc_name]
image_id = lc.get('Properties', {}).get('ImageId')
else:
image_id = resource.get('Properties', {}).get('ImageId')
if isinstance(image_id, dict) and image_id.keys() == ['Ref']:
if image_id['Ref'] in string_params:
image_param = image_id['Ref']
found.append((resource_elements, image_param, resource_packages))
return found
def build_images(prefix, found, extra, use_existing):
""" build images for stack elements """
images = []
for f, image_param, packages in found:
key = '-'.join(sorted(f))
if key == '' and len(packages):
key = '-'.join(sorted(packages))
image_name = '%s%s' % (prefix, key)
images.append((image_name, image_param))
image_file_name = '%s.qcow2' % image_name
if os.path.exists(image_file_name) and use_existing:
logging.info('Using existing image file %s' % image_file_name)
continue
command = ['disk-image-create', '-o', '%s' % image_name]
if len(packages):
command.extend(['-p', ','.join(packages)])
command.extend(f)
command.extend(extra)
Run.cmd(command)
return images
def image_exists(image):
global devnull
args = ['glance', 'image-show', image]
if devnull is None:
devnull = open('/dev/null', 'a')
try:
subprocess.check_call(args, stdout=devnull, stderr=devnull)
except subprocess.CalledProcessError:
return False
return True
def upload_images(images, noop, use_existing, kernel, initrd):
""" upload images to glance """
for img, _ in images:
if use_existing and image_exists(img):
continue
img_filename = '%s.qcow2' % img
script_path = os.path.join(INCUBATOR_SCRIPTS, 'load-image')
args = [script_path, '-d', img_filename]
Run.cmd(args)
def run_heat(images, options):
params = []
for i, param in images:
if param:
params.append('%s=%s' % (param, i))
if len(params) != len(images):
print >>sys.stderr, 'Cannot determine parameters to run heat.'
if len(params):
print >>sys.stderr, ('Known parameters are as follows: %s'
% (params))
else:
args = ['heat', 'create', options.stack_name,
'--template-file', options.stack_path,
'--parameters', ';'.join(params)]
Run.cmd(args)
def main():
""" This program is made to integrate with diskimage-builder and
OpenStack HEAT to deploy workloads defined as elements in
diskimage-builder.
For each resource, specify which elements and/or packages to
build with MetaData keys. Use OpenStack::ImageBuilder::Elements
or OpenStack::ImageBuilder::Packages respectively."""
parser = argparse.ArgumentParser(description=main.__doc__)
parser.add_argument('stack_path',
help='Heat template to deploy')
parser.add_argument('stack_name', nargs='?',
help='Stack name for heat')
parser.add_argument('--log-level', default='WARN',
help='Basic root logging level')
parser.add_argument('--extra', '-e', nargs='*', default=[],
help='Extra elements to add to all images')
parser.add_argument('--prefix', '-p', default='image-',
help='Prefix for image names')
parser.add_argument('--noop', '-n', default=False, action='store_true',
help='Just print actions instead of executing them.')
parser.add_argument('--use-existing', '-u', default=False, action='store_true',
help='Use any existing image files or images in glance')
parser.add_argument('--skip-heat', default=False, action='store_true',
help='Do not try to run heat')
parser.add_argument('--kernel', nargs='?',
help='Attach this kernel to the image in glance')
parser.add_argument('--initrd', nargs='?',
help='Attach this initrd ot the image in glance')
options = parser.parse_args()
if options.stack_name is None:
options.stack_name = os.path.basename(options.stack_path).rsplit('.', 1)[0]
logging.basicConfig(level=options.log_level)
Run.noop = options.noop
stack = read_stack(options.stack_path)
if not stack:
return 1
found = find_element_metadata(stack)
if found:
images = build_images(options.prefix,
found, options.extra, options.use_existing)
if images:
upload_images(images,
options.noop,
options.use_existing,
options.kernel,
options.initrd)
if not options.skip_heat:
run_heat(images, options)
return 0
if __name__ == '__main__':
try:
sys.exit(main())
except RunError as e:
print >>sys.stderr, "Fatal error: %s" % e
sys.exit(1)