4a7502a5c9
Resolve the relevant TODO. Migrate tests in the process. Change-Id: I372266b158667683cad973f20976619c34538491
524 lines
17 KiB
Python
524 lines
17 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.
|
|
# Copyright (c) 2011 Piston Cloud Computing, Inc
|
|
# Copyright (c) 2011 OpenStack Foundation
|
|
# (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
#
|
|
# 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 errno
|
|
import os
|
|
import re
|
|
|
|
from lxml import etree
|
|
from oslo_concurrency import processutils
|
|
from oslo_log import log as logging
|
|
|
|
import nova.conf
|
|
from nova.i18n import _
|
|
from nova.i18n import _LI
|
|
from nova.i18n import _LW
|
|
from nova.objects import fields as obj_fields
|
|
from nova import utils
|
|
from nova.virt import images
|
|
from nova.virt.libvirt import config as vconfig
|
|
from nova.virt.libvirt.volume import remotefs
|
|
from nova.virt import volumeutils
|
|
|
|
CONF = nova.conf.CONF
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
RESIZE_SNAPSHOT_NAME = 'nova-resize'
|
|
|
|
|
|
def execute(*args, **kwargs):
|
|
return utils.execute(*args, **kwargs)
|
|
|
|
|
|
def get_iscsi_initiator():
|
|
return volumeutils.get_iscsi_initiator()
|
|
|
|
|
|
def create_image(disk_format, path, size):
|
|
"""Create a disk image
|
|
|
|
:param disk_format: Disk image format (as known by qemu-img)
|
|
:param path: Desired location of the disk image
|
|
:param size: Desired size of disk image. May be given as an int or
|
|
a string. If given as an int, it will be interpreted
|
|
as bytes. If it's a string, it should consist of a number
|
|
with an optional suffix ('K' for Kibibytes,
|
|
M for Mebibytes, 'G' for Gibibytes, 'T' for Tebibytes).
|
|
If no suffix is given, it will be interpreted as bytes.
|
|
"""
|
|
execute('qemu-img', 'create', '-f', disk_format, path, size)
|
|
|
|
|
|
def create_cow_image(backing_file, path, size=None):
|
|
"""Create COW image
|
|
|
|
Creates a COW image with the given backing file
|
|
|
|
:param backing_file: Existing image on which to base the COW image
|
|
:param path: Desired location of the COW image
|
|
"""
|
|
base_cmd = ['qemu-img', 'create', '-f', 'qcow2']
|
|
cow_opts = []
|
|
if backing_file:
|
|
cow_opts += ['backing_file=%s' % backing_file]
|
|
base_details = images.qemu_img_info(backing_file)
|
|
else:
|
|
base_details = None
|
|
# Explicitly inherit the value of 'cluster_size' property of a qcow2
|
|
# overlay image from its backing file. This can be useful in cases
|
|
# when people create a base image with a non-default 'cluster_size'
|
|
# value or cases when images were created with very old QEMU
|
|
# versions which had a different default 'cluster_size'.
|
|
if base_details and base_details.cluster_size is not None:
|
|
cow_opts += ['cluster_size=%s' % base_details.cluster_size]
|
|
if size is not None:
|
|
cow_opts += ['size=%s' % size]
|
|
if cow_opts:
|
|
# Format as a comma separated list
|
|
csv_opts = ",".join(cow_opts)
|
|
cow_opts = ['-o', csv_opts]
|
|
cmd = base_cmd + cow_opts + [path]
|
|
execute(*cmd)
|
|
|
|
|
|
def pick_disk_driver_name(hypervisor_version, is_block_dev=False):
|
|
"""Pick the libvirt primary backend driver name
|
|
|
|
If the hypervisor supports multiple backend drivers we have to tell libvirt
|
|
which one should be used.
|
|
|
|
Xen supports the following drivers: "tap", "tap2", "phy", "file", or
|
|
"qemu", being "qemu" the preferred one. Qemu only supports "qemu".
|
|
|
|
:param is_block_dev:
|
|
:returns: driver_name or None
|
|
"""
|
|
if CONF.libvirt.virt_type == "xen":
|
|
if is_block_dev:
|
|
return "phy"
|
|
else:
|
|
# 4002000 == 4.2.0
|
|
if hypervisor_version >= 4002000:
|
|
try:
|
|
execute('xend', 'status',
|
|
run_as_root=True, check_exit_code=True)
|
|
except OSError as exc:
|
|
if exc.errno == errno.ENOENT:
|
|
LOG.debug("xend is not found")
|
|
# libvirt will try to use libxl toolstack
|
|
return 'qemu'
|
|
else:
|
|
raise
|
|
except processutils.ProcessExecutionError:
|
|
LOG.debug("xend is not started")
|
|
# libvirt will try to use libxl toolstack
|
|
return 'qemu'
|
|
# libvirt will use xend/xm toolstack
|
|
try:
|
|
out, err = execute('tap-ctl', 'check', check_exit_code=False)
|
|
if out == 'ok\n':
|
|
# 4000000 == 4.0.0
|
|
if hypervisor_version > 4000000:
|
|
return "tap2"
|
|
else:
|
|
return "tap"
|
|
else:
|
|
LOG.info(_LI("tap-ctl check: %s"), out)
|
|
except OSError as exc:
|
|
if exc.errno == errno.ENOENT:
|
|
LOG.debug("tap-ctl tool is not installed")
|
|
else:
|
|
raise
|
|
return "file"
|
|
elif CONF.libvirt.virt_type in ('kvm', 'qemu'):
|
|
return "qemu"
|
|
else:
|
|
# UML doesn't want a driver_name set
|
|
return None
|
|
|
|
|
|
def get_disk_size(path, format=None):
|
|
"""Get the (virtual) size of a disk image
|
|
|
|
:param path: Path to the disk image
|
|
:param format: the on-disk format of path
|
|
:returns: Size (in bytes) of the given disk image as it would be seen
|
|
by a virtual machine.
|
|
"""
|
|
size = images.qemu_img_info(path, format).virtual_size
|
|
return int(size)
|
|
|
|
|
|
def get_disk_backing_file(path, basename=True, format=None):
|
|
"""Get the backing file of a disk image
|
|
|
|
:param path: Path to the disk image
|
|
:returns: a path to the image's backing store
|
|
"""
|
|
backing_file = images.qemu_img_info(path, format).backing_file
|
|
if backing_file and basename:
|
|
backing_file = os.path.basename(backing_file)
|
|
|
|
return backing_file
|
|
|
|
|
|
def copy_image(src, dest, host=None, receive=False,
|
|
on_execute=None, on_completion=None,
|
|
compression=True):
|
|
"""Copy a disk image to an existing directory
|
|
|
|
:param src: Source image
|
|
:param dest: Destination path
|
|
:param host: Remote host
|
|
:param receive: Reverse the rsync direction
|
|
:param on_execute: Callback method to store pid of process in cache
|
|
:param on_completion: Callback method to remove pid of process from cache
|
|
:param compression: Allows to use rsync operation with or without
|
|
compression
|
|
"""
|
|
|
|
if not host:
|
|
# We shell out to cp because that will intelligently copy
|
|
# sparse files. I.E. holes will not be written to DEST,
|
|
# rather recreated efficiently. In addition, since
|
|
# coreutils 8.11, holes can be read efficiently too.
|
|
# we add '-r' argument because ploop disks are directories
|
|
execute('cp', '-r', src, dest)
|
|
else:
|
|
if receive:
|
|
src = "%s:%s" % (utils.safe_ip_format(host), src)
|
|
else:
|
|
dest = "%s:%s" % (utils.safe_ip_format(host), dest)
|
|
|
|
remote_filesystem_driver = remotefs.RemoteFilesystem()
|
|
remote_filesystem_driver.copy_file(src, dest,
|
|
on_execute=on_execute, on_completion=on_completion,
|
|
compression=compression)
|
|
|
|
|
|
def write_to_file(path, contents, umask=None):
|
|
"""Write the given contents to a file
|
|
|
|
:param path: Destination file
|
|
:param contents: Desired contents of the file
|
|
:param umask: Umask to set when creating this file (will be reset)
|
|
"""
|
|
if umask:
|
|
saved_umask = os.umask(umask)
|
|
|
|
try:
|
|
with open(path, 'w') as f:
|
|
f.write(contents)
|
|
finally:
|
|
if umask:
|
|
os.umask(saved_umask)
|
|
|
|
|
|
def chown(path, owner):
|
|
"""Change ownership of file or directory
|
|
|
|
:param path: File or directory whose ownership to change
|
|
:param owner: Desired new owner (given as uid or username)
|
|
"""
|
|
execute('chown', owner, path, run_as_root=True)
|
|
|
|
|
|
def update_mtime(path):
|
|
"""Touch a file without being the owner.
|
|
|
|
:param path: File bump the mtime on
|
|
"""
|
|
try:
|
|
execute('touch', '-c', path, run_as_root=True)
|
|
except processutils.ProcessExecutionError as exc:
|
|
# touch can intermittently fail when launching several instances with
|
|
# the same base image and using shared storage, so log the exception
|
|
# but don't fail. Ideally we'd know if we were on shared storage and
|
|
# would re-raise the error if we are not on shared storage.
|
|
LOG.warning(_LW("Failed to update mtime on path %(path)s. "
|
|
"Error: %(error)s"),
|
|
{'path': path, "error": exc})
|
|
|
|
|
|
def _id_map_to_config(id_map):
|
|
return "%s:%s:%s" % (id_map.start, id_map.target, id_map.count)
|
|
|
|
|
|
def chown_for_id_maps(path, id_maps):
|
|
"""Change ownership of file or directory for an id mapped
|
|
environment
|
|
|
|
:param path: File or directory whose ownership to change
|
|
:param id_maps: List of type LibvirtConfigGuestIDMap
|
|
"""
|
|
uid_maps_str = ','.join([_id_map_to_config(id_map) for id_map in id_maps if
|
|
isinstance(id_map,
|
|
vconfig.LibvirtConfigGuestUIDMap)])
|
|
gid_maps_str = ','.join([_id_map_to_config(id_map) for id_map in id_maps if
|
|
isinstance(id_map,
|
|
vconfig.LibvirtConfigGuestGIDMap)])
|
|
execute('nova-idmapshift', '-i', '-u', uid_maps_str,
|
|
'-g', gid_maps_str, path, run_as_root=True)
|
|
|
|
|
|
def extract_snapshot(disk_path, source_fmt, out_path, dest_fmt):
|
|
"""Extract a snapshot from a disk image.
|
|
Note that nobody should write to the disk image during this operation.
|
|
|
|
:param disk_path: Path to disk image
|
|
:param out_path: Desired path of extracted snapshot
|
|
"""
|
|
# NOTE(markmc): ISO is just raw to qemu-img
|
|
if dest_fmt == 'iso':
|
|
dest_fmt = 'raw'
|
|
if dest_fmt == 'ploop':
|
|
dest_fmt = 'parallels'
|
|
|
|
qemu_img_cmd = ('qemu-img', 'convert', '-f', source_fmt, '-O', dest_fmt)
|
|
|
|
# Conditionally enable compression of snapshots.
|
|
if CONF.libvirt.snapshot_compression and dest_fmt == "qcow2":
|
|
qemu_img_cmd += ('-c',)
|
|
|
|
qemu_img_cmd += (disk_path, out_path)
|
|
execute(*qemu_img_cmd)
|
|
|
|
|
|
def load_file(path):
|
|
"""Read contents of file
|
|
|
|
:param path: File to read
|
|
"""
|
|
with open(path, 'r') as fp:
|
|
return fp.read()
|
|
|
|
|
|
def file_open(*args, **kwargs):
|
|
"""Open file
|
|
|
|
see built-in open() documentation for more details
|
|
|
|
Note: The reason this is kept in a separate module is to easily
|
|
be able to provide a stub module that doesn't alter system
|
|
state at all (for unit tests)
|
|
"""
|
|
return open(*args, **kwargs)
|
|
|
|
|
|
def file_delete(path):
|
|
"""Delete (unlink) file
|
|
|
|
Note: The reason this is kept in a separate module is to easily
|
|
be able to provide a stub module that doesn't alter system
|
|
state at all (for unit tests)
|
|
"""
|
|
return os.unlink(path)
|
|
|
|
|
|
def path_exists(path):
|
|
"""Returns if path exists
|
|
|
|
Note: The reason this is kept in a separate module is to easily
|
|
be able to provide a stub module that doesn't alter system
|
|
state at all (for unit tests)
|
|
"""
|
|
return os.path.exists(path)
|
|
|
|
|
|
def find_disk(virt_dom):
|
|
"""Find root device path for instance
|
|
|
|
May be file or device
|
|
"""
|
|
xml_desc = virt_dom.XMLDesc(0)
|
|
domain = etree.fromstring(xml_desc)
|
|
os_type = domain.find('os/type').text
|
|
driver = None
|
|
if CONF.libvirt.virt_type == 'lxc':
|
|
filesystem = domain.find('devices/filesystem')
|
|
driver = filesystem.find('driver')
|
|
|
|
source = filesystem.find('source')
|
|
disk_path = source.get('dir')
|
|
disk_path = disk_path[0:disk_path.rfind('rootfs')]
|
|
disk_path = os.path.join(disk_path, 'disk')
|
|
elif (CONF.libvirt.virt_type == 'parallels' and
|
|
os_type == obj_fields.VMMode.EXE):
|
|
filesystem = domain.find('devices/filesystem')
|
|
driver = filesystem.find('driver')
|
|
|
|
source = filesystem.find('source')
|
|
disk_path = source.get('file')
|
|
else:
|
|
disk = domain.find('devices/disk')
|
|
driver = disk.find('driver')
|
|
|
|
source = disk.find('source')
|
|
disk_path = source.get('file') or source.get('dev')
|
|
if not disk_path and CONF.libvirt.images_type == 'rbd':
|
|
disk_path = source.get('name')
|
|
if disk_path:
|
|
disk_path = 'rbd:' + disk_path
|
|
|
|
if not disk_path:
|
|
raise RuntimeError(_("Can't retrieve root device path "
|
|
"from instance libvirt configuration"))
|
|
|
|
if driver is not None:
|
|
format = driver.get('type')
|
|
# This is a legacy quirk of libvirt/xen. Everything else should
|
|
# report the on-disk format in type.
|
|
if format == 'aio':
|
|
format = 'raw'
|
|
else:
|
|
format = None
|
|
return (disk_path, format)
|
|
|
|
|
|
def get_disk_type_from_path(path):
|
|
"""Retrieve disk type (raw, qcow2, lvm, ploop) for given file."""
|
|
if path.startswith('/dev'):
|
|
return 'lvm'
|
|
elif path.startswith('rbd:'):
|
|
return 'rbd'
|
|
elif (os.path.isdir(path) and
|
|
os.path.exists(os.path.join(path, "DiskDescriptor.xml"))):
|
|
return 'ploop'
|
|
|
|
# We can't reliably determine the type from this path
|
|
return None
|
|
|
|
|
|
def get_fs_info(path):
|
|
"""Get free/used/total space info for a filesystem
|
|
|
|
:param path: Any dirent on the filesystem
|
|
:returns: A dict containing:
|
|
|
|
:free: How much space is free (in bytes)
|
|
:used: How much space is used (in bytes)
|
|
:total: How big the filesystem is (in bytes)
|
|
"""
|
|
hddinfo = os.statvfs(path)
|
|
total = hddinfo.f_frsize * hddinfo.f_blocks
|
|
free = hddinfo.f_frsize * hddinfo.f_bavail
|
|
used = hddinfo.f_frsize * (hddinfo.f_blocks - hddinfo.f_bfree)
|
|
return {'total': total,
|
|
'free': free,
|
|
'used': used}
|
|
|
|
|
|
def fetch_image(context, target, image_id):
|
|
"""Grab image."""
|
|
images.fetch_to_raw(context, image_id, target)
|
|
|
|
|
|
def fetch_raw_image(context, target, image_id):
|
|
"""Grab initrd or kernel image.
|
|
|
|
This function does not attempt raw conversion, as these images will
|
|
already be in raw format.
|
|
"""
|
|
images.fetch(context, image_id, target)
|
|
|
|
|
|
def get_instance_path(instance, relative=False):
|
|
"""Determine the correct path for instance storage.
|
|
|
|
This method determines the directory name for instance storage.
|
|
|
|
:param instance: the instance we want a path for
|
|
:param relative: if True, just the relative path is returned
|
|
|
|
:returns: a path to store information about that instance
|
|
"""
|
|
if relative:
|
|
return instance.uuid
|
|
return os.path.join(CONF.instances_path, instance.uuid)
|
|
|
|
|
|
def get_instance_path_at_destination(instance, migrate_data=None):
|
|
"""Get the instance path on destination node while live migration.
|
|
|
|
This method determines the directory name for instance storage on
|
|
destination node, while live migration.
|
|
|
|
:param instance: the instance we want a path for
|
|
:param migrate_data: if not None, it is a dict which holds data
|
|
required for live migration without shared
|
|
storage.
|
|
|
|
:returns: a path to store information about that instance
|
|
"""
|
|
instance_relative_path = None
|
|
if migrate_data:
|
|
instance_relative_path = migrate_data.instance_relative_path
|
|
# NOTE(mikal): this doesn't use libvirt_utils.get_instance_path
|
|
# because we are ensuring that the same instance directory name
|
|
# is used as was at the source
|
|
if instance_relative_path:
|
|
instance_dir = os.path.join(CONF.instances_path,
|
|
instance_relative_path)
|
|
else:
|
|
instance_dir = get_instance_path(instance)
|
|
return instance_dir
|
|
|
|
|
|
def get_arch(image_meta):
|
|
"""Determine the architecture of the guest (or host).
|
|
|
|
This method determines the CPU architecture that must be supported by
|
|
the hypervisor. It gets the (guest) arch info from image_meta properties,
|
|
and it will fallback to the nova-compute (host) arch if no architecture
|
|
info is provided in image_meta.
|
|
|
|
:param image_meta: the metadata associated with the instance image
|
|
|
|
:returns: guest (or host) architecture
|
|
"""
|
|
if image_meta:
|
|
image_arch = image_meta.properties.get('hw_architecture')
|
|
if image_arch is not None:
|
|
return image_arch
|
|
|
|
return obj_fields.Architecture.from_host()
|
|
|
|
|
|
def is_mounted(mount_path, source=None):
|
|
"""Check if the given source is mounted at given destination point."""
|
|
try:
|
|
check_cmd = ['findmnt', '--target', mount_path]
|
|
if source:
|
|
check_cmd.extend(['--source', source])
|
|
|
|
utils.execute(*check_cmd)
|
|
return True
|
|
except processutils.ProcessExecutionError:
|
|
return False
|
|
except OSError as exc:
|
|
# info since it's not required to have this tool.
|
|
if exc.errno == errno.ENOENT:
|
|
LOG.info(_LI("findmnt tool is not installed"))
|
|
return False
|
|
|
|
|
|
def is_valid_hostname(hostname):
|
|
return re.match(r"^[\w\-\.:]+$", hostname)
|