Merge "Bug#898257 abstract out disk image access methods"
This commit is contained in:
1
Authors
1
Authors
@@ -113,6 +113,7 @@ Naveed Massjouni <naveedm9@gmail.com>
|
|||||||
Nikolay Sokolov <nsokolov@griddynamics.com>
|
Nikolay Sokolov <nsokolov@griddynamics.com>
|
||||||
Nirmal Ranganathan <nirmal.ranganathan@rackspace.com>
|
Nirmal Ranganathan <nirmal.ranganathan@rackspace.com>
|
||||||
Ollie Leahy <oliver.leahy@hp.com>
|
Ollie Leahy <oliver.leahy@hp.com>
|
||||||
|
Pádraig Brady <pbrady@redhat.com>
|
||||||
Paul Voccio <paul@openstack.org>
|
Paul Voccio <paul@openstack.org>
|
||||||
Renuka Apte <renuka.apte@citrix.com>
|
Renuka Apte <renuka.apte@citrix.com>
|
||||||
Ricardo Carrillo Cruz <emaildericky@gmail.com>
|
Ricardo Carrillo Cruz <emaildericky@gmail.com>
|
||||||
|
@@ -19,53 +19,50 @@
|
|||||||
from nova.rootwrap.filters import CommandFilter, DnsmasqFilter
|
from nova.rootwrap.filters import CommandFilter, DnsmasqFilter
|
||||||
|
|
||||||
filters = [
|
filters = [
|
||||||
# nova/virt/disk.py: 'kpartx', '-a', device
|
# nova/virt/disk/mount.py: 'kpartx', '-a', device
|
||||||
# nova/virt/disk.py: 'kpartx', '-d', device
|
# nova/virt/disk/mount.py: 'kpartx', '-d', device
|
||||||
CommandFilter("/sbin/kpartx", "root"),
|
CommandFilter("/sbin/kpartx", "root"),
|
||||||
|
|
||||||
# nova/virt/disk.py: 'tune2fs', '-c', 0, '-i', 0, mapped_device
|
# nova/virt/disk/mount.py: 'tune2fs', '-c', 0, '-i', 0, mapped_device
|
||||||
# nova/virt/xenapi/vm_utils.py: "tune2fs", "-O ^has_journal", part_path
|
# nova/virt/xenapi/vm_utils.py: "tune2fs", "-O ^has_journal", part_path
|
||||||
# nova/virt/xenapi/vm_utils.py: "tune2fs", "-j", partition_path
|
# nova/virt/xenapi/vm_utils.py: "tune2fs", "-j", partition_path
|
||||||
CommandFilter("/sbin/tune2fs", "root"),
|
CommandFilter("/sbin/tune2fs", "root"),
|
||||||
|
|
||||||
# nova/virt/disk.py: 'mount', mapped_device, tmpdir
|
# nova/virt/disk/mount.py: 'mount', mapped_device, mount_dir
|
||||||
# nova/virt/disk.py: 'mount', device, container_dir
|
|
||||||
# nova/virt/disk.py: 'mount'
|
|
||||||
# nova/virt/xenapi/vm_utils.py: 'mount', '-t', 'ext2,ext3,ext4,reiserfs'..
|
# nova/virt/xenapi/vm_utils.py: 'mount', '-t', 'ext2,ext3,ext4,reiserfs'..
|
||||||
CommandFilter("/bin/mount", "root"),
|
CommandFilter("/bin/mount", "root"),
|
||||||
|
|
||||||
# nova/virt/disk.py: 'umount', mapped_device
|
# nova/virt/disk/mount.py: 'umount', mapped_device
|
||||||
# nova/virt/disk.py: 'umount', container_dir
|
|
||||||
# nova/virt/xenapi/vm_utils.py: 'umount', dev_path
|
# nova/virt/xenapi/vm_utils.py: 'umount', dev_path
|
||||||
CommandFilter("/bin/umount", "root"),
|
CommandFilter("/bin/umount", "root"),
|
||||||
|
|
||||||
# nova/virt/disk.py: 'qemu-nbd', '-c', device, image
|
# nova/virt/disk/nbd.py: 'qemu-nbd', '-c', device, image
|
||||||
# nova/virt/disk.py: 'qemu-nbd', '-d', device
|
# nova/virt/disk/nbd.py: 'qemu-nbd', '-d', device
|
||||||
CommandFilter("/usr/bin/qemu-nbd", "root"),
|
CommandFilter("/usr/bin/qemu-nbd", "root"),
|
||||||
|
|
||||||
# nova/virt/disk.py: 'losetup', '--find', '--show', image
|
# nova/virt/disk/loop.py: 'losetup', '--find', '--show', image
|
||||||
# nova/virt/disk.py: 'losetup', '--detach', device
|
# nova/virt/disk/loop.py: 'losetup', '--detach', device
|
||||||
CommandFilter("/sbin/losetup", "root"),
|
CommandFilter("/sbin/losetup", "root"),
|
||||||
|
|
||||||
# nova/virt/disk.py: 'tee', metadata_path
|
# nova/virt/disk/api.py: 'tee', metadata_path
|
||||||
# nova/virt/disk.py: 'tee', '-a', keyfile
|
# nova/virt/disk/api.py: 'tee', '-a', keyfile
|
||||||
# nova/virt/disk.py: 'tee', netfile
|
# nova/virt/disk/api.py: 'tee', netfile
|
||||||
CommandFilter("/usr/bin/tee", "root"),
|
CommandFilter("/usr/bin/tee", "root"),
|
||||||
|
|
||||||
# nova/virt/disk.py: 'mkdir', '-p', sshdir
|
# nova/virt/disk/api.py: 'mkdir', '-p', sshdir
|
||||||
# nova/virt/disk.py: 'mkdir', '-p', netdir
|
# nova/virt/disk/api.py: 'mkdir', '-p', netdir
|
||||||
CommandFilter("/bin/mkdir", "root"),
|
CommandFilter("/bin/mkdir", "root"),
|
||||||
|
|
||||||
# nova/virt/disk.py: 'chown', 'root', sshdir
|
# nova/virt/disk/api.py: 'chown', 'root', sshdir
|
||||||
# nova/virt/disk.py: 'chown', 'root:root', netdir
|
# nova/virt/disk/api.py: 'chown', 'root:root', netdir
|
||||||
# nova/virt/libvirt/connection.py: 'chown', os.getuid(), console_log
|
# nova/virt/libvirt/connection.py: 'chown', os.getuid(), console_log
|
||||||
# nova/virt/libvirt/connection.py: 'chown', os.getuid(), console_log
|
# nova/virt/libvirt/connection.py: 'chown', os.getuid(), console_log
|
||||||
# nova/virt/libvirt/connection.py: 'chown', 'root', basepath('disk')
|
# nova/virt/libvirt/connection.py: 'chown', 'root', basepath('disk')
|
||||||
# nova/virt/xenapi/vm_utils.py: 'chown', os.getuid(), dev_path
|
# nova/virt/xenapi/vm_utils.py: 'chown', os.getuid(), dev_path
|
||||||
CommandFilter("/bin/chown", "root"),
|
CommandFilter("/bin/chown", "root"),
|
||||||
|
|
||||||
# nova/virt/disk.py: 'chmod', '700', sshdir
|
# nova/virt/disk/api.py: 'chmod', '700', sshdir
|
||||||
# nova/virt/disk.py: 'chmod', 755, netdir
|
# nova/virt/disk/api.py: 'chmod', 755, netdir
|
||||||
CommandFilter("/bin/chmod", "root"),
|
CommandFilter("/bin/chmod", "root"),
|
||||||
|
|
||||||
# nova/virt/libvirt/vif.py: 'ip', 'tuntap', 'add', dev, 'mode', 'tap'
|
# nova/virt/libvirt/vif.py: 'ip', 'tuntap', 'add', dev, 'mode', 'tap'
|
||||||
|
@@ -36,7 +36,7 @@ from nova import utils
|
|||||||
from nova.api.ec2 import cloud
|
from nova.api.ec2 import cloud
|
||||||
from nova.compute import power_state
|
from nova.compute import power_state
|
||||||
from nova.compute import vm_states
|
from nova.compute import vm_states
|
||||||
from nova.virt import disk
|
from nova.virt.disk import api as disk
|
||||||
from nova.virt import images
|
from nova.virt import images
|
||||||
from nova.virt import driver
|
from nova.virt import driver
|
||||||
from nova.virt.libvirt import connection
|
from nova.virt.libvirt import connection
|
||||||
|
@@ -238,6 +238,36 @@ def execute(*cmd, **kwargs):
|
|||||||
greenthread.sleep(0)
|
greenthread.sleep(0)
|
||||||
|
|
||||||
|
|
||||||
|
def trycmd(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
A wrapper around execute() to more easily handle warnings and errors.
|
||||||
|
|
||||||
|
Returns an (out, err) tuple of strings containing the output of
|
||||||
|
the command's stdout and stderr. If 'err' is not empty then the
|
||||||
|
command can be considered to have failed.
|
||||||
|
|
||||||
|
:discard_warnings True | False. Defaults to False. If set to True,
|
||||||
|
then for succeeding commands, stderr is cleared
|
||||||
|
|
||||||
|
"""
|
||||||
|
discard_warnings = kwargs.pop('discard_warnings', False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
out, err = execute(*args, **kwargs)
|
||||||
|
failed = False
|
||||||
|
except exception.ProcessExecutionError, exn:
|
||||||
|
out, err = '', str(exn)
|
||||||
|
LOG.debug(err)
|
||||||
|
failed = True
|
||||||
|
|
||||||
|
if not failed and discard_warnings and err:
|
||||||
|
# Handle commands that output to stderr but otherwise succeed
|
||||||
|
LOG.debug(err)
|
||||||
|
err = ''
|
||||||
|
|
||||||
|
return out, err
|
||||||
|
|
||||||
|
|
||||||
def ssh_execute(ssh, cmd, process_input=None,
|
def ssh_execute(ssh, cmd, process_input=None,
|
||||||
addl_env=None, check_exit_code=True):
|
addl_env=None, check_exit_code=True):
|
||||||
LOG.debug(_('Running cmd (SSH): %s'), ' '.join(cmd))
|
LOG.debug(_('Running cmd (SSH): %s'), ' '.join(cmd))
|
||||||
|
21
nova/virt/disk/__init__.py
Normal file
21
nova/virt/disk/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 Red Hat, 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.
|
||||||
|
"""
|
||||||
|
Operations on disk images including:
|
||||||
|
|
||||||
|
resize, file system creation, data injection.
|
||||||
|
|
||||||
|
"""
|
@@ -28,13 +28,13 @@ Includes injection of SSH PGP keys into authorized_keys file.
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import time
|
|
||||||
|
|
||||||
from nova import exception
|
from nova import exception
|
||||||
from nova import flags
|
from nova import flags
|
||||||
from nova import log as logging
|
from nova import log as logging
|
||||||
from nova import utils
|
from nova import utils
|
||||||
|
from nova.virt.disk import loop
|
||||||
|
from nova.virt.disk import nbd
|
||||||
|
|
||||||
LOG = logging.getLogger('nova.compute.disk')
|
LOG = logging.getLogger('nova.compute.disk')
|
||||||
FLAGS = flags.FLAGS
|
FLAGS = flags.FLAGS
|
||||||
@@ -43,10 +43,9 @@ flags.DEFINE_integer('minimum_root_size', 1024 * 1024 * 1024 * 10,
|
|||||||
flags.DEFINE_string('injected_network_template',
|
flags.DEFINE_string('injected_network_template',
|
||||||
utils.abspath('virt/interfaces.template'),
|
utils.abspath('virt/interfaces.template'),
|
||||||
'Template file for injected network')
|
'Template file for injected network')
|
||||||
flags.DEFINE_integer('timeout_nbd', 10,
|
flags.DEFINE_list('img_handlers', ['loop', 'nbd'],
|
||||||
'time to wait for a NBD device coming up')
|
'Order of methods used to mount disk images')
|
||||||
flags.DEFINE_integer('max_nbd_devices', 16,
|
|
||||||
'maximum number of possible nbd devices')
|
|
||||||
|
|
||||||
# NOTE(yamahata): DEFINE_list() doesn't work because the command may
|
# NOTE(yamahata): DEFINE_list() doesn't work because the command may
|
||||||
# include ','. For example,
|
# include ','. For example,
|
||||||
@@ -101,8 +100,89 @@ def extend(image, size):
|
|||||||
utils.execute('resize2fs', image, check_exit_code=False)
|
utils.execute('resize2fs', image, check_exit_code=False)
|
||||||
|
|
||||||
|
|
||||||
|
class _DiskImage(object):
|
||||||
|
"""Provide operations on a disk image file."""
|
||||||
|
|
||||||
|
def __init__(self, image, partition=None, use_cow=False,
|
||||||
|
disable_auto_fsck=False, mount_dir=None):
|
||||||
|
# These passed to each mounter
|
||||||
|
self.image = image
|
||||||
|
self.partition = partition
|
||||||
|
self.disable_auto_fsck = disable_auto_fsck
|
||||||
|
self.mount_dir = mount_dir
|
||||||
|
|
||||||
|
# Internal
|
||||||
|
self._mkdir = False
|
||||||
|
self._mounter = None
|
||||||
|
self._errors = []
|
||||||
|
|
||||||
|
# As a performance tweak, don't bother trying to
|
||||||
|
# directly loopback mount a cow image.
|
||||||
|
self.handlers = FLAGS.img_handlers[:]
|
||||||
|
if use_cow:
|
||||||
|
self.handlers.remove('loop')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def errors(self):
|
||||||
|
"""Return the collated errors from all operations."""
|
||||||
|
return '\n--\n'.join([''] + self._errors)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _handler_class(mode):
|
||||||
|
"""Look up the appropriate class to use based on MODE."""
|
||||||
|
for cls in (loop.Mount, nbd.Mount):
|
||||||
|
if cls.mode == mode:
|
||||||
|
return cls
|
||||||
|
raise exception.Error(_("unknown disk image handler: %s" % mode))
|
||||||
|
|
||||||
|
def mount(self):
|
||||||
|
"""Mount a disk image, using the object attributes.
|
||||||
|
|
||||||
|
The first supported means provided by the mount classes is used.
|
||||||
|
|
||||||
|
True, or False is returned and the 'errors' attribute
|
||||||
|
contains any diagnostics.
|
||||||
|
"""
|
||||||
|
if self._mounter:
|
||||||
|
raise exception.Error(_('image already mounted'))
|
||||||
|
|
||||||
|
if not self.mount_dir:
|
||||||
|
self.mount_dir = tempfile.mkdtemp()
|
||||||
|
self._mkdir = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
for h in self.handlers:
|
||||||
|
mounter_cls = self._handler_class(h)
|
||||||
|
mounter = mounter_cls(image=self.image,
|
||||||
|
partition=self.partition,
|
||||||
|
disable_auto_fsck=self.disable_auto_fsck,
|
||||||
|
mount_dir=self.mount_dir)
|
||||||
|
if mounter.do_mount():
|
||||||
|
self._mounter = mounter
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
LOG.debug(mounter.error)
|
||||||
|
self._errors.append(mounter.error)
|
||||||
|
finally:
|
||||||
|
if not self._mounter:
|
||||||
|
self.umount() # rmdir
|
||||||
|
|
||||||
|
return bool(self._mounter)
|
||||||
|
|
||||||
|
def umount(self):
|
||||||
|
"""Unmount a disk image from the file system."""
|
||||||
|
try:
|
||||||
|
if self._mounter:
|
||||||
|
self._mounter.do_umount()
|
||||||
|
finally:
|
||||||
|
if self._mkdir:
|
||||||
|
os.rmdir(self.mount_dir)
|
||||||
|
|
||||||
|
|
||||||
|
# Public module functions
|
||||||
|
|
||||||
def inject_data(image, key=None, net=None, metadata=None,
|
def inject_data(image, key=None, net=None, metadata=None,
|
||||||
partition=None, nbd=False, tune2fs=True):
|
partition=None, use_cow=False, disable_auto_fsck=True):
|
||||||
"""Injects a ssh key and optionally net data into a disk image.
|
"""Injects a ssh key and optionally net data into a disk image.
|
||||||
|
|
||||||
it will mount the image as a fully partitioned disk and attempt to inject
|
it will mount the image as a fully partitioned disk and attempt to inject
|
||||||
@@ -111,57 +191,19 @@ def inject_data(image, key=None, net=None, metadata=None,
|
|||||||
If partition is not specified it mounts the image as a single partition.
|
If partition is not specified it mounts the image as a single partition.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
device = _link_device(image, nbd)
|
img = _DiskImage(image=image, partition=partition, use_cow=use_cow,
|
||||||
|
disable_auto_fsck=disable_auto_fsck)
|
||||||
|
if img.mount():
|
||||||
try:
|
try:
|
||||||
if not partition is None:
|
inject_data_into_fs(img.mount_dir, key, net, metadata,
|
||||||
# create partition
|
|
||||||
out, err = utils.execute('kpartx', '-a', device, run_as_root=True)
|
|
||||||
if err:
|
|
||||||
raise exception.Error(_('Failed to load partition: %s') % err)
|
|
||||||
mapped_device = '/dev/mapper/%sp%s' % (device.split('/')[-1],
|
|
||||||
partition)
|
|
||||||
else:
|
|
||||||
mapped_device = device
|
|
||||||
|
|
||||||
try:
|
|
||||||
# We can only loopback mount raw images. If the device isn't there,
|
|
||||||
# it's normally because it's a .vmdk or a .vdi etc
|
|
||||||
if not os.path.exists(mapped_device):
|
|
||||||
raise exception.Error('Mapped device was not found (we can'
|
|
||||||
' only inject raw disk images): %s' %
|
|
||||||
mapped_device)
|
|
||||||
|
|
||||||
if tune2fs:
|
|
||||||
# Configure ext2fs so that it doesn't auto-check every N boots
|
|
||||||
out, err = utils.execute('tune2fs', '-c', 0, '-i', 0,
|
|
||||||
mapped_device, run_as_root=True)
|
|
||||||
tmpdir = tempfile.mkdtemp()
|
|
||||||
try:
|
|
||||||
# mount loopback to dir
|
|
||||||
out, err = utils.execute('mount', mapped_device, tmpdir,
|
|
||||||
run_as_root=True)
|
|
||||||
if err:
|
|
||||||
raise exception.Error(_('Failed to mount filesystem: %s')
|
|
||||||
% err)
|
|
||||||
|
|
||||||
try:
|
|
||||||
inject_data_into_fs(tmpdir, key, net, metadata,
|
|
||||||
utils.execute)
|
utils.execute)
|
||||||
finally:
|
finally:
|
||||||
# unmount device
|
img.umount()
|
||||||
utils.execute('umount', mapped_device, run_as_root=True)
|
else:
|
||||||
finally:
|
raise exception.Error(img.errors)
|
||||||
# remove temporary directory
|
|
||||||
utils.execute('rmdir', tmpdir)
|
|
||||||
finally:
|
|
||||||
if not partition is None:
|
|
||||||
# remove partitions
|
|
||||||
utils.execute('kpartx', '-d', device, run_as_root=True)
|
|
||||||
finally:
|
|
||||||
_unlink_device(device, nbd)
|
|
||||||
|
|
||||||
|
|
||||||
def setup_container(image, container_dir=None, nbd=False):
|
def setup_container(image, container_dir=None, use_cow=False):
|
||||||
"""Setup the LXC container.
|
"""Setup the LXC container.
|
||||||
|
|
||||||
It will mount the loopback image to the container directory in order
|
It will mount the loopback image to the container directory in order
|
||||||
@@ -170,86 +212,30 @@ def setup_container(image, container_dir=None, nbd=False):
|
|||||||
LXC does not support qcow2 images yet.
|
LXC does not support qcow2 images yet.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
device = _link_device(image, nbd)
|
img = _DiskImage(image=image, use_cow=use_cow, mount_dir=container_dir)
|
||||||
utils.execute('mount', device, container_dir, run_as_root=True)
|
if img.mount():
|
||||||
|
return img
|
||||||
|
else:
|
||||||
|
raise exception.Error(img.errors)
|
||||||
except Exception, exn:
|
except Exception, exn:
|
||||||
LOG.exception(_('Failed to mount filesystem: %s'), exn)
|
LOG.exception(_('Failed to mount filesystem: %s'), exn)
|
||||||
_unlink_device(device, nbd)
|
|
||||||
|
|
||||||
|
|
||||||
def destroy_container(target, instance, nbd=False):
|
def destroy_container(img):
|
||||||
"""Destroy the container once it terminates.
|
"""Destroy the container once it terminates.
|
||||||
|
|
||||||
It will umount the container that is mounted, try to find the loopback
|
It will umount the container that is mounted,
|
||||||
device associated with the container and delete it.
|
and delete any linked devices.
|
||||||
|
|
||||||
LXC does not support qcow2 images yet.
|
LXC does not support qcow2 images yet.
|
||||||
"""
|
"""
|
||||||
out, err = utils.execute('mount', run_as_root=True)
|
|
||||||
for loop in out.splitlines():
|
|
||||||
if instance['name'] in loop:
|
|
||||||
device = loop.split()[0]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
container_dir = '%s/rootfs' % target
|
if img:
|
||||||
utils.execute('umount', container_dir, run_as_root=True)
|
img.umount()
|
||||||
_unlink_device(device, nbd)
|
|
||||||
except Exception, exn:
|
except Exception, exn:
|
||||||
LOG.exception(_('Failed to remove container: %s'), exn)
|
LOG.exception(_('Failed to remove container: %s'), exn)
|
||||||
|
|
||||||
|
|
||||||
def _link_device(image, nbd):
|
|
||||||
"""Link image to device using loopback or nbd"""
|
|
||||||
|
|
||||||
if nbd:
|
|
||||||
device = _allocate_device()
|
|
||||||
utils.execute('qemu-nbd', '-c', device, image, run_as_root=True)
|
|
||||||
# NOTE(vish): this forks into another process, so give it a chance
|
|
||||||
# to set up before continuuing
|
|
||||||
for i in xrange(FLAGS.timeout_nbd):
|
|
||||||
if os.path.exists("/sys/block/%s/pid" % os.path.basename(device)):
|
|
||||||
return device
|
|
||||||
time.sleep(1)
|
|
||||||
raise exception.Error(_('nbd device %s did not show up') % device)
|
|
||||||
else:
|
|
||||||
out, err = utils.execute('losetup', '--find', '--show', image,
|
|
||||||
run_as_root=True)
|
|
||||||
if err:
|
|
||||||
raise exception.Error(_('Could not attach image to loopback: %s')
|
|
||||||
% err)
|
|
||||||
return out.strip()
|
|
||||||
|
|
||||||
|
|
||||||
def _unlink_device(device, nbd):
|
|
||||||
"""Unlink image from device using loopback or nbd"""
|
|
||||||
if nbd:
|
|
||||||
utils.execute('qemu-nbd', '-d', device, run_as_root=True)
|
|
||||||
_free_device(device)
|
|
||||||
else:
|
|
||||||
utils.execute('losetup', '--detach', device, run_as_root=True)
|
|
||||||
|
|
||||||
|
|
||||||
_DEVICES = ['/dev/nbd%s' % i for i in xrange(FLAGS.max_nbd_devices)]
|
|
||||||
|
|
||||||
|
|
||||||
def _allocate_device():
|
|
||||||
# NOTE(vish): This assumes no other processes are allocating nbd devices.
|
|
||||||
# It may race cause a race condition if multiple
|
|
||||||
# workers are running on a given machine.
|
|
||||||
|
|
||||||
while True:
|
|
||||||
if not _DEVICES:
|
|
||||||
raise exception.Error(_('No free nbd devices'))
|
|
||||||
device = _DEVICES.pop()
|
|
||||||
if not os.path.exists("/sys/block/%s/pid" % os.path.basename(device)):
|
|
||||||
break
|
|
||||||
return device
|
|
||||||
|
|
||||||
|
|
||||||
def _free_device(device):
|
|
||||||
_DEVICES.append(device)
|
|
||||||
|
|
||||||
|
|
||||||
def inject_data_into_fs(fs, key, net, metadata, execute):
|
def inject_data_into_fs(fs, key, net, metadata, execute):
|
||||||
"""Injects data into a filesystem already mounted by the caller.
|
"""Injects data into a filesystem already mounted by the caller.
|
||||||
Virt connections can call this directly if they mount their fs
|
Virt connections can call this directly if they mount their fs
|
41
nova/virt/disk/loop.py
Normal file
41
nova/virt/disk/loop.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 Red Hat, 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.
|
||||||
|
"""Support for mounting images with the loop device"""
|
||||||
|
|
||||||
|
from nova import utils
|
||||||
|
from nova.virt.disk import mount
|
||||||
|
|
||||||
|
|
||||||
|
class Mount(mount.Mount):
|
||||||
|
"""loop back support for raw images."""
|
||||||
|
mode = 'loop'
|
||||||
|
|
||||||
|
def get_dev(self):
|
||||||
|
out, err = utils.trycmd('losetup', '--find', '--show', self.image,
|
||||||
|
run_as_root=True)
|
||||||
|
if err:
|
||||||
|
self.error = _('Could not attach image to loopback: %s') % err
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.device = out.strip()
|
||||||
|
self.linked = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
def unget_dev(self):
|
||||||
|
if not self.linked:
|
||||||
|
return
|
||||||
|
utils.execute('losetup', '--detach', self.device, run_as_root=True)
|
||||||
|
self.linked = False
|
142
nova/virt/disk/mount.py
Normal file
142
nova/virt/disk/mount.py
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 Red Hat, 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.
|
||||||
|
"""Support for mounting virtual image files"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from nova import log as logging
|
||||||
|
from nova import utils
|
||||||
|
|
||||||
|
LOG = logging.getLogger('nova.compute.disk')
|
||||||
|
|
||||||
|
|
||||||
|
class Mount(object):
|
||||||
|
"""Standard mounting operations, that can be overridden by subclasses.
|
||||||
|
|
||||||
|
The basic device operations provided are get, map and mount,
|
||||||
|
to be called in that order.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, image, mount_dir, partition=None,
|
||||||
|
disable_auto_fsck=False):
|
||||||
|
|
||||||
|
# Input
|
||||||
|
self.image = image
|
||||||
|
self.partition = partition
|
||||||
|
self.disable_auto_fsck = disable_auto_fsck
|
||||||
|
self.mount_dir = mount_dir
|
||||||
|
|
||||||
|
# Output
|
||||||
|
self.error = ""
|
||||||
|
|
||||||
|
# Internal
|
||||||
|
self.linked = self.mapped = self.mounted = False
|
||||||
|
self.device = self.mapped_device = None
|
||||||
|
|
||||||
|
def get_dev(self):
|
||||||
|
"""Make the image available as a block device in the file system."""
|
||||||
|
self.device = None
|
||||||
|
self.linked = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
def unget_dev(self):
|
||||||
|
"""Release the block device from the file system namespace."""
|
||||||
|
self.linked = False
|
||||||
|
|
||||||
|
def map_dev(self):
|
||||||
|
"""Map partitions of the device to the file system namespace."""
|
||||||
|
assert(os.path.exists(self.device))
|
||||||
|
|
||||||
|
if self.partition:
|
||||||
|
map_path = '/dev/mapper/%sp%s' % (self.device.split('/')[-1],
|
||||||
|
self.partition)
|
||||||
|
assert(not os.path.exists(map_path))
|
||||||
|
|
||||||
|
# Note kpartx can output warnings to stderr and succeed
|
||||||
|
# Also it can output failures to stderr and "succeed"
|
||||||
|
# So we just go on the existence of the mapped device
|
||||||
|
_out, err = utils.trycmd('kpartx', '-a', self.device,
|
||||||
|
run_as_root=True, discard_warnings=True)
|
||||||
|
|
||||||
|
# Note kpartx does nothing when presented with a raw image,
|
||||||
|
# so given we only use it when we expect a partitioned image, fail
|
||||||
|
if not os.path.exists(map_path):
|
||||||
|
if not err:
|
||||||
|
err = _('no partitions found')
|
||||||
|
self.error = _('Failed to map partitions: %s') % err
|
||||||
|
else:
|
||||||
|
self.mapped_device = map_path
|
||||||
|
self.mapped = True
|
||||||
|
else:
|
||||||
|
self.mapped_device = self.device
|
||||||
|
self.mapped = True
|
||||||
|
|
||||||
|
# This is an orthogonal operation
|
||||||
|
# which only needs to be done once
|
||||||
|
if self.disable_auto_fsck and self.mapped:
|
||||||
|
self.disable_auto_fsck = False
|
||||||
|
# attempt to set ext[234] so that it doesn't auto-fsck
|
||||||
|
_out, err = utils.trycmd('tune2fs', '-c', 0, '-i', 0,
|
||||||
|
self.mapped_device, run_as_root=True)
|
||||||
|
if err:
|
||||||
|
LOG.info(_('Failed to disable fs check: %s') % err)
|
||||||
|
|
||||||
|
return self.mapped
|
||||||
|
|
||||||
|
def unmap_dev(self):
|
||||||
|
"""Remove partitions of the device from the file system namespace."""
|
||||||
|
if not self.mapped:
|
||||||
|
return
|
||||||
|
if self.partition:
|
||||||
|
utils.execute('kpartx', '-d', self.device, run_as_root=True)
|
||||||
|
self.mapped = False
|
||||||
|
|
||||||
|
def mnt_dev(self):
|
||||||
|
"""Mount the device into the file system."""
|
||||||
|
_out, err = utils.trycmd('mount', self.mapped_device, self.mount_dir,
|
||||||
|
run_as_root=True)
|
||||||
|
if err:
|
||||||
|
self.error = _('Failed to mount filesystem: %s') % err
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.mounted = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
def unmnt_dev(self):
|
||||||
|
"""Unmount the device from the file system."""
|
||||||
|
if not self.mounted:
|
||||||
|
return
|
||||||
|
utils.execute('umount', self.mapped_device, run_as_root=True)
|
||||||
|
self.mounted = False
|
||||||
|
|
||||||
|
def do_mount(self):
|
||||||
|
"""Call the get, map and mnt operations."""
|
||||||
|
status = False
|
||||||
|
try:
|
||||||
|
status = self.get_dev() and self.map_dev() and self.mnt_dev()
|
||||||
|
finally:
|
||||||
|
if not status:
|
||||||
|
self.do_umount()
|
||||||
|
return status
|
||||||
|
|
||||||
|
def do_umount(self):
|
||||||
|
"""Call the unmnt, unmap and unget operations."""
|
||||||
|
if self.mounted:
|
||||||
|
self.unmnt_dev()
|
||||||
|
if self.mapped:
|
||||||
|
self.unmap_dev()
|
||||||
|
if self.linked:
|
||||||
|
self.unget_dev()
|
95
nova/virt/disk/nbd.py
Normal file
95
nova/virt/disk/nbd.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 Red Hat, 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.
|
||||||
|
"""Support for mounting images with qemu-nbd"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
from nova import flags
|
||||||
|
from nova import utils
|
||||||
|
from nova.virt.disk import mount
|
||||||
|
|
||||||
|
FLAGS = flags.FLAGS
|
||||||
|
flags.DEFINE_integer('timeout_nbd', 10,
|
||||||
|
'time to wait for a NBD device coming up')
|
||||||
|
flags.DEFINE_integer('max_nbd_devices', 16,
|
||||||
|
'maximum number of possible nbd devices')
|
||||||
|
|
||||||
|
|
||||||
|
class Mount(mount.Mount):
|
||||||
|
"""qemu-nbd support disk images."""
|
||||||
|
mode = 'nbd'
|
||||||
|
|
||||||
|
# NOTE(padraig): There are three issues with this nbd device handling
|
||||||
|
# 1. max_nbd_devices should be inferred (#861504)
|
||||||
|
# 2. We assume nothing else on the system uses nbd devices
|
||||||
|
# 3. Multiple workers on a system can race against each other
|
||||||
|
# A patch has been proposed in Nov 2011, to add add a -f option to
|
||||||
|
# qemu-nbd, akin to losetup -f. One could test for this by running qemu-nbd
|
||||||
|
# with just the -f option, where it will fail if not supported, or if there
|
||||||
|
# are no free devices. Note that patch currently hardcodes 16 devices.
|
||||||
|
# We might be able to alleviate problem 2. by scanning /proc/partitions
|
||||||
|
# like the aformentioned patch does.
|
||||||
|
_DEVICES = ['/dev/nbd%s' % i for i in range(FLAGS.max_nbd_devices)]
|
||||||
|
|
||||||
|
def _allocate_nbd(self):
|
||||||
|
while True:
|
||||||
|
if not self._DEVICES:
|
||||||
|
# really want to log this info, not raise
|
||||||
|
self.error = _('No free nbd devices')
|
||||||
|
return None
|
||||||
|
device = self._DEVICES.pop()
|
||||||
|
if not os.path.exists("/sys/block/%s/pid" %
|
||||||
|
os.path.basename(device)):
|
||||||
|
break
|
||||||
|
return device
|
||||||
|
|
||||||
|
def _free_nbd(self, device):
|
||||||
|
self._DEVICES.append(device)
|
||||||
|
|
||||||
|
def get_dev(self):
|
||||||
|
device = self._allocate_nbd()
|
||||||
|
if not device:
|
||||||
|
return False
|
||||||
|
_out, err = utils.trycmd('qemu-nbd', '-c', device, self.image,
|
||||||
|
run_as_root=True)
|
||||||
|
if err:
|
||||||
|
self.error = _('qemu-nbd error: %s') % err
|
||||||
|
self._free_nbd(device)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# NOTE(vish): this forks into another process, so give it a chance
|
||||||
|
# to set up before continuing
|
||||||
|
for _i in range(FLAGS.timeout_nbd):
|
||||||
|
if os.path.exists("/sys/block/%s/pid" % os.path.basename(device)):
|
||||||
|
self.device = device
|
||||||
|
break
|
||||||
|
time.sleep(1)
|
||||||
|
else:
|
||||||
|
self.error = _('nbd device %s did not show up') % device
|
||||||
|
self._free_nbd(device)
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.linked = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
def unget_dev(self):
|
||||||
|
if not self.linked:
|
||||||
|
return
|
||||||
|
utils.execute('qemu-nbd', '-d', self.device, run_as_root=True)
|
||||||
|
self._free_nbd(self.device)
|
||||||
|
self.linked = False
|
||||||
|
self.device = None
|
@@ -63,7 +63,7 @@ from nova import flags
|
|||||||
import nova.image
|
import nova.image
|
||||||
from nova import log as logging
|
from nova import log as logging
|
||||||
from nova import utils
|
from nova import utils
|
||||||
from nova.virt import disk
|
from nova.virt.disk import api as disk
|
||||||
from nova.virt import driver
|
from nova.virt import driver
|
||||||
from nova.virt import images
|
from nova.virt import images
|
||||||
from nova.virt.libvirt import utils as libvirt_utils
|
from nova.virt.libvirt import utils as libvirt_utils
|
||||||
@@ -181,6 +181,7 @@ class LibvirtConnection(driver.ComputeDriver):
|
|||||||
|
|
||||||
self._host_state = None
|
self._host_state = None
|
||||||
self._wrapped_conn = None
|
self._wrapped_conn = None
|
||||||
|
self.container = None
|
||||||
self.read_only = read_only
|
self.read_only = read_only
|
||||||
|
|
||||||
fw_class = utils.import_class(FLAGS.firewall_driver)
|
fw_class = utils.import_class(FLAGS.firewall_driver)
|
||||||
@@ -404,7 +405,7 @@ class LibvirtConnection(driver.ComputeDriver):
|
|||||||
LOG.info(_('instance %(instance_name)s: deleting instance files'
|
LOG.info(_('instance %(instance_name)s: deleting instance files'
|
||||||
' %(target)s') % locals())
|
' %(target)s') % locals())
|
||||||
if FLAGS.libvirt_type == 'lxc':
|
if FLAGS.libvirt_type == 'lxc':
|
||||||
disk.destroy_container(target, instance, nbd=FLAGS.use_cow_images)
|
disk.destroy_container(self.container)
|
||||||
if os.path.exists(target):
|
if os.path.exists(target):
|
||||||
shutil.rmtree(target)
|
shutil.rmtree(target)
|
||||||
|
|
||||||
@@ -1037,11 +1038,11 @@ class LibvirtConnection(driver.ComputeDriver):
|
|||||||
if config_drive: # Should be True or None by now.
|
if config_drive: # Should be True or None by now.
|
||||||
injection_path = basepath('disk.config')
|
injection_path = basepath('disk.config')
|
||||||
img_id = 'config-drive'
|
img_id = 'config-drive'
|
||||||
tune2fs = False
|
disable_auto_fsck = False
|
||||||
else:
|
else:
|
||||||
injection_path = basepath('disk')
|
injection_path = basepath('disk')
|
||||||
img_id = inst.image_ref
|
img_id = inst.image_ref
|
||||||
tune2fs = True
|
disable_auto_fsck = True
|
||||||
|
|
||||||
for injection in ('metadata', 'key', 'net'):
|
for injection in ('metadata', 'key', 'net'):
|
||||||
if locals()[injection]:
|
if locals()[injection]:
|
||||||
@@ -1051,8 +1052,8 @@ class LibvirtConnection(driver.ComputeDriver):
|
|||||||
try:
|
try:
|
||||||
disk.inject_data(injection_path, key, net, metadata,
|
disk.inject_data(injection_path, key, net, metadata,
|
||||||
partition=target_partition,
|
partition=target_partition,
|
||||||
nbd=FLAGS.use_cow_images,
|
use_cow=FLAGS.use_cow_images,
|
||||||
tune2fs=tune2fs)
|
disable_auto_fsck=disable_auto_fsck)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# This could be a windows image, or a vmdk format disk
|
# This could be a windows image, or a vmdk format disk
|
||||||
@@ -1060,9 +1061,9 @@ class LibvirtConnection(driver.ComputeDriver):
|
|||||||
' data into image %(img_id)s (%(e)s)') % locals())
|
' data into image %(img_id)s (%(e)s)') % locals())
|
||||||
|
|
||||||
if FLAGS.libvirt_type == 'lxc':
|
if FLAGS.libvirt_type == 'lxc':
|
||||||
disk.setup_container(basepath('disk'),
|
self.container = disk.setup_container(basepath('disk'),
|
||||||
container_dir=container_dir,
|
container_dir=container_dir,
|
||||||
nbd=FLAGS.use_cow_images)
|
use_cow=FLAGS.use_cow_images)
|
||||||
|
|
||||||
if FLAGS.libvirt_type == 'uml':
|
if FLAGS.libvirt_type == 'uml':
|
||||||
libvirt_utils.chown(basepath('disk'), 'root')
|
libvirt_utils.chown(basepath('disk'), 'root')
|
||||||
|
@@ -26,7 +26,7 @@ import shutil
|
|||||||
from nova import exception
|
from nova import exception
|
||||||
from nova import flags
|
from nova import flags
|
||||||
from nova import utils
|
from nova import utils
|
||||||
from nova.virt import disk
|
from nova.virt.disk import api as disk
|
||||||
from nova.virt import images
|
from nova.virt import images
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
FLAGS = flags.FLAGS
|
||||||
|
@@ -40,7 +40,7 @@ from nova import log as logging
|
|||||||
from nova import utils
|
from nova import utils
|
||||||
from nova.compute import instance_types
|
from nova.compute import instance_types
|
||||||
from nova.compute import power_state
|
from nova.compute import power_state
|
||||||
from nova.virt import disk
|
from nova.virt.disk import api as disk
|
||||||
from nova.virt.xenapi import HelperBase
|
from nova.virt.xenapi import HelperBase
|
||||||
from nova.virt.xenapi import volume_utils
|
from nova.virt.xenapi import volume_utils
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user