
Nova has removed nova/openstack/common/lockutils.py and switched to oslo.concurrency so we can no longer import lockutils from the nova tree. Make the same switch in the ironic tree. Closes-Bug: #1386631 Change-Id: I8db99d61dbe6c50c9edae37077242e2696bc5671
371 lines
14 KiB
Python
371 lines
14 KiB
Python
# Copyright 2010 United States Government as represented by the
|
|
# Administrator of the National Aeronautics and Space Administration.
|
|
# All Rights Reserved.
|
|
# Copyright (c) 2010 Citrix Systems, Inc.
|
|
#
|
|
# 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.
|
|
|
|
"""
|
|
Handling of VM disk images.
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
|
|
import jinja2
|
|
from oslo.concurrency import processutils
|
|
from oslo.config import cfg
|
|
|
|
from ironic.common import exception
|
|
from ironic.common.i18n import _
|
|
from ironic.common.i18n import _LE
|
|
from ironic.common import image_service as service
|
|
from ironic.common import paths
|
|
from ironic.common import utils
|
|
from ironic.openstack.common import fileutils
|
|
from ironic.openstack.common import imageutils
|
|
from ironic.openstack.common import log as logging
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
image_opts = [
|
|
cfg.BoolOpt('force_raw_images',
|
|
default=True,
|
|
help='Force backing images to raw format.'),
|
|
cfg.StrOpt('isolinux_bin',
|
|
default='/usr/lib/syslinux/isolinux.bin',
|
|
help='Path to isolinux binary file.'),
|
|
cfg.StrOpt('isolinux_config_template',
|
|
default=paths.basedir_def('common/isolinux_config.template'),
|
|
help='Template file for isolinux configuration file.'),
|
|
]
|
|
|
|
CONF = cfg.CONF
|
|
CONF.register_opts(image_opts)
|
|
|
|
|
|
def _create_root_fs(root_directory, files_info):
|
|
"""Creates a filesystem root in given directory.
|
|
|
|
Given a mapping of absolute path of files to their relative paths
|
|
within the filesystem, this method copies the files to their
|
|
destination.
|
|
|
|
:param root_directory: the filesystem root directory.
|
|
:param files_info: A dict containing absolute path of file to be copied
|
|
-> relative path within the vfat image. For example,
|
|
{
|
|
'/absolute/path/to/file' -> 'relative/path/within/root'
|
|
...
|
|
}
|
|
:raises: OSError, if creation of any directory failed.
|
|
:raises: IOError, if copying any of the files failed.
|
|
"""
|
|
for src_file, path in files_info.items():
|
|
target_file = os.path.join(root_directory, path)
|
|
dirname = os.path.dirname(target_file)
|
|
if not os.path.exists(dirname):
|
|
os.makedirs(dirname)
|
|
|
|
shutil.copyfile(src_file, target_file)
|
|
|
|
|
|
def create_vfat_image(output_file, files_info=None, parameters=None,
|
|
parameters_file='parameters.txt', fs_size_kib=100):
|
|
"""Creates the fat fs image on the desired file.
|
|
|
|
This method copies the given files to a root directory (optional),
|
|
writes the parameters specified to the parameters file within the
|
|
root directory (optional), and then creates a vfat image of the root
|
|
directory.
|
|
|
|
:param output_file: The path to the file where the fat fs image needs
|
|
to be created.
|
|
:param files_info: A dict containing absolute path of file to be copied
|
|
-> relative path within the vfat image. For example,
|
|
{
|
|
'/absolute/path/to/file' -> 'relative/path/within/root'
|
|
...
|
|
}
|
|
:param parameters: A dict containing key-value pairs of parameters.
|
|
:param parameters_file: The filename for the parameters file.
|
|
:param fs_size_kib: size of the vfat filesystem in KiB.
|
|
:raises: ImageCreationFailed, if image creation failed while doing any
|
|
of filesystem manipulation activities like creating dirs, mounting,
|
|
creating filesystem, copying files, etc.
|
|
"""
|
|
try:
|
|
utils.dd('/dev/zero', output_file, 'count=1', "bs=%dKiB" % fs_size_kib)
|
|
except processutils.ProcessExecutionError as e:
|
|
raise exception.ImageCreationFailed(image_type='vfat', error=e)
|
|
|
|
with utils.tempdir() as tmpdir:
|
|
|
|
try:
|
|
utils.mkfs('vfat', output_file)
|
|
utils.mount(output_file, tmpdir, '-o', 'umask=0')
|
|
except processutils.ProcessExecutionError as e:
|
|
raise exception.ImageCreationFailed(image_type='vfat', error=e)
|
|
|
|
try:
|
|
if files_info:
|
|
_create_root_fs(tmpdir, files_info)
|
|
|
|
if parameters:
|
|
parameters_file = os.path.join(tmpdir, parameters_file)
|
|
params_list = ['%(key)s=%(val)s' % {'key': k, 'val': v}
|
|
for k, v in parameters.items()]
|
|
file_contents = '\n'.join(params_list)
|
|
utils.write_to_file(parameters_file, file_contents)
|
|
|
|
except Exception as e:
|
|
LOG.exception(_LE("vfat image creation failed. Error: %s"), e)
|
|
raise exception.ImageCreationFailed(image_type='vfat', error=e)
|
|
|
|
finally:
|
|
try:
|
|
utils.umount(tmpdir)
|
|
except processutils.ProcessExecutionError as e:
|
|
raise exception.ImageCreationFailed(image_type='vfat', error=e)
|
|
|
|
|
|
def _generate_isolinux_cfg(kernel_params):
|
|
"""Generates a isolinux configuration file.
|
|
|
|
Given a given a list of strings containing kernel parameters, this method
|
|
returns the kernel cmdline string.
|
|
:param kernel_params: a list of strings(each element being a string like
|
|
'K=V' or 'K' or combination of them like 'K1=V1 K2 K3=V3') to be added
|
|
as the kernel cmdline.
|
|
:returns: a string containing the contents of the isolinux configuration
|
|
file.
|
|
"""
|
|
if not kernel_params:
|
|
kernel_params = []
|
|
kernel_params_str = ' '.join(kernel_params)
|
|
|
|
template = CONF.isolinux_config_template
|
|
tmpl_path, tmpl_file = os.path.split(template)
|
|
env = jinja2.Environment(loader=jinja2.FileSystemLoader(tmpl_path))
|
|
template = env.get_template(tmpl_file)
|
|
|
|
options = {'kernel': '/vmlinuz', 'ramdisk': '/initrd',
|
|
'kernel_params': kernel_params_str}
|
|
|
|
cfg = template.render(options)
|
|
return cfg
|
|
|
|
|
|
def create_isolinux_image(output_file, kernel, ramdisk, kernel_params=None):
|
|
"""Creates an isolinux image on the specified file.
|
|
|
|
Copies the provided kernel, ramdisk to a directory, generates the isolinux
|
|
configuration file using the kernel parameters provided, and then generates
|
|
a bootable ISO image.
|
|
|
|
:param output_file: the path to the file where the iso image needs to be
|
|
created.
|
|
:param kernel: the kernel to use.
|
|
:param ramdisk: the ramdisk to use.
|
|
:param kernel_params: a list of strings(each element being a string like
|
|
'K=V' or 'K' or combination of them like 'K1=V1,K2,...') to be added
|
|
as the kernel cmdline.
|
|
:raises: ImageCreationFailed, if image creation failed while copying files
|
|
or while running command to generate iso.
|
|
"""
|
|
ISOLINUX_BIN = 'isolinux/isolinux.bin'
|
|
ISOLINUX_CFG = 'isolinux/isolinux.cfg'
|
|
|
|
with utils.tempdir() as tmpdir:
|
|
|
|
files_info = {
|
|
kernel: 'vmlinuz',
|
|
ramdisk: 'initrd',
|
|
CONF.isolinux_bin: ISOLINUX_BIN,
|
|
}
|
|
|
|
try:
|
|
_create_root_fs(tmpdir, files_info)
|
|
except (OSError, IOError) as e:
|
|
LOG.exception(_LE("Creating the filesystem root failed."))
|
|
raise exception.ImageCreationFailed(image_type='iso', error=e)
|
|
|
|
cfg = _generate_isolinux_cfg(kernel_params)
|
|
|
|
isolinux_cfg = os.path.join(tmpdir, ISOLINUX_CFG)
|
|
utils.write_to_file(isolinux_cfg, cfg)
|
|
|
|
try:
|
|
utils.execute('mkisofs', '-r', '-V', "BOOT IMAGE",
|
|
'-cache-inodes', '-J', '-l', '-no-emul-boot',
|
|
'-boot-load-size', '4', '-boot-info-table',
|
|
'-b', ISOLINUX_BIN, '-o', output_file, tmpdir)
|
|
except processutils.ProcessExecutionError as e:
|
|
LOG.exception(_LE("Creating ISO image failed."))
|
|
raise exception.ImageCreationFailed(image_type='iso', error=e)
|
|
|
|
|
|
def qemu_img_info(path):
|
|
"""Return an object containing the parsed output from qemu-img info."""
|
|
if not os.path.exists(path):
|
|
return imageutils.QemuImgInfo()
|
|
|
|
out, err = utils.execute('env', 'LC_ALL=C', 'LANG=C',
|
|
'qemu-img', 'info', path)
|
|
return imageutils.QemuImgInfo(out)
|
|
|
|
|
|
def convert_image(source, dest, out_format, run_as_root=False):
|
|
"""Convert image to other format."""
|
|
cmd = ('qemu-img', 'convert', '-O', out_format, source, dest)
|
|
utils.execute(*cmd, run_as_root=run_as_root)
|
|
|
|
|
|
def fetch(context, image_href, path, image_service=None):
|
|
# TODO(vish): Improve context handling and add owner and auth data
|
|
# when it is added to glance. Right now there is no
|
|
# auth checking in glance, so we assume that access was
|
|
# checked before we got here.
|
|
if not image_service:
|
|
image_service = service.Service(version=1, context=context)
|
|
|
|
with fileutils.remove_path_on_error(path):
|
|
with open(path, "wb") as image_file:
|
|
image_service.download(image_href, image_file)
|
|
|
|
|
|
def fetch_to_raw(context, image_href, path, image_service=None):
|
|
path_tmp = "%s.part" % path
|
|
fetch(context, image_href, path_tmp, image_service)
|
|
image_to_raw(image_href, path, path_tmp)
|
|
|
|
|
|
def image_to_raw(image_href, path, path_tmp):
|
|
with fileutils.remove_path_on_error(path_tmp):
|
|
data = qemu_img_info(path_tmp)
|
|
|
|
fmt = data.file_format
|
|
if fmt is None:
|
|
raise exception.ImageUnacceptable(
|
|
reason=_("'qemu-img info' parsing failed."),
|
|
image_id=image_href)
|
|
|
|
backing_file = data.backing_file
|
|
if backing_file is not None:
|
|
raise exception.ImageUnacceptable(image_id=image_href,
|
|
reason=_("fmt=%(fmt)s backed by: %(backing_file)s") %
|
|
{'fmt': fmt,
|
|
'backing_file': backing_file})
|
|
|
|
if fmt != "raw" and CONF.force_raw_images:
|
|
staged = "%s.converted" % path
|
|
LOG.debug("%(image)s was %(format)s, converting to raw" %
|
|
{'image': image_href, 'format': fmt})
|
|
with fileutils.remove_path_on_error(staged):
|
|
convert_image(path_tmp, staged, 'raw')
|
|
os.unlink(path_tmp)
|
|
|
|
data = qemu_img_info(staged)
|
|
if data.file_format != "raw":
|
|
raise exception.ImageConvertFailed(image_id=image_href,
|
|
reason=_("Converted to raw, but format is now %s") %
|
|
data.file_format)
|
|
|
|
os.rename(staged, path)
|
|
else:
|
|
os.rename(path_tmp, path)
|
|
|
|
|
|
def download_size(context, image_href, image_service=None):
|
|
if not image_service:
|
|
image_service = service.Service(version=1, context=context)
|
|
return image_service.show(image_href)['size']
|
|
|
|
|
|
def converted_size(path):
|
|
"""Get size of converted raw image.
|
|
|
|
The size of image converted to raw format can be growing up to the virtual
|
|
size of the image.
|
|
|
|
:param path: path to the image file.
|
|
:returns: virtual size of the image or 0 if conversion not needed.
|
|
|
|
"""
|
|
data = qemu_img_info(path)
|
|
if data.file_format == "raw" or not CONF.force_raw_images:
|
|
return 0
|
|
return data.virtual_size
|
|
|
|
|
|
def get_glance_image_property(context, image_uuid, property):
|
|
"""Returns the value of a glance image property.
|
|
|
|
:param context: context
|
|
:param image_uuid: the UUID of the image in glance
|
|
:param property: the property whose value is required.
|
|
:returns: the value of the property if it exists, otherwise None.
|
|
"""
|
|
glance_service = service.Service(version=1, context=context)
|
|
iproperties = glance_service.show(image_uuid)['properties']
|
|
return iproperties.get(property)
|
|
|
|
|
|
def get_temp_url_for_glance_image(context, image_uuid):
|
|
"""Returns the tmp url for a glance image.
|
|
|
|
:param context: context
|
|
:param image_uuid: the UUID of the image in glance
|
|
:returns: the tmp url for the glance image.
|
|
"""
|
|
# Glance API version 2 is required for getting direct_url of the image.
|
|
glance_service = service.Service(version=2, context=context)
|
|
image_properties = glance_service.show(image_uuid)
|
|
LOG.debug('Got image info: %(info)s for image %(image_uuid)s.',
|
|
{'info': image_properties, 'image_uuid': image_uuid})
|
|
return glance_service.swift_temp_url(image_properties)
|
|
|
|
|
|
def create_boot_iso(context, output_filename, kernel_uuid,
|
|
ramdisk_uuid, root_uuid=None, kernel_params=None):
|
|
"""Creates a bootable ISO image for a node.
|
|
|
|
Given the glance UUID of kernel, ramdisk, root partition's UUID and
|
|
kernel cmdline arguments, this method fetches the kernel, ramdisk from
|
|
glance, and builds a bootable ISO image that can be used to boot up the
|
|
baremetal node.
|
|
|
|
:param context: context
|
|
:param output_filename: the absolute path of the output ISO file
|
|
:param kernel_uuid: glance uuid of the kernel to use
|
|
:param ramdisk_uuid: glance uuid of the ramdisk to use
|
|
:param root_uuid: uuid of the root filesystem (optional)
|
|
:param kernel_params: a string containing whitespace separated values
|
|
kernel cmdline arguments of the form K=V or K (optional).
|
|
:raises: ImageCreationFailed, if creating boot ISO failed.
|
|
"""
|
|
with utils.tempdir() as tmpdir:
|
|
kernel_path = os.path.join(tmpdir, kernel_uuid)
|
|
ramdisk_path = os.path.join(tmpdir, ramdisk_uuid)
|
|
fetch_to_raw(context, kernel_uuid, kernel_path)
|
|
fetch_to_raw(context, ramdisk_uuid, ramdisk_path)
|
|
|
|
params = []
|
|
if root_uuid:
|
|
params.append('root=UUID=%s' % root_uuid)
|
|
if kernel_params:
|
|
params.append(kernel_params)
|
|
|
|
create_isolinux_image(output_filename, kernel_path,
|
|
ramdisk_path, params)
|