257 lines
9.0 KiB
Python
Executable File
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)
|