Source code for lago.guestfs_tools
#
# Copyright 2017 Red Hat, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Refer to the README and COPYING files for full details of the license
#
from __future__ import absolute_import
import os
import guestfs
import logging
import time
import contextlib
from lago.plugins.vm import ExtractPathNoPathError
from lago.utils import LagoException
LOGGER = logging.getLogger(__name__)
[docs]class GuestFSError(LagoException):
pass
[docs]@contextlib.contextmanager
def guestfs_conn_ro(disk):
"""
Open a GuestFS handle and add `disk` in read only mode.
Args:
disk(disk path): Path to the disk.
Yields:
guestfs.GuestFS: Open GuestFS handle
Raises:
:exc:`GuestFSError`: On any guestfs operation failure
"""
disk_path = os.path.expandvars(disk)
conn = guestfs.GuestFS(python_return_dict=True)
conn.add_drive_ro(disk_path)
conn.set_backend(os.environ.get('LIBGUESTFS_BACKEND', 'direct'))
try:
conn.launch()
except RuntimeError as err:
LOGGER.debug(err)
raise GuestFSError(
'failed starting guestfs in readonly mode for disk: {0}'.
format(disk)
)
try:
yield conn
finally:
conn.shutdown()
conn.close()
[docs]@contextlib.contextmanager
def guestfs_conn_mount_ro(disk_path, disk_root, retries=5, wait=1):
"""
Open a GuestFS handle with `disk_path` and try mounting the root
filesystem. `disk_root` is a hint where it should be looked and will
only be used if GuestFS will not be able to deduce it independently.
Note that mounting a live guest, can lead to filesystem inconsistencies,
causing the mount operation to fail. As we use readonly mode, this is
safe, but the operation itself can still fail. Therefore, this method
will watch for mount failures and retry 5 times before throwing
an exception.
Args:
disk_path(str): Path to the disk.
disk_root(str): Hint what is the root device with the OS filesystem.
retries(int): Number of retries for :func:`~guestfs.GuestFS.mount_ro`
operation. Note that on each retry a new GuestFS handle will
be used.
wait(int): Time to wait between retries.
Yields:
guestfs.GuestFS: An open GuestFS handle.
Raises:
:exc:`GuestFSError`: On any guestfs operation error, including
exceeding retries for the :func:`~guestfs.GuestFS.mount_ro`
operation.
"""
for attempt in range(retries):
with guestfs_conn_ro(disk_path) as conn:
rootfs = find_rootfs(conn, disk_root)
try:
conn.mount_ro(rootfs, '/')
except RuntimeError as err:
LOGGER.debug(err)
if attempt < retries - 1:
LOGGER.debug(
(
'failed mounting %s:%s using guestfs, '
'attempt %s/%s'
), disk_path, rootfs, attempt + 1, retries
)
time.sleep(wait)
continue
else:
raise GuestFSError(
'failed mounting {0}:{1} using guestfs'.format(
disk_path, rootfs
)
)
yield conn
try:
conn.umount(rootfs)
except RuntimeError as err:
LOGGER.debug(err)
raise GuestFSError(
('failed unmounting {0}:{1} using'
'guestfs').format(disk_path, rootfs)
)
break
[docs]def find_rootfs(conn, disk_root):
"""
Find the image's device root filesystem, and return its path.
1. Use :func:`guestfs.GuestFS.inspect_os` method. If it returns more than
one root filesystem or None, try:
2. Find an exact match of `disk_root` from
:func:`guestfs.GuestFS.list_filesystems`, if none is found, try:
3. Return the device that has the substring `disk_root` contained in it,
from the output of :func:`guestfs.GuestFS.list_filesystems`.
Args:
conn(guestfs.GuestFS): Open GuestFS handle.
disk_root(str): Root device to search for. Note that by default, if
guestfs can deduce the filesystem, it will not be used.
Returns:
str: root device path
Raises:
:exc:`GuestFSError` if no root filesystem was found
"""
rootfs = conn.inspect_os()
if not rootfs or len(rootfs) > 1:
filesystems = conn.list_filesystems()
if disk_root in filesystems:
rootfs = [disk_root]
else:
rootfs = [fs for fs in filesystems.keys() if disk_root in fs]
if not rootfs:
raise GuestFSError(
'no root fs {0} could be found from list {1}'.format(
disk_root, str(filesystems)
)
)
return sorted(rootfs)[0]
[docs]def _copy_path(conn, guest_path, host_path):
if conn.is_file(guest_path, followsymlinks=True):
try:
conn.download(guest_path, host_path)
except RuntimeError as err:
LOGGER.debug(err)
raise GuestFSError(
'failed copying file {0} to {1} using guestfs'.format(
guest_path, host_path
)
)
elif conn.is_dir(guest_path, followsymlinks=True):
if not os.path.isdir(host_path):
os.makedirs(host_path)
try:
conn.copy_out(guest_path, host_path)
except RuntimeError as err:
LOGGER.debug(err)
raise GuestFSError(
'failed copying directory {0} to {1} using guestfs'.format(
guest_path, host_path
)
)
else:
raise ExtractPathNoPathError(
('unable to extract {0}: path does not '
'exist.').format(guest_path)
)