nova/nova/virt/disk/mount/api.py

235 lines
8.7 KiB
Python

# 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
import time
from nova.openstack.common import importutils
from nova.openstack.common import log as logging
from nova import utils
LOG = logging.getLogger(__name__)
MAX_DEVICE_WAIT = 30
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.
"""
mode = None # to be overridden in subclasses
@staticmethod
def instance_for_format(imgfile, mountdir, partition, imgfmt):
LOG.debug(_("Instance for format imgfile=%(imgfile)s "
"mountdir=%(mountdir)s partition=%(partition)s "
"imgfmt=%(imgfmt)s"),
{'imgfile': imgfile, 'mountdir': mountdir,
'partition': partition, 'imgfmt': imgfmt})
if imgfmt == "raw":
LOG.debug(_("Using LoopMount"))
return importutils.import_object(
"nova.virt.disk.mount.loop.LoopMount",
imgfile, mountdir, partition)
else:
LOG.debug(_("Using NbdMount"))
return importutils.import_object(
"nova.virt.disk.mount.nbd.NbdMount",
imgfile, mountdir, partition)
@staticmethod
def instance_for_device(imgfile, mountdir, partition, device):
LOG.debug(_("Instance for device imgfile=%(imgfile)s "
"mountdir=%(mountdir)s partition=%(partition)s "
"device=%(device)s"),
{'imgfile': imgfile, 'mountdir': mountdir,
'partition': partition, 'device': device})
if "loop" in device:
LOG.debug(_("Using LoopMount"))
return importutils.import_object(
"nova.virt.disk.mount.loop.LoopMount",
imgfile, mountdir, partition, device)
else:
LOG.debug(_("Using NbdMount"))
return importutils.import_object(
"nova.virt.disk.mount.nbd.NbdMount",
imgfile, mountdir, partition, device)
def __init__(self, image, mount_dir, partition=None, device=None):
# Input
self.image = image
self.partition = partition
self.mount_dir = mount_dir
# Output
self.error = ""
# Internal
self.linked = self.mapped = self.mounted = self.automapped = False
self.device = self.mapped_device = device
# Reset to mounted dir if possible
self.reset_dev()
def reset_dev(self):
"""Reset device paths to allow unmounting."""
if not self.device:
return
self.linked = self.mapped = self.mounted = True
device = self.device
if os.path.isabs(device) and os.path.exists(device):
if device.startswith('/dev/mapper/'):
device = os.path.basename(device)
device, self.partition = device.rsplit('p', 1)
self.device = os.path.join('/dev', device)
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 _get_dev_retry_helper(self):
"""Some implementations need to retry their get_dev."""
# NOTE(mikal): This method helps implement retries. The implementation
# simply calls _get_dev_retry_helper from their get_dev, and implements
# _inner_get_dev with their device acquisition logic. The NBD
# implementation has an example.
start_time = time.time()
device = self._inner_get_dev()
while not device:
LOG.info(_('Device allocation failed. Will retry in 2 seconds.'))
time.sleep(2)
if time.time() - start_time > MAX_DEVICE_WAIT:
LOG.warn(_('Device allocation failed after repeated retries.'))
return False
device = self._inner_get_dev()
return True
def _inner_get_dev(self):
raise NotImplementedError()
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))
LOG.debug(_("Map dev %s"), self.device)
automapped_path = '/dev/%sp%s' % (os.path.basename(self.device),
self.partition)
if self.partition == -1:
self.error = _('partition search unsupported with %s') % self.mode
elif self.partition and not os.path.exists(automapped_path):
map_path = '/dev/mapper/%sp%s' % (os.path.basename(self.device),
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 = _('partition %s not found') % self.partition
self.error = _('Failed to map partitions: %s') % err
else:
self.mapped_device = map_path
self.mapped = True
elif self.partition and os.path.exists(automapped_path):
# Note auto mapping can be enabled with the 'max_part' option
# to the nbd or loop kernel modules. Beware of possible races
# in the partition scanning for _loop_ devices though
# (details in bug 1024586), which are currently uncatered for.
self.mapped_device = automapped_path
self.mapped = True
self.automapped = True
else:
self.mapped_device = self.device
self.mapped = True
return self.mapped
def unmap_dev(self):
"""Remove partitions of the device from the file system namespace."""
if not self.mapped:
return
LOG.debug(_("Unmap dev %s"), self.device)
if self.partition and not self.automapped:
utils.execute('kpartx', '-d', self.device, run_as_root=True)
self.mapped = False
self.automapped = False
def mnt_dev(self):
"""Mount the device into the file system."""
LOG.debug(_("Mount %(dev)s on %(dir)s") %
{'dev': self.mapped_device, 'dir': self.mount_dir})
_out, err = utils.trycmd('mount', self.mapped_device, self.mount_dir,
discard_warnings=True, run_as_root=True)
if err:
self.error = _('Failed to mount filesystem: %s') % err
LOG.debug(self.error)
return False
self.mounted = True
return True
def unmnt_dev(self):
"""Unmount the device from the file system."""
if not self.mounted:
return
LOG.debug(_("Umount %s") % self.mapped_device)
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:
LOG.debug(_("Fail to mount, tearing back down"))
self.do_teardown()
return status
def do_umount(self):
"""Call the unmnt operation."""
if self.mounted:
self.unmnt_dev()
def do_teardown(self):
"""Call the umnt, unmap, and unget operations."""
if self.mounted:
self.unmnt_dev()
if self.mapped:
self.unmap_dev()
if self.linked:
self.unget_dev()