ironic/ironic/common/images.py

295 lines
11 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.config import cfg
from ironic.common import exception
from ironic.common import i18n
from ironic.common.i18n import _
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
from ironic.openstack.common import processutils
LOG = logging.getLogger(__name__)
_LE = i18n._LE
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']