Ejemplo n.º 1
0
    def __init__(self, disk, parent=None, index="0", size=0, offset=0, flag='alloc', slot=0, fstype=None, key="",
                 vstype='', volume_detector='auto'):
        """Creates a Volume object that is not mounted yet.

        Only use arguments as keyword arguments.

        :param disk: the parent disk
        :type disk: :class:`Disk`
        :param parent: the parent volume or disk.
        :param str index: the volume index within its volume system, see the attribute documentation.
        :param int size: the volume size, see the attribute documentation.
        :param int offset: the volume offset, see the attribute documentation.
        :param str flag: the volume flag, see the attribute documentation.
        :param int slot: the volume slot, see the attribute documentation.
        :param FileSystem fstype: the fstype you wish to use for this Volume.
            If not specified, will be retrieved from the ImageParser instance instead.
        :param str key: the key to use for this Volume.
        :param str vstype: the volume system type to use.
        :param str volume_detector: the volume system detection method to use
        """

        self.parent = parent
        self.disk = disk

        # Should be filled somewhere
        self.size = size
        self.offset = offset
        self.index = index
        self.slot = slot
        self.flag = flag
        self.block_size = self.disk.block_size

        self.volumes = VolumeSystem(parent=self, vstype=vstype, volume_detector=volume_detector)

        self._get_fstype_from_parser(fstype)

        if key:
            self.key = key
        elif self.index in self.disk.parser.keys:
            self.key = self.disk.parser.keys[self.index]
        elif '*' in self.disk.parser.keys:
            self.key = self.disk.parser.keys['*']
        else:
            self.key = ""

        self.info = {}
        self._paths = {}

        self.mountpoint = ""
        self.loopback = ""
        self.was_mounted = False
        self.is_mounted = False
Ejemplo n.º 2
0
    def __init__(self,
                 parser,
                 path,
                 index=None,
                 offset=0,
                 block_size=BLOCK_SIZE,
                 read_write=False,
                 vstype='',
                 disk_mounter='auto',
                 volume_detector='auto'):
        """Instantiation of this class does not automatically mount, detect or analyse the disk. You will need the
        :func:`init` method for this.

        Only use arguments offset and further as keyword arguments.

        :param parser: the parent parser
        :type parser: :class:`ImageParser`
        :param str path: the path of the Disk
        :param str index: the base index of this Disk
        :param int offset: offset of the disk where the volume (system) resides
        :param int block_size:
        :param bool read_write: indicates whether the disk should be mounted with a read-write cache enabled
        :param str vstype: the volume system type to use.
        :param str disk_mounter: the method to mount the base image with
        :param str volume_detector: the volume system detection method to use
        """

        self.parser = parser

        # Find the type and the paths
        path = os.path.expandvars(os.path.expanduser(path))
        self.paths = sorted(_util.expand_path(path))

        self.offset = offset
        self.block_size = block_size
        self.read_write = read_write
        self.disk_mounter = disk_mounter or 'auto'
        self.index = index

        self._name = os.path.split(path)[1]
        self._paths = {}
        self.rwpath = ""
        self.mountpoint = ""
        self.volumes = VolumeSystem(parent=self,
                                    volume_detector=volume_detector,
                                    vstype=vstype)

        self.was_mounted = False
        self.is_mounted = False

        self._disktype = defaultdict(dict)
Ejemplo n.º 3
0
    def __init__(self, disk, parent=None, index="0", size=0, offset=0, flag='alloc', slot=0, fstype=None, key="",
                 vstype='', volume_detector='auto'):
        """Creates a Volume object that is not mounted yet.

        Only use arguments as keyword arguments.

        :param disk: the parent disk
        :type disk: :class:`Disk`
        :param parent: the parent volume or disk.
        :param str index: the volume index within its volume system, see the attribute documentation.
        :param int size: the volume size, see the attribute documentation.
        :param int offset: the volume offset, see the attribute documentation.
        :param str flag: the volume flag, see the attribute documentation.
        :param int slot: the volume slot, see the attribute documentation.
        :param FileSystemType fstype: the fstype you wish to use for this Volume.
            If not specified, will be retrieved from the ImageParser instance instead.
        :param str key: the key to use for this Volume.
        :param str vstype: the volume system type to use.
        :param str volume_detector: the volume system detection method to use
        """

        self.parent = parent
        self.disk = disk

        # Should be filled somewhere
        self.size = size
        self.offset = offset
        self.index = index
        self.slot = slot
        self.flag = flag
        self.block_size = self.disk.block_size

        self.volumes = VolumeSystem(parent=self, vstype=vstype, volume_detector=volume_detector)

        self._get_fstype_from_parser(fstype)

        if key:
            self.key = key
        elif self.index in self.disk.parser.keys:
            self.key = self.disk.parser.keys[self.index]
        elif '*' in self.disk.parser.keys:
            self.key = self.disk.parser.keys['*']
        else:
            self.key = ""

        self.info = {}
        self._paths = {}

        self.mountpoint = ""
        self.loopback = ""
        self.was_mounted = False
        self.is_mounted = False
Ejemplo n.º 4
0
    def __init__(
        self,
        parser,
        path,
        index=None,
        offset=0,
        block_size=BLOCK_SIZE,
        read_write=False,
        vstype="",
        disk_mounter="auto",
        volume_detector="auto",
    ):
        """Instantiation of this class does not automatically mount, detect or analyse the disk. You will need the
        :func:`init` method for this.

        Only use arguments offset and further as keyword arguments.

        :param parser: the parent parser
        :type parser: :class:`ImageParser`
        :param str path: the path of the Disk
        :param str index: the base index of this Disk
        :param int offset: offset of the disk where the volume (system) resides
        :param int block_size:
        :param bool read_write: indicates whether the disk should be mounted with a read-write cache enabled
        :param str vstype: the volume system type to use.
        :param str disk_mounter: the method to mount the base image with
        :param str volume_detector: the volume system detection method to use
        """

        self.parser = parser

        # Find the type and the paths
        path = os.path.expandvars(os.path.expanduser(path))
        self.paths = sorted(_util.expand_path(path))

        self.offset = offset
        self.block_size = block_size
        self.read_write = read_write
        self.disk_mounter = disk_mounter or "auto"
        self.index = index

        self._name = os.path.split(path)[1]
        self._paths = {}
        self.rwpath = ""
        self.mountpoint = ""
        self.volumes = VolumeSystem(parent=self, volume_detector=volume_detector, vstype=vstype)

        self.was_mounted = False
        self.is_mounted = False

        self._disktype = defaultdict(dict)
Ejemplo n.º 5
0
class Disk(object):
    """Representation of a disk, image file or anything else that can be considered a disk. """

    # noinspection PyUnusedLocal
    def __init__(self,
                 parser,
                 path,
                 index=None,
                 offset=0,
                 block_size=BLOCK_SIZE,
                 read_write=False,
                 vstype='',
                 disk_mounter='auto',
                 volume_detector='auto'):
        """Instantiation of this class does not automatically mount, detect or analyse the disk. You will need the
        :func:`init` method for this.

        Only use arguments offset and further as keyword arguments.

        :param parser: the parent parser
        :type parser: :class:`ImageParser`
        :param str path: the path of the Disk
        :param str index: the base index of this Disk
        :param int offset: offset of the disk where the volume (system) resides
        :param int block_size:
        :param bool read_write: indicates whether the disk should be mounted with a read-write cache enabled
        :param str vstype: the volume system type to use.
        :param str disk_mounter: the method to mount the base image with
        :param str volume_detector: the volume system detection method to use
        """

        self.parser = parser

        # Find the type and the paths
        path = os.path.expandvars(os.path.expanduser(path))
        self.paths = sorted(_util.expand_path(path))

        self.offset = offset
        self.block_size = block_size
        self.read_write = read_write
        self.disk_mounter = disk_mounter or 'auto'
        self.index = index

        self._name = os.path.split(path)[1]
        self._paths = {}
        self.rwpath = ""
        self.mountpoint = ""
        self.volumes = VolumeSystem(parent=self,
                                    volume_detector=volume_detector,
                                    vstype=vstype)

        self.was_mounted = False
        self.is_mounted = False

        self._disktype = defaultdict(dict)

    def __unicode__(self):
        return self._name

    def __str__(self):
        return self.__unicode__()

    def __getitem__(self, item):
        return self.volumes[item]

    def get_disk_type(self):
        if _util.is_encase(self.paths[0]):
            return 'encase'
        elif _util.is_vmware(self.paths[0]):
            return 'vmdk'
        elif _util.is_compressed(self.paths[0]):
            return 'compressed'
        elif _util.is_qcow2(self.paths[0]):
            return 'qcow2'
        else:
            return 'dd'

    def _get_mount_methods(self, disk_type):
        """Finds which mount methods are suitable for the specified disk type. Returns a list of all suitable mount
        methods.
        """
        if self.disk_mounter == 'auto':
            methods = []

            def add_method_if_exists(method):
                if (method == 'avfs' and _util.command_exists('avfsd')) or \
                        (method == 'nbd' and _util.command_exists('qemu-nbd')) or \
                        _util.command_exists(method):
                    methods.append(method)

            if self.read_write:
                add_method_if_exists('xmount')
            else:
                if disk_type == 'encase':
                    add_method_if_exists('ewfmount')
                elif disk_type == 'vmdk':
                    add_method_if_exists('vmware-mount')
                    add_method_if_exists('affuse')
                elif disk_type == 'dd':
                    add_method_if_exists('affuse')
                elif disk_type == 'compressed':
                    add_method_if_exists('avfs')
                elif disk_type == 'qcow2':
                    add_method_if_exists('nbd')
                add_method_if_exists('xmount')
        else:
            methods = [self.disk_mounter]
        return methods

    def _mount_avfs(self):
        """Mounts the AVFS filesystem."""

        self._paths['avfs'] = tempfile.mkdtemp(prefix='image_mounter_avfs_')

        # start by calling the mountavfs command to initialize avfs
        _util.check_call_(['avfsd', self._paths['avfs'], '-o', 'allow_other'],
                          stdout=subprocess.PIPE)

        # no multifile support for avfs
        avfspath = self._paths['avfs'] + '/' + os.path.abspath(
            self.paths[0]) + '#'
        targetraw = os.path.join(self.mountpoint, 'avfs')

        os.symlink(avfspath, targetraw)
        logger.debug("Symlinked {} with {}".format(avfspath, targetraw))
        raw_path = self.get_raw_path()
        logger.debug("Raw path to avfs is {}".format(raw_path))
        if raw_path is None:
            raise MountpointEmptyError()

    def mount(self):
        """Mounts the base image on a temporary location using the mount method stored in :attr:`method`. If mounting
        was successful, :attr:`mountpoint` is set to the temporary mountpoint.

        If :attr:`read_write` is enabled, a temporary read-write cache is also created and stored in :attr:`rwpath`.

        :return: whether the mounting was successful
        :rtype: bool
        """

        if self.parser.casename:
            self.mountpoint = tempfile.mkdtemp(prefix='image_mounter_',
                                               suffix='_' +
                                               self.parser.casename)
        else:
            self.mountpoint = tempfile.mkdtemp(prefix='image_mounter_')

        if self.read_write:
            self.rwpath = tempfile.mkstemp(prefix="image_mounter_rw_cache_")[1]

        disk_type = self.get_disk_type()
        methods = self._get_mount_methods(disk_type)

        cmds = []
        for method in methods:
            if method == 'avfs':  # avfs does not participate in the fallback stuff, unfortunately
                self._mount_avfs()
                self.disk_mounter = method
                self.was_mounted = True
                self.is_mounted = True
                return

            elif method == 'dummy':
                os.rmdir(self.mountpoint)
                self.mountpoint = ""
                logger.debug("Raw path to dummy is {}".format(
                    self.get_raw_path()))
                self.disk_mounter = method
                self.was_mounted = True
                self.is_mounted = True
                return

            elif method == 'xmount':
                cmds.append([
                    'xmount', '--in', 'ewf' if disk_type == 'encase' else 'dd'
                ])
                if self.read_write:
                    cmds[-1].extend(['--rw', self.rwpath])
                cmds[-1].extend(
                    self.paths)  # specify all paths, xmount needs this :(
                cmds[-1].append(self.mountpoint)

            elif method == 'affuse':
                cmds.extend([[
                    'affuse', '-o', 'allow_other', self.paths[0],
                    self.mountpoint
                ], ['affuse', self.paths[0], self.mountpoint]])

            elif method == 'ewfmount':
                cmds.extend([[
                    'ewfmount', '-X', 'allow_other', self.paths[0],
                    self.mountpoint
                ], ['ewfmount', self.paths[0], self.mountpoint]])

            elif method == 'vmware-mount':
                cmds.append([
                    'vmware-mount', '-r', '-f', self.paths[0], self.mountpoint
                ])

            elif method == 'nbd':
                _util.check_output_(['modprobe', 'nbd',
                                     'max_part=63'])  # Load nbd driver
                try:
                    self._paths['nbd'] = _util.get_free_nbd_device(
                    )  # Get free nbd device
                except NoNetworkBlockAvailableError:
                    logger.warning("No free network block device found.",
                                   exc_info=True)
                    raise
                cmds.extend([[
                    'qemu-nbd', '--read-only', '-c', self._paths['nbd'],
                    self.paths[0]
                ]])

            else:
                raise ArgumentError("Unknown mount method {0}".format(
                    self.disk_mounter))

        for cmd in cmds:
            # noinspection PyBroadException
            try:
                _util.check_call_(cmd, stdout=subprocess.PIPE)
                # mounting does not seem to be instant, add a timer here
                time.sleep(.1)
            except Exception:
                logger.warning(
                    'Could not mount {0}, trying other method'.format(
                        self.paths[0]),
                    exc_info=True)
                continue
            else:
                raw_path = self.get_raw_path()
                logger.debug("Raw path to disk is {}".format(raw_path))
                self.disk_mounter = cmd[0]

                if raw_path is None:
                    raise MountpointEmptyError()
                self.was_mounted = True
                self.is_mounted = True
                return

        logger.error('Unable to mount {0}'.format(self.paths[0]))
        os.rmdir(self.mountpoint)
        self.mountpoint = ""
        raise MountError()

    def get_raw_path(self):
        """Returns the raw path to the mounted disk image, i.e. the raw :file:`.dd`, :file:`.raw` or :file:`ewf1`
        file.

        :rtype: str
        """

        if self.disk_mounter == 'dummy':
            return self.paths[0]
        else:
            if self.disk_mounter == 'avfs' and os.path.isdir(
                    os.path.join(self.mountpoint, 'avfs')):
                logger.debug(
                    "AVFS mounted as a directory, will look in directory for (random) file."
                )
                # there is no support for disks inside disks, so this will fail to work for zips containing
                # E01 files or so.
                searchdirs = (os.path.join(self.mountpoint,
                                           'avfs'), self.mountpoint)
            else:
                searchdirs = (self.mountpoint, )

            raw_path = []
            if self._paths.get('nbd'):
                raw_path.append(self._paths['nbd'])

            for searchdir in searchdirs:
                # avfs: apparently it is not a dir
                for pattern in [
                        '*.dd', '*.iso', '*.raw', '*.dmg', 'ewf1', 'flat',
                        'avfs'
                ]:
                    raw_path.extend(glob.glob(os.path.join(searchdir,
                                                           pattern)))

            if not raw_path:
                logger.warning(
                    "No viable mount file found in {}.".format(searchdirs))
                return None
            return raw_path[0]

    def get_fs_path(self):
        """Returns the path to the filesystem. Most of the times this is the image file, but may instead also return
        the MD device or loopback device the filesystem is mounted to.

        :rtype: str
        """

        if self._paths.get('md'):
            return self._paths['md']
        else:
            return self.get_raw_path()

    def detect_volumes(self, single=None):
        """Generator that detects the volumes from the Disk, using one of two methods:

        * Single volume: the entire Disk is a single volume
        * Multiple volumes: the Disk is a volume system

        :param single: If *single* is :const:`True`, this method will call :Func:`init_single_volumes`.
                       If *single* is False, only :func:`init_multiple_volumes` is called. If *single* is None,
                       :func:`init_multiple_volumes` is always called, being followed by :func:`init_single_volume`
                       if no volumes were detected.
        """
        # prevent adding the same volumes twice
        if self.volumes.has_detected:
            for v in self.volumes:
                yield v

        elif single:
            for v in self.volumes.detect_volumes(method='single'):
                yield v

        else:
            # if single == False or single == None, loop over all volumes
            amount = 0
            try:
                for v in self.volumes.detect_volumes():
                    amount += 1
                    yield v
            except ImageMounterError:
                pass  # ignore and continue to single mount

            # if single == None and no volumes were mounted, use single_volume
            if single is None and amount == 0:
                logger.info("Detecting as single volume instead")
                for v in self.volumes.detect_volumes(method='single',
                                                     force=True):
                    yield v

    def init(self,
             single=None,
             only_mount=None,
             skip_mount=None,
             swallow_exceptions=True):
        """Calls several methods required to perform a full initialisation: :func:`mount`, and
        :func:`mount_volumes` and yields all detected volumes.

        :param bool|None single: indicates whether the disk should be mounted as a single disk, not as a single disk or
            whether it should try both (defaults to :const:`None`)
        :param list only_mount: If set, must be a list of volume indexes that are only mounted.
        :param list skip_mount: If set, must be a list of volume indexes tat should not be mounted.
        :param bool swallow_exceptions: If True, Exceptions are not raised but rather set on the instance.
        :rtype: generator
        """

        self.mount()
        self.volumes.preload_volume_data()

        for v in self.init_volumes(single,
                                   only_mount=only_mount,
                                   skip_mount=skip_mount,
                                   swallow_exceptions=swallow_exceptions):
            yield v

    def init_volumes(self,
                     single=None,
                     only_mount=None,
                     skip_mount=None,
                     swallow_exceptions=True):
        """Generator that detects and mounts all volumes in the disk.

        :param single: If *single* is :const:`True`, this method will call :Func:`init_single_volumes`.
                       If *single* is False, only :func:`init_multiple_volumes` is called. If *single* is None,
                       :func:`init_multiple_volumes` is always called, being followed by :func:`init_single_volume`
                       if no volumes were detected.
        :param list only_mount: If set, must be a list of volume indexes that are only mounted.
        :param list skip_mount: If set, must be a list of volume indexes tat should not be mounted.
        :param bool swallow_exceptions: If True, Exceptions are not raised but rather set on the instance.
        """

        for volume in self.detect_volumes(single=single):
            for vol in volume.init(only_mount=only_mount,
                                   skip_mount=skip_mount,
                                   swallow_exceptions=swallow_exceptions):
                yield vol

    def get_volumes(self):
        """Gets a list of all volumes in this disk, including volumes that are contained in other volumes."""

        volumes = []
        for v in self.volumes:
            volumes.extend(v.get_volumes())
        return volumes

    def rw_active(self):
        """Indicates whether anything has been written to a read-write cache."""

        return self.rwpath and os.path.getsize(self.rwpath)

    def unmount(self, remove_rw=False, allow_lazy=False):
        """Removes all ties of this disk to the filesystem, so the image can be unmounted successfully.

        :raises SubsystemError: when one of the underlying commands fails. Some are swallowed.
        :raises CleanupError: when actual cleanup fails. Some are swallowed.
        """

        for m in list(
                sorted(self.volumes,
                       key=lambda v: v.mountpoint or "",
                       reverse=True)):
            try:
                m.unmount(allow_lazy=allow_lazy)
            except ImageMounterError:
                logger.warning("Error unmounting volume {0}".format(
                    m.mountpoint))

        if self._paths.get('nbd'):
            _util.clean_unmount(['qemu-nbd', '-d'],
                                self._paths['nbd'],
                                rmdir=False)

        if self.mountpoint:
            try:
                _util.clean_unmount(['fusermount', '-u'], self.mountpoint)
            except SubsystemError:
                if not allow_lazy:
                    raise
                _util.clean_unmount(['fusermount', '-uz'], self.mountpoint)

        if self._paths.get('avfs'):
            try:
                _util.clean_unmount(['fusermount', '-u'], self._paths['avfs'])
            except SubsystemError:
                if not allow_lazy:
                    raise
                _util.clean_unmount(['fusermount', '-uz'], self._paths['avfs'])

        if self.rw_active() and remove_rw:
            os.remove(self.rwpath)

        self.is_mounted = False
Ejemplo n.º 6
0
class Volume(object):
    """Information about a volume. Note that every detected volume gets their own Volume object, though it may or may
    not be mounted. This can be seen through the :attr:`mountpoint` attribute -- if it is not set, perhaps the
    :attr:`exception` attribute is set with an exception.
    """
    def __init__(self,
                 disk,
                 parent=None,
                 index="0",
                 size=0,
                 offset=0,
                 flag='alloc',
                 slot=0,
                 fstype=None,
                 key="",
                 vstype='',
                 volume_detector='auto'):
        """Creates a Volume object that is not mounted yet.

        Only use arguments as keyword arguments.

        :param disk: the parent disk
        :type disk: :class:`Disk`
        :param parent: the parent volume or disk.
        :param str index: the volume index within its volume system, see the attribute documentation.
        :param int size: the volume size, see the attribute documentation.
        :param int offset: the volume offset, see the attribute documentation.
        :param str flag: the volume flag, see the attribute documentation.
        :param int slot: the volume slot, see the attribute documentation.
        :param FileSystemType fstype: the fstype you wish to use for this Volume.
            If not specified, will be retrieved from the ImageParser instance instead.
        :param str key: the key to use for this Volume.
        :param str vstype: the volume system type to use.
        :param str volume_detector: the volume system detection method to use
        """

        self.parent = parent
        self.disk = disk

        # Should be filled somewhere
        self.size = size
        self.offset = offset
        self.index = index
        self.slot = slot
        self.flag = flag
        self.block_size = self.disk.block_size

        self.volumes = VolumeSystem(parent=self,
                                    vstype=vstype,
                                    volume_detector=volume_detector)

        self._get_fstype_from_parser(fstype)

        if key:
            self.key = key
        elif self.index in self.disk.parser.keys:
            self.key = self.disk.parser.keys[self.index]
        elif '*' in self.disk.parser.keys:
            self.key = self.disk.parser.keys['*']
        else:
            self.key = ""

        self.info = {}
        self._paths = {}

        self.mountpoint = ""
        self.loopback = ""
        self.was_mounted = False
        self.is_mounted = False

    def __unicode__(self):
        return '{0}:{1}'.format(self.index,
                                self.info.get('fsdescription') or '-')

    def __str__(self):
        return str(self.__unicode__())

    def __getitem__(self, item):
        return self.volumes[item]

    @property
    def numeric_index(self):
        try:
            return tuple([int(x) for x in self.index.split(".")])
        except ValueError:
            return ()

    def _get_fstype_from_parser(self, fstype=None):
        """Load fstype information from the parser instance."""
        if fstype:
            self.fstype = fstype
        elif self.index in self.disk.parser.fstypes:
            self.fstype = self.disk.parser.fstypes[self.index]
        elif '*' in self.disk.parser.fstypes:
            self.fstype = self.disk.parser.fstypes['*']
        elif '?' in self.disk.parser.fstypes and self.disk.parser.fstypes[
                '?'] is not None:
            self.fstype = "?" + self.disk.parser.fstypes['?']
        else:
            self.fstype = ""

        if self.fstype in VOLUME_SYSTEM_TYPES:
            self.volumes.vstype = self.fstype
            self.fstype = 'volumesystem'

        # convert fstype from string to a FileSystemType object
        if not isinstance(self.fstype, filesystems.FileSystemType):
            if self.fstype.startswith("?"):
                fallback = FILE_SYSTEM_TYPES[self.fstype[1:]]
                self.fstype = filesystems.FallbackFileSystemType(fallback)
            else:
                self.fstype = FILE_SYSTEM_TYPES[self.fstype]

    def get_description(self, with_size=True, with_index=True):
        """Obtains a generic description of the volume, containing the file system type, index, label and NTFS version.
        If *with_size* is provided, the volume size is also included.
        """

        desc = ''

        if with_size and self.size:
            desc += '{0} '.format(self.get_formatted_size())

        s = self.info.get('statfstype') or self.info.get(
            'fsdescription') or '-'
        if with_index:
            desc += '{1}:{0}'.format(s, self.index)
        else:
            desc += s

        if self.info.get('label'):
            desc += ' {0}'.format(self.info.get('label'))

        if self.info.get('version'):  # NTFS
            desc += ' [{0}]'.format(self.info.get('version'))

        return desc

    def get_formatted_size(self):
        """Obtains the size of the volume in a human-readable format (i.e. in TiBs, GiBs or MiBs)."""

        if self.size is not None:
            if self.size < 1024:
                return "{0} B".format(self.size)
            elif self.size < 1024**2:
                return "{0} KiB".format(round(self.size / 1024, 2))
            elif self.size < 1024**3:
                return "{0} MiB".format(round(self.size / 1024**2, 2))
            elif self.size < 1024**4:
                return "{0} GiB".format(round(self.size / 1024**3, 2))
            else:
                return "{0} TiB".format(round(self.size / 1024**4, 2))
        else:
            return self.size

    @dependencies.require(dependencies.blkid, none_on_failure=True)
    def _get_blkid_type(self):
        """Retrieves the FS type from the blkid command."""
        try:
            result = _util.check_output_(
                ['blkid', '-p', '-O',
                 str(self.offset),
                 self.get_raw_path()])
            if not result:
                return None

            # noinspection PyTypeChecker
            blkid_result = dict(re.findall(r'([A-Z]+)="(.+?)"', result))

            self.info['blkid_data'] = blkid_result

            if 'PTTYPE' in blkid_result and 'TYPE' not in blkid_result:
                return blkid_result.get('PTTYPE')
            else:
                return blkid_result.get('TYPE')

        except Exception:
            return None  # returning None is better here, since we do not care about the exception in determine_fs_type

    @dependencies.require(dependencies.magic, none_on_failure=True)
    def _get_magic_type(self):
        """Checks the volume for its magic bytes and returns the magic."""

        try:
            with io.open(self.disk.get_fs_path(), "rb") as file:
                file.seek(self.offset)
                fheader = file.read(
                    min(self.size, 4096) if self.size else 4096)
        except IOError:
            logger.exception("Failed reading first 4K bytes from volume.")
            return None

        # TODO fallback to img-cat image -s blocknum | file -
        # if we were able to load the module magic
        try:
            # noinspection PyUnresolvedReferences
            import magic

            if hasattr(magic, 'from_buffer'):
                # using https://github.com/ahupp/python-magic
                logger.debug(
                    "Using python-magic Python package for file type magic")
                result = magic.from_buffer(fheader).decode()
                self.info['magic_data'] = result
                return result

            elif hasattr(magic, 'open'):
                # using Magic file extensions by Rueben Thomas (Ubuntu python-magic module)
                logger.debug(
                    "Using python-magic system package for file type magic")
                ms = magic.open(magic.NONE)
                ms.load()
                result = ms.buffer(fheader)
                ms.close()
                self.info['magic_data'] = result
                return result

            else:
                logger.warning(
                    "The python-magic module is not available, but another module named magic was found."
                )

        except ImportError:
            logger.warning("The python-magic module is not available.")
        except AttributeError:
            logger.warning(
                "The python-magic module is not available, but another module named magic was found."
            )
        return None  # returning None is better here, since we do not care about the exception in determine_fs_type

    def get_raw_path(self, include_self=False):
        """Retrieves the base mount path of the volume. Typically equals to :func:`Disk.get_fs_path` but may also be the
        path to a logical volume. This is used to determine the source path for a mount call.

        The value returned is normally based on the parent's paths, e.g. if this volume is mounted to a more specific
        path, only its children return the more specific path, this volume itself will keep returning the same path.
        This makes for consistent use of the offset attribute. If you do not need this behaviour, you can override this
        with the include_self argument.

        This behavior, however, is not retained for paths that directly affect the volume itself, not the child volumes.
        This includes VSS stores and LV volumes.
        """

        v = self
        if not include_self:
            # lv / vss_store are exceptions, as it covers the volume itself, not the child volume
            if v._paths.get('lv'):
                return v._paths['lv']
            elif v._paths.get('vss_store'):
                return v._paths['vss_store']
            elif v.parent and v.parent != self.disk:
                v = v.parent
            else:
                return self.disk.get_fs_path()

        while True:
            if v._paths.get('lv'):
                return v._paths['lv']
            elif v._paths.get('bde'):
                return v._paths['bde'] + '/bde1'
            elif v._paths.get('luks'):
                return '/dev/mapper/' + v._paths['luks']
            elif v._paths.get('md'):
                return v._paths['md']
            elif v._paths.get('vss_store'):
                return v._paths['vss_store']

            # Only if the volume has a parent that is not a disk, we try to check the parent for a location.
            if v.parent and v.parent != self.disk:
                v = v.parent
            else:
                break
        return self.disk.get_fs_path()

    def get_safe_label(self):
        """Returns a label that is safe to add to a path in the mountpoint for this volume."""

        if self.info.get('label') == '/':
            return 'root'

        suffix = re.sub(
            r"[/ \(\)]+", "_",
            self.info.get('label')) if self.info.get('label') else ""
        if suffix and suffix[0] == '_':
            suffix = suffix[1:]
        if len(suffix) > 2 and suffix[-1] == '_':
            suffix = suffix[:-1]
        return suffix

    @dependencies.require(dependencies.photorec)
    def carve(self, freespace=True):
        """Call this method to carve the free space of the volume for (deleted) files. Note that photorec has its
        own interface that temporarily takes over the shell.

        :param freespace: indicates whether the entire volume should be carved (False) or only the free space (True)
        :type freespace: bool
        :return: string to the path where carved data is available
        :raises CommandNotFoundError: if the underlying command does not exist
        :raises SubsystemError: if the underlying command fails
        :raises NoMountpointAvailableError: if there is no mountpoint available
        :raises NoLoopbackAvailableError: if there is no loopback available (only when volume has no slot number)
        """

        self._make_mountpoint(var_name='carve', suffix="carve", in_paths=True)

        # if no slot, we need to make a loopback that we can use to carve the volume
        loopback_was_created_for_carving = False
        if not self.slot:
            if not self.loopback:
                self._find_loopback()
                # Can't carve if volume has no slot number and can't be mounted on loopback.
                loopback_was_created_for_carving = True

            # noinspection PyBroadException
            try:
                _util.check_call_([
                    "photorec", "/d",
                    self._paths['carve'] + os.sep, "/cmd", self.loopback,
                    ("freespace," if freespace else "") + "search"
                ])

                # clean out the loop device if we created it specifically for carving
                if loopback_was_created_for_carving:
                    # noinspection PyBroadException
                    try:
                        _util.check_call_(['losetup', '-d', self.loopback])
                    except Exception:
                        pass
                    else:
                        self.loopback = ""

                return self._paths['carve']
            except Exception as e:
                logger.exception("Failed carving the volume.")
                raise SubsystemError(e)
        else:
            # noinspection PyBroadException
            try:
                _util.check_call_([
                    "photorec", "/d", self._paths['carve'] + os.sep, "/cmd",
                    self.get_raw_path(),
                    str(self.slot) + (",freespace" if freespace else "") +
                    ",search"
                ])
                return self._paths['carve']

            except Exception as e:
                logger.exception("Failed carving the volume.")
                raise SubsystemError(e)

    @dependencies.require(dependencies.vshadowmount)
    def detect_volume_shadow_copies(self):
        """Method to call vshadowmount and mount NTFS volume shadow copies.

        :return: iterable with the :class:`Volume` objects of the VSS
        :raises CommandNotFoundError: if the underlying command does not exist
        :raises SubSystemError: if the underlying command fails
        :raises NoMountpointAvailableError: if there is no mountpoint available
        """

        self._make_mountpoint(var_name='vss', suffix="vss", in_paths=True)

        try:
            _util.check_call_([
                "vshadowmount", "-o",
                str(self.offset),
                self.get_raw_path(), self._paths['vss']
            ])
        except Exception as e:
            logger.exception("Failed mounting the volume shadow copies.")
            raise SubsystemError(e)
        else:
            return self.volumes.detect_volumes(vstype='vss')

    def _should_mount(self, only_mount=None, skip_mount=None):
        """Indicates whether this volume should be mounted. Internal method, used by imount.py"""

        om = only_mount is None or \
             self.index in only_mount or \
             self.info.get('lastmountpoint') in only_mount or \
             self.info.get('label') in only_mount
        sm = skip_mount is None or \
             (self.index not in skip_mount and
              self.info.get('lastmountpoint') not in skip_mount and
              self.info.get('label') not in skip_mount)
        return om and sm

    def init(self, only_mount=None, skip_mount=None, swallow_exceptions=True):
        """Generator that mounts this volume and either yields itself or recursively generates its subvolumes.

        More specifically, this function will call :func:`load_fsstat_data` (iff *no_stats* is False), followed by
        :func:`mount`, followed by a call to :func:`detect_mountpoint`, after which ``self`` is yielded, or the result
        of the :func:`init` call on each subvolume is yielded

        :param only_mount: if specified, only volume indexes in this list are mounted. Volume indexes are strings.
        :param skip_mount: if specified, volume indexes in this list are not mounted.
        :param swallow_exceptions: if True, any error occuring when mounting the volume is swallowed and added as an
            exception attribute to the yielded objects.
        """
        if swallow_exceptions:
            self.exception = None

        try:
            if not self._should_mount(only_mount, skip_mount):
                yield self
                return

            if not self.init_volume():
                yield self
                return

        except ImageMounterError as e:
            if swallow_exceptions:
                self.exception = e
            else:
                raise

        if not self.volumes:
            yield self
        else:
            for v in self.volumes:
                for s in v.init(only_mount, skip_mount, swallow_exceptions):
                    yield s

    def init_volume(self, fstype=None):
        """Initializes a single volume. You should use this method instead of :func:`mount` if you want some sane checks
        before mounting.
        """

        logger.debug("Initializing volume {0}".format(self))

        if not self._should_mount():
            return False

        if self.flag != 'alloc':
            return False

        if self.info.get('raid_status') == 'waiting':
            logger.info("RAID array %s not ready for mounting", self)
            return False

        if self.is_mounted:
            logger.info("%s is currently mounted, not mounting it again", self)
            return False

        logger.info("Mounting volume {0}".format(self))
        self.mount(fstype=fstype)
        self.detect_mountpoint()

        return True

    def _make_mountpoint(self,
                         casename=None,
                         var_name='mountpoint',
                         suffix='',
                         in_paths=False):
        """Creates a directory that can be used as a mountpoint. The directory is stored in :attr:`mountpoint`,
        or the varname as specified by the argument. If in_paths is True, the path is stored in the :attr:`_paths`
        attribute instead.

        :returns: the mountpoint path
        :raises NoMountpointAvailableError: if no mountpoint could be made
        """
        parser = self.disk.parser

        if parser.mountdir and not os.path.exists(parser.mountdir):
            os.makedirs(parser.mountdir)

        if parser.pretty:
            md = parser.mountdir or tempfile.gettempdir()
            case_name = casename or self.disk.parser.casename or \
                        ".".join(os.path.basename(self.disk.paths[0]).split('.')[0:-1]) or \
                        os.path.basename(self.disk.paths[0])

            if self.disk.parser.casename == case_name:  # the casename is already in the path in this case
                pretty_label = "{0}-{1}".format(
                    self.index,
                    self.get_safe_label() or self.fstype or 'volume')
            else:
                pretty_label = "{0}-{1}-{2}".format(
                    case_name, self.index,
                    self.get_safe_label() or self.fstype or 'volume')
            if suffix:
                pretty_label += "-" + suffix
            path = os.path.join(md, pretty_label)

            # check if path already exists, otherwise try to find another nice path
            if os.path.exists(path):
                for i in range(2, 100):
                    path = os.path.join(md, pretty_label + "-" + str(i))
                    if not os.path.exists(path):
                        break
                else:
                    logger.error("Could not find free mountdir.")
                    raise NoMountpointAvailableError()

            # noinspection PyBroadException
            try:
                os.mkdir(path, 777)
                if in_paths:
                    self._paths[var_name] = path
                else:
                    setattr(self, var_name, path)
                return path
            except Exception:
                logger.exception("Could not create mountdir.")
                raise NoMountpointAvailableError()
        else:
            t = tempfile.mkdtemp(prefix='im_' + self.index + '_',
                                 suffix='_' + self.get_safe_label() +
                                 ("_" + suffix if suffix else ""),
                                 dir=parser.mountdir)
            if in_paths:
                self._paths[var_name] = t
            else:
                setattr(self, var_name, t)
            return t

    def _clear_mountpoint(self):
        """Clears a created mountpoint. Does not unmount it, merely deletes it."""

        if self.mountpoint:
            os.rmdir(self.mountpoint)
            self.mountpoint = ""

    def _find_loopback(self, use_loopback=True, var_name='loopback'):
        """Finds a free loopback device that can be used. The loopback is stored in :attr:`loopback`. If *use_loopback*
        is True, the loopback will also be used directly.

        :returns: the loopback address
        :raises NoLoopbackAvailableError: if no loopback could be found
        """

        # noinspection PyBroadException
        try:
            loopback = _util.check_output_(['losetup', '-f']).strip()
            setattr(self, var_name, loopback)
        except Exception:
            logger.warning("No free loopback device found.", exc_info=True)
            raise NoLoopbackAvailableError()

        # noinspection PyBroadException
        if use_loopback:
            try:
                cmd = [
                    'losetup', '-o',
                    str(self.offset), '--sizelimit',
                    str(self.size), loopback,
                    self.get_raw_path()
                ]
                if not self.disk.read_write:
                    cmd.insert(1, '-r')
                _util.check_call_(cmd, stdout=subprocess.PIPE)
            except Exception:
                logger.exception("Loopback device could not be mounted.")
                raise NoLoopbackAvailableError()
        return loopback

    def _free_loopback(self, var_name='loopback'):
        if getattr(self, var_name):
            _util.check_call_(
                ['losetup', '-d', getattr(self, var_name)], wrap_error=True)
            setattr(self, var_name, "")

    def determine_fs_type(self):
        """Determines the FS type for this partition. This function is used internally to determine which mount system
        to use, based on the file system description. Return values include *ext*, *ufs*, *ntfs*, *lvm* and *luks*.

        Note: does not do anything if fstype is already set to something sensible.
        """

        fstype_fallback = None
        if isinstance(self.fstype, filesystems.FallbackFileSystemType):
            fstype_fallback = self.fstype.fallback
        elif isinstance(self.fstype, filesystems.FileSystemType):
            return self.fstype

        result = collections.Counter()

        for source, description in (('fsdescription',
                                     self.info.get('fsdescription')),
                                    ('guid', self.info.get('guid')),
                                    ('blikid', self._get_blkid_type),
                                    ('magic', self._get_magic_type)):
            # For efficiency reasons, not all functions are called instantly.
            if callable(description):
                description = description()

            logger.debug("Trying to determine fs type from {} '{}'".format(
                source, description))
            if not description:
                continue

            # Iterate over all results and update the certainty of all FS types
            for type in FILE_SYSTEM_TYPES.values():
                result.update(type.detect(source, description))

            # Now sort the results by their certainty
            logger.debug("Current certainty levels: {}".format(result))

            # If we have not found any candidates, we continue
            if not result:
                continue

            # If we have candidates of which we are not entirely certain, we just continue
            max_res = result.most_common(1)[0][1]
            if max_res < 50:
                logger.debug(
                    "Highest certainty item is lower than 50, continuing...")
            # If we have multiple candidates with the same score, we just continue
            elif len([
                    True for type, certainty in result.items()
                    if certainty == max_res
            ]) > 1:
                logger.debug(
                    "Multiple items with highest certainty level, so continuing..."
                )
            else:
                self.fstype = result.most_common(1)[0][0]
                return self.fstype

        # Now be more lax with the fallback:
        if result:
            max_res = result.most_common(1)[0][1]
            if max_res > 0:
                self.fstype = result.most_common(1)[0][0]
                return self.fstype
        if fstype_fallback:
            self.fstype = fstype_fallback
            return self.fstype

    def mount(self, fstype=None):
        """Based on the file system type as determined by :func:`determine_fs_type`, the proper mount command is executed
        for this volume. The volume is mounted in a temporary path (or a pretty path if :attr:`pretty` is enabled) in
        the mountpoint as specified by :attr:`mountpoint`.

        If the file system type is a LUKS container or LVM, additional methods may be called, adding subvolumes to
        :attr:`volumes`

        :raises NotMountedError: if the parent volume/disk is not mounted
        :raises NoMountpointAvailableError: if no mountpoint was found
        :raises NoLoopbackAvailableError: if no loopback device was found
        :raises UnsupportedFilesystemError: if the fstype is not supported for mounting
        :raises SubsystemError: if one of the underlying commands failed
        """

        if not self.parent.is_mounted:
            raise NotMountedError(self.parent)

        if fstype is None:
            fstype = self.determine_fs_type()
        self._load_fsstat_data()

        # Prepare mount command
        try:
            fstype.mount(self)

            self.was_mounted = True
            self.is_mounted = True
            self.fstype = fstype

        except Exception as e:
            logger.exception("Execution failed due to {} {}".format(
                type(e), e),
                             exc_info=True)
            if not isinstance(e, ImageMounterError):
                raise SubsystemError(e)
            else:
                raise

    def bindmount(self, mountpoint):
        """Bind mounts the volume to another mountpoint. Only works if the volume is already mounted.

        :raises NotMountedError: when the volume is not yet mounted
        :raises SubsystemError: when the underlying command failed
        """

        if not self.mountpoint:
            raise NotMountedError(self)
        try:
            _util.check_call_(['mount', '--bind', self.mountpoint, mountpoint],
                              stdout=subprocess.PIPE)
            if 'bindmounts' in self._paths:
                self._paths['bindmounts'].append(mountpoint)
            else:
                self._paths['bindmounts'] = [mountpoint]
            return True
        except Exception as e:
            logger.exception("Error bind mounting {0}.".format(self))
            raise SubsystemError(e)

    def get_volumes(self):
        """Recursively gets a list of all subvolumes and the current volume."""

        if self.volumes:
            volumes = []
            for v in self.volumes:
                volumes.extend(v.get_volumes())
            volumes.append(self)
            return volumes
        else:
            return [self]

    @dependencies.require(dependencies.fsstat, none_on_failure=True)
    def _load_fsstat_data(self, timeout=3):
        """Using :command:`fsstat`, adds some additional information of the volume to the Volume."""
        def stats_thread():
            try:
                cmd = [
                    'fsstat',
                    self.get_raw_path(), '-o',
                    str(self.offset // self.disk.block_size)
                ]

                # Setting the fstype explicitly makes fsstat much faster and more reliable
                # In some versions, the auto-detect yaffs2 check takes ages for large images
                fstype = {
                    "ntfs": "ntfs",
                    "fat": "fat",
                    "ext": "ext",
                    "iso": "iso9660",
                    "hfs+": "hfs",
                    "ufs": "ufs",
                    "swap": "swap",
                    "exfat": "exfat",
                }.get(self.fstype, None)
                if fstype:
                    cmd.extend(["-f", fstype])

                logger.debug('$ {0}'.format(' '.join(cmd)))
                stats_thread.process = subprocess.Popen(cmd,
                                                        stdout=subprocess.PIPE,
                                                        stderr=subprocess.PIPE)

                for line in iter(stats_thread.process.stdout.readline, b''):
                    line = line.decode('utf-8')
                    logger.debug('< {0}'.format(line))
                    if line.startswith("File System Type:"):
                        self.info['statfstype'] = line[line.index(':') +
                                                       2:].strip()
                    elif line.startswith(
                            "Last Mount Point:") or line.startswith(
                                "Last mounted on:"):
                        self.info['lastmountpoint'] = line[line.index(':') +
                                                           2:].strip().replace(
                                                               "//", "/")
                    elif line.startswith(
                            "Volume Name:") and not self.info.get('label'):
                        self.info['label'] = line[line.index(':') + 2:].strip()
                    elif line.startswith("Version:"):
                        self.info['version'] = line[line.index(':') +
                                                    2:].strip()
                    elif line.startswith("Source OS:"):
                        self.info['version'] = line[line.index(':') +
                                                    2:].strip()
                    elif 'CYLINDER GROUP INFORMATION' in line or 'BLOCK GROUP INFORMATION' in line:
                        # noinspection PyBroadException
                        try:
                            stats_thread.process.terminate()
                            logger.debug(
                                "Terminated fsstat at cylinder/block group information."
                            )
                        except Exception:
                            pass
                        break

                if self.info.get('lastmountpoint') and self.info.get('label'):
                    self.info['label'] = "{0} ({1})".format(
                        self.info['lastmountpoint'], self.info['label'])
                elif self.info.get(
                        'lastmountpoint') and not self.info.get('label'):
                    self.info['label'] = self.info['lastmountpoint']
                elif not self.info.get('lastmountpoint') and self.info.get('label') and \
                        self.info['label'].startswith("/"):  # e.g. /boot1
                    if self.info['label'].endswith("1"):
                        self.info['lastmountpoint'] = self.info['label'][:-1]
                    else:
                        self.info['lastmountpoint'] = self.info['label']

            except Exception:  # ignore any exceptions here.
                logger.exception("Error while obtaining stats.")

        stats_thread.process = None

        thread = threading.Thread(target=stats_thread)
        thread.start()
        thread.join(timeout)
        if thread.is_alive():
            # noinspection PyBroadException
            try:
                stats_thread.process.terminate()
            except Exception:
                pass
            thread.join()
            logger.debug("Killed fsstat after {0}s".format(timeout))

    def detect_mountpoint(self):
        """Attempts to detect the previous mountpoint if this was not done through :func:`load_fsstat_data`. This
        detection does some heuristic method on the mounted volume.
        """

        if self.info.get('lastmountpoint'):
            return self.info.get('lastmountpoint')
        if not self.mountpoint:
            return None

        result = None
        paths = os.listdir(self.mountpoint)
        if 'grub' in paths:
            result = '/boot'
        elif 'usr' in paths and 'var' in paths and 'root' in paths:
            result = '/'
        elif 'bin' in paths and 'lib' in paths and 'local' in paths and 'src' in paths and 'usr' not in paths:
            result = '/usr'
        elif 'bin' in paths and 'lib' in paths and 'local' not in paths and 'src' in paths and 'usr' not in paths:
            result = '/usr/local'
        elif 'lib' in paths and 'local' in paths and 'tmp' in paths and 'var' not in paths:
            result = '/var'
        # elif sum(['bin' in paths, 'boot' in paths, 'cdrom' in paths, 'dev' in paths, 'etc' in paths, 'home' in paths,
        #          'lib' in paths, 'lib64' in paths, 'media' in paths, 'mnt' in paths, 'opt' in paths,
        #          'proc' in paths, 'root' in paths, 'sbin' in paths, 'srv' in paths, 'sys' in paths, 'tmp' in paths,
        #          'usr' in paths, 'var' in paths]) > 11:
        #    result = '/'

        if result:
            self.info['lastmountpoint'] = result
            if not self.info.get('label'):
                self.info['label'] = self.info['lastmountpoint']
            logger.info(
                "Detected mountpoint as {0} based on files in volume".format(
                    self.info['lastmountpoint']))

        return result

    # noinspection PyBroadException
    def unmount(self, allow_lazy=False):
        """Unounts the volume from the filesystem.

        :raises SubsystemError: if one of the underlying processes fails
        :raises CleanupError: if the cleanup fails
        """

        for volume in self.volumes:
            try:
                volume.unmount(allow_lazy=allow_lazy)
            except ImageMounterError:
                pass

        if self.is_mounted:
            logger.info("Unmounting volume %s", self)

        if self.loopback and self.info.get('volume_group'):
            _util.check_call_(
                ["lvm", 'vgchange', '-a', 'n', self.info['volume_group']],
                wrap_error=True,
                stdout=subprocess.PIPE)
            self.info['volume_group'] = ""

        if self.loopback and self._paths.get('luks'):
            _util.check_call_(['cryptsetup', 'luksClose', self._paths['luks']],
                              wrap_error=True,
                              stdout=subprocess.PIPE)
            del self._paths['luks']

        if self._paths.get('bde'):
            try:
                _util.clean_unmount(['fusermount', '-u'], self._paths['bde'])
            except SubsystemError:
                if not allow_lazy:
                    raise
                _util.clean_unmount(['fusermount', '-uz'], self._paths['bde'])
            del self._paths['bde']

        if self._paths.get('md'):
            md_path = self._paths['md']
            del self._paths[
                'md']  # removing it here to ensure we do not enter an infinite loop, will add it back later

            # MD arrays are a bit complicated, we also check all other volumes that are part of this array and
            # unmount them as well.
            logger.debug(
                "All other volumes that use %s as well will also be unmounted",
                md_path)
            for v in self.disk.get_volumes():
                if v != self and v._paths.get('md') == md_path:
                    v.unmount(allow_lazy=allow_lazy)

            try:
                _util.check_output_(["mdadm", '--stop', md_path],
                                    stderr=subprocess.STDOUT)
            except Exception as e:
                self._paths['md'] = md_path
                raise SubsystemError(e)

        if self._paths.get('vss'):
            try:
                _util.clean_unmount(['fusermount', '-u'], self._paths['vss'])
            except SubsystemError:
                if not allow_lazy:
                    raise
                _util.clean_unmount(['fusermount', '-uz'], self._paths['vss'])
            del self._paths['vss']

        if self.loopback:
            _util.check_call_(['losetup', '-d', self.loopback],
                              wrap_error=True)
            self.loopback = ""

        if self._paths.get('bindmounts'):
            for mp in self._paths['bindmounts']:
                _util.clean_unmount(['umount'], mp, rmdir=False)
            del self._paths['bindmounts']

        if self.mountpoint:
            _util.clean_unmount(['umount'], self.mountpoint)
            self.mountpoint = ""

        if self._paths.get('carve'):
            try:
                shutil.rmtree(self._paths['carve'])
            except OSError as e:
                raise SubsystemError(e)
            else:
                del self._paths['carve']

        self.is_mounted = False
Ejemplo n.º 7
0
class Volume(object):
    """Information about a volume. Note that every detected volume gets their own Volume object, though it may or may
    not be mounted. This can be seen through the :attr:`mountpoint` attribute -- if it is not set, perhaps the
    :attr:`exception` attribute is set with an exception.
    """

    def __init__(self, disk, parent=None, index="0", size=0, offset=0, flag='alloc', slot=0, fstype="", key="",
                 vstype='', volume_detector='auto'):
        """Creates a Volume object that is not mounted yet.

        Only use arguments as keyword arguments.

        :param disk: the parent disk
        :type disk: :class:`Disk`
        :param str fstype: the fstype you wish to use for this Volume. May be ?<fstype> as a fallback value. If not
                           specified, will be retrieved from the ImageParser instance instead.
        :param str key: the key to use for this Volume.
        :param str vstype: the volume system type to use.
        :param str volume_detector: the volume system detection method to use
        """

        self.parent = parent
        self.disk = disk

        # Should be filled somewhere
        self.size = size
        self.offset = offset
        self.index = index
        self.slot = slot
        self.flag = flag
        self.block_size = self.disk.block_size

        self.volumes = VolumeSystem(parent=self, vstype=vstype, volume_detector=volume_detector)

        self.fstype = fstype
        self._get_fstype_from_parser(fstype)

        if key:
            self.key = key
        elif self.index in self.disk.parser.keys:
            self.key = self.disk.parser.keys[self.index]
        else:
            self.key = ""

        self.info = {}
        self._paths = {}

        self.mountpoint = ""
        self.loopback = ""
        self.was_mounted = False
        self.is_mounted = False

    def __unicode__(self):
        return '{0}:{1}'.format(self.index, self.info.get('fsdescription') or '-')

    def __str__(self):
        return str(self.__unicode__())

    def __getitem__(self, item):
        return self.volumes[item]

    def _get_fstype_from_parser(self, fstype=None):
        """Load fstype information from the parser instance."""
        if fstype:
            self.fstype = fstype
        elif self.index in self.disk.parser.fstypes:
            self.fstype = self.disk.parser.fstypes[self.index]
        elif '*' in self.disk.parser.fstypes:
            self.fstype = self.disk.parser.fstypes['*']
        elif '?' in self.disk.parser.fstypes and self.disk.parser.fstypes['?'] is not None:
            self.fstype = "?" + self.disk.parser.fstypes['?']
        else:
            self.fstype = ""

        if self.fstype in VOLUME_SYSTEM_TYPES:
            self.volumes.vstype = self.fstype
            self.fstype = 'volumesystem'

    def get_description(self, with_size=True, with_index=True):
        """Obtains a generic description of the volume, containing the file system type, index, label and NTFS version.
        If *with_size* is provided, the volume size is also included.
        """

        desc = ''

        if with_size and self.size:
            desc += '{0} '.format(self.get_formatted_size())

        s = self.info.get('statfstype') or self.info.get('fsdescription') or '-'
        if with_index:
            desc += '{1}:{0}'.format(s, self.index)
        else:
            desc += s

        if self.info.get('label'):
            desc += ' {0}'.format(self.info.get('label'))

        if self.info.get('version'):  # NTFS
            desc += ' [{0}]'.format(self.info.get('version'))

        return desc

    def get_formatted_size(self):
        """Obtains the size of the volume in a human-readable format (i.e. in TiBs, GiBs or MiBs)."""

        if self.size is not None:
            if self.size < 1024:
                return "{0} B".format(self.size)
            elif self.size < 1024 ** 2:
                return "{0} KiB".format(round(self.size / 1024, 2))
            elif self.size < 1024 ** 3:
                return "{0} MiB".format(round(self.size / 1024.0 ** 2, 2))
            elif self.size < 1024 ** 4:
                return "{0} GiB".format(round(self.size / 1024.0 ** 3, 2))
            else:
                return "{0} TiB".format(round(self.size / 1024.0 ** 4, 2))
        else:
            return self.size

    def _get_blkid_type(self):
        """Retrieves the FS type from the blkid command."""
        try:
            result = _util.check_output_(['blkid', '-p', '-O', str(self.offset), self.get_raw_path()])
            if not result:
                return None

            # noinspection PyTypeChecker
            blkid_result = dict(re.findall(r'([A-Z]+)="(.+?)"', result))

            self.info['blkid_data'] = blkid_result

            if 'PTTYPE' in blkid_result and 'TYPE' not in blkid_result:
                return blkid_result.get('PTTYPE')
            else:
                return blkid_result.get('TYPE')

        except Exception:
            return None  # returning None is better here, since we do not care about the exception in determine_fs_type

    def _get_magic_type(self):
        """Checks the volume for its magic bytes and returns the magic."""

        with io.open(self.disk.get_fs_path(), "rb") as file:
            file.seek(self.offset)
            fheader = file.read(min(self.size, 4096) if self.size else 4096)

        # TODO fallback to img-cat image -s blocknum | file -
        # if we were able to load the module magic
        try:
            # noinspection PyUnresolvedReferences
            import magic

            if hasattr(magic, 'from_buffer'):
                # using https://github.com/ahupp/python-magic
                logger.debug("Using python-magic Python package for file type magic")
                result = magic.from_buffer(fheader).decode()
                self.info['magic_data'] = result
                return result

            elif hasattr(magic, 'open'):
                # using Magic file extensions by Rueben Thomas (Ubuntu python-magic module)
                logger.debug("Using python-magic system package for file type magic")
                ms = magic.open(magic.NONE)
                ms.load()
                result = ms.buffer(fheader)
                ms.close()
                self.info['magic_data'] = result
                return result

            else:
                logger.warning("The python-magic module is not available, but another module named magic was found.")

        except ImportError:
            logger.warning("The python-magic module is not available.")
        except AttributeError:
            logger.warning("The python-magic module is not available, but another module named magic was found.")
        return None  # returning None is better here, since we do not care about the exception in determine_fs_type

    def get_raw_path(self):
        """Retrieves the base mount path of the volume. Typically equals to :func:`Disk.get_fs_path` but may also be the
        path to a logical volume. This is used to determine the source path for a mount call.
        """

        v = self
        while True:
            if v._paths.get('lv'):
                return v._paths['lv']
            elif v._paths.get('bde'):
                return v._paths['bde'] + '/bde1'
            elif v._paths.get('luks'):
                return '/dev/mapper/' + v._paths['luks']
            elif v._paths.get('md'):
                return v._paths['md']

            # Only if the volume has a parent that is not a disk, we try to check the parent for a location.
            if v.parent and v.parent != self.disk:
                v = v.parent
            else:
                break
        return self.disk.get_fs_path()

    def get_safe_label(self):
        """Returns a label that is safe to add to a path in the mountpoint for this volume."""

        if self.info.get('label') == '/':
            return 'root'

        suffix = re.sub(r"[/ \(\)]+", "_", self.info.get('label')) if self.info.get('label') else ""
        if suffix and suffix[0] == '_':
            suffix = suffix[1:]
        if len(suffix) > 2 and suffix[-1] == '_':
            suffix = suffix[:-1]
        return suffix

    def carve(self, freespace=True):
        """Call this method to carve the free space of the volume for (deleted) files. Note that photorec has its
        own interface that temporarily takes over the shell.

        :param freespace: indicates whether the entire volume should be carved (False) or only the free space (True)
        :type freespace: bool
        :return: string to the path where carved data is available
        :raises CommandNotFoundError: if the underlying command does not exist
        :raises SubsystemError: if the underlying command fails
        :raises NoMountpointAvailableError: if there is no mountpoint available
        :raises NoLoopbackAvailableError: if there is no loopback available (only when volume has no slot number)
        """

        if not _util.command_exists('photorec'):
            logger.warning("photorec is not installed, could not carve volume")
            raise CommandNotFoundError("photorec")

        self._make_mountpoint(var_name='carve', suffix="carve", in_paths=True)

        # if no slot, we need to make a loopback that we can use to carve the volume
        loopback_was_created_for_carving = False
        if not self.slot:
            if not self.loopback:
                self._find_loopback()
                #Can't carve if volume has no slot number and can't be mounted on loopback.
                loopback_was_created_for_carving = True

            # noinspection PyBroadException
            try:
                _util.check_call_(["photorec", "/d", self._paths['carve'] + os.sep, "/cmd", self.loopback,
                                  ("freespace," if freespace else "") + "search"])

                # clean out the loop device if we created it specifically for carving
                if loopback_was_created_for_carving:
                    # noinspection PyBroadException
                    try:
                        _util.check_call_(['losetup', '-d', self.loopback])
                    except Exception:
                        pass
                    else:
                        self.loopback = ""

                return self._paths['carve']
            except Exception as e:
                logger.exception("Failed carving the volume.")
                raise SubsystemError(e)
        else:
            # noinspection PyBroadException
            try:
                _util.check_call_(["photorec", "/d", self._paths['carve'] + os.sep, "/cmd", self.get_raw_path(),
                                  str(self.slot) + (",freespace" if freespace else "") + ",search"])
                return self._paths['carve']

            except Exception as e:
                logger.exception("Failed carving the volume.")
                raise SubsystemError(e)

    def vshadowmount(self):
        """Method to call vshadowmount and mount NTFS volume shadow copies.

        :return: string representing the path to the volume shadow copies
        :raises CommandNotFoundError: if the underlying command does not exist
        :raises SubSystemError: if the underlying command fails
        :raises NoMountpointAvailableError: if there is no mountpoint available
        """

        if not _util.command_exists('vshadowmount'):
            logger.warning("vshadowmount is not installed, could not mount volume shadow copies")
            raise CommandNotFoundError('vshadowmount')

        self._make_mountpoint(var_name='vss', suffix="vss", in_paths=True)

        try:
            _util.check_call_(["vshadowmount", "-o", str(self.offset), self.get_raw_path(), self._paths['vss']])
            return self._paths['vss']
        except Exception as e:
            logger.exception("Failed mounting the volume shadow copies.")
            raise SubsystemError(e)

    def _should_mount(self, only_mount=None):
        """Indicates whether this volume should be mounted. Internal method, used by imount.py"""

        return only_mount is None or \
            self.index in only_mount or \
            self.info.get('lastmountpoint') in only_mount or \
            self.info.get('label') in only_mount

    def init(self, only_mount=None, swallow_exceptions=True):
        """Generator that mounts this volume and either yields itself or recursively generates its subvolumes.

        More specifically, this function will call :func:`load_fsstat_data` (iff *no_stats* is False), followed by
        :func:`mount`, followed by a call to :func:`detect_mountpoint`, after which ``self`` is yielded, or the result
        of the :func:`init` call on each subvolume is yielded

        :param only_mount: if specified, only volume indexes in this list are mounted. Volume indexes are strings.
        :param swallow_exceptions: if True, any error occuring when mounting the volume is swallowed and added as an
            exception attribute to the yielded objects.
        """
        if swallow_exceptions:
            self.exception = None

        try:
            if not self._should_mount(only_mount):
                yield self
                return

            if not self.init_volume():
                yield self
                return

        except ImageMounterError as e:
            if swallow_exceptions:
                self.exception = e
            else:
                raise

        if not self.volumes:
            yield self
        else:
            for v in self.volumes:
                for s in v.init(only_mount, swallow_exceptions):
                    yield s

    def init_volume(self):
        """Initializes a single volume. You should use this method instead of :func:`mount` if you want some sane checks
        before mounting.
        """

        logger.debug("Initializing volume {0}".format(self))

        if not self._should_mount():
            return False

        if self.flag != 'alloc':
            return False

        if self.info.get('raid_status') == 'waiting':
            logger.info("RAID array %s not ready for mounting", self)
            return False

        if self.is_mounted:
            logger.info("%s is currently mounted, not mounting it again", self)
            return False

        logger.info("Mounting volume {0}".format(self))
        self.mount()
        self.detect_mountpoint()

        return True

    def _make_mountpoint(self, casename=None, var_name='mountpoint', suffix='', in_paths=False):
        """Creates a directory that can be used as a mountpoint. The directory is stored in :attr:`mountpoint`,
        or the varname as specified by the argument. If in_paths is True, the path is stored in the :attr:`_paths`
        attribute instead.

        :returns: the mountpoint path
        :raises NoMountpointAvailableError: if no mountpoint could be made
        """
        parser = self.disk.parser

        if parser.mountdir and not os.path.exists(parser.mountdir):
            os.makedirs(parser.mountdir)

        if parser.pretty:
            md = parser.mountdir or tempfile.gettempdir()
            case_name = casename or self.disk.parser.casename or \
                        ".".join(os.path.basename(self.disk.paths[0]).split('.')[0:-1]) or \
                        os.path.basename(self.disk.paths[0])
            if self.disk.parser.casename == case_name:  # the casename is already in the path in this case
                pretty_label = "{0}-{1}".format(self.index, self.get_safe_label() or self.fstype or 'volume')
            else:
                pretty_label = "{0}-{1}-{2}".format(case_name, self.index,
                                                    self.get_safe_label() or self.fstype or 'volume')
            if suffix:
                pretty_label += "-" + suffix
            path = os.path.join(md, pretty_label)

            # check if path already exists, otherwise try to find another nice path
            if os.path.exists(path):
                for i in range(2, 100):
                    path = os.path.join(md, pretty_label + "-" + str(i))
                    if not os.path.exists(path):
                        break
                else:
                    logger.error("Could not find free mountdir.")
                    raise NoMountpointAvailableError()

            # noinspection PyBroadException
            try:
                os.mkdir(path, 777)
                if in_paths:
                    self._paths[var_name] = path
                else:
                    setattr(self, var_name, path)
                return path
            except Exception:
                logger.exception("Could not create mountdir.")
                raise NoMountpointAvailableError()
        else:
            t = tempfile.mkdtemp(prefix='im_' + self.index + '_',
                                 suffix='_' + self.get_safe_label() + ("_" + suffix if suffix else ""),
                                 dir=parser.mountdir)
            if in_paths:
                self._paths[var_name] = t
            else:
                setattr(self, var_name, t)
            return t

    def _find_loopback(self, use_loopback=True, var_name='loopback'):
        """Finds a free loopback device that can be used. The loopback is stored in :attr:`loopback`. If *use_loopback*
        is True, the loopback will also be used directly.

        :returns: the loopback address
        :raises NoLoopbackAvailableError: if no loopback could be found
        """

        # noinspection PyBroadException
        try:
            loopback = _util.check_output_(['losetup', '-f']).strip()
            setattr(self, var_name, loopback)
        except Exception:
            logger.warning("No free loopback device found.", exc_info=True)
            raise NoLoopbackAvailableError()

        # noinspection PyBroadException
        if use_loopback:
            try:
                cmd = ['losetup', '-o', str(self.offset), '--sizelimit', str(self.size),
                       loopback, self.get_raw_path()]
                if not self.disk.read_write:
                    cmd.insert(1, '-r')
                _util.check_call_(cmd, stdout=subprocess.PIPE)
            except Exception:
                logger.exception("Loopback device could not be mounted.")
                raise NoLoopbackAvailableError()
        return loopback

    def _free_loopback(self, var_name='loopback'):
        if getattr(self, var_name):
            _util.check_call_(['losetup', '-d', getattr(self, var_name)], wrap_error=True)
            setattr(self, var_name, "")

    def determine_fs_type(self):
        """Determines the FS type for this partition. This function is used internally to determine which mount system
        to use, based on the file system description. Return values include *ext*, *ufs*, *ntfs*, *lvm* and *luks*.

        Note: does not do anything if fstype is already set to something sensible.
        """

        fstype_fallback = self.fstype[1:] if self.fstype and self.fstype.startswith("?") else ""

        # Determine fs type. If forced, always use provided type.
        if self.fstype in FILE_SYSTEM_TYPES:
            pass  # already correctly set
        elif self.fstype in VOLUME_SYSTEM_TYPES:
            self.volumes.vstype = self.fstype
            self.fstype = 'volumesystem'
        else:
            last_resort = None  # use this if we can't determine the FS type more reliably
            # we have two possible sources for determining the FS type: the description given to us by the detection
            # method, and the type given to us by the stat function
            for fsdesc in (self.info.get('fsdescription'), self.info.get('guid'),
                           self._get_blkid_type, self.info.get('statfstype'), self._get_magic_type):
                # For efficiency reasons, not all functions are called instantly.
                if callable(fsdesc):
                    fsdesc = fsdesc()
                logger.debug("Trying to determine fs type from '{}'".format(fsdesc))
                if not fsdesc:
                    continue
                fsdesc = str(fsdesc.lower())

                # for the purposes of this function, logical volume is nothing, and 'primary' is rather useless info
                if fsdesc in ('logical volume', 'luks volume', 'bde volume', 'raid volume',
                              'primary', 'basic data partition'):
                    continue

                if fsdesc == 'directory':
                    self.fstype = 'dir'  # dummy fs type
                elif re.search(r'\bext[0-9]*\b', fsdesc):
                    self.fstype = 'ext'
                elif 'bsd' in fsdesc:
                    self.fstype = 'ufs'
                elif '0x07' in fsdesc or 'ntfs' in fsdesc:
                    self.fstype = 'ntfs'
                elif '0x8e' in fsdesc or 'lvm' in fsdesc:
                    self.fstype = 'lvm'
                elif 'hfs+' in fsdesc:
                    self.fstype = 'hfs+'
                elif 'hfs' in fsdesc:
                    self.fstype = 'hfs'
                elif 'luks' in fsdesc:
                    self.fstype = 'luks'
                elif 'fat' in fsdesc or 'efi system partition' in fsdesc:
                    # based on http://en.wikipedia.org/wiki/EFI_System_partition, efi is always fat.
                    self.fstype = 'fat'
                elif 'iso 9660' in fsdesc:
                    self.fstype = 'iso'
                elif 'linux compressed rom file system' in fsdesc or 'cramfs' in fsdesc:
                    self.fstype = 'cramfs'
                elif fsdesc.startswith("sgi xfs") or re.search(r'\bxfs\b', fsdesc):
                    self.fstype = "xfs"
                elif 'swap file' in fsdesc or 'linux swap' in fsdesc or 'linux-swap' in fsdesc:
                    self.fstype = 'swap'
                elif 'squashfs' in fsdesc:
                    self.fstype = 'squashfs'
                elif "jffs2" in fsdesc:
                    self.fstype = 'jffs2'
                elif "minix filesystem" in fsdesc:
                    self.fstype = 'minix'
                elif fsdesc == 'dos':
                    self.fstype = 'volumesystem'
                    self.volumes.vstype = 'dos'
                elif "dos/mbr boot sector" in fsdesc:
                    self.fstype = 'volumesystem'
                    self.volumes.vstype = 'detect'
                elif 'linux_raid_member' in fsdesc or 'linux software raid' in fsdesc:
                    self.fstype = 'raid'
                elif fsdesc.upper() in FILE_SYSTEM_GUIDS:
                    # this is a bit of a workaround for the fill_guid method
                    self.fstype = FILE_SYSTEM_GUIDS[fsdesc.upper()]
                elif '0x83' in fsdesc:
                    # this is a linux mount, but we can't figure out which one.
                    # we hand it off to the OS, maybe it can try something.
                    # if we use last_resort for more enhanced stuff, we may need to check if we are not setting
                    # it to something less specific here
                    last_resort = 'unknown'
                    continue
                else:
                    continue  # this loop failed

                logger.info("Detected {0} as {1}".format(fsdesc, self.fstype))
                break  # we found something
            else:  # we found nothing
                # if last_resort is something more sensible than unknown, we use that
                # if we have specified a fsfallback which is not set to None, we use that
                # if last_resort is unknown or the fallback is not None, we use unknown
                if last_resort and last_resort != 'unknown':
                    self.fstype = last_resort
                elif fstype_fallback:
                    self.fstype = fstype_fallback
                elif last_resort == 'unknown' or not fstype_fallback:
                    self.fstype = 'unknown'

        return self.fstype

    def mount(self):
        """Based on the file system type as determined by :func:`determine_fs_type`, the proper mount command is executed
        for this volume. The volume is mounted in a temporary path (or a pretty path if :attr:`pretty` is enabled) in
        the mountpoint as specified by :attr:`mountpoint`.

        If the file system type is a LUKS container or LVM, additional methods may be called, adding subvolumes to
        :attr:`volumes`

        :raises NoMountpointAvailableError: if no mountpoint was found
        :raises NoLoopbackAvailableError: if no loopback device was found
        :raises UnsupportedFilesystemError: if the fstype is not supported for mounting
        :raises SubsystemError: if one of the underlying commands failed
        """

        raw_path = self.get_raw_path()
        self.determine_fs_type()
        self._load_fsstat_data()

        # we need a mountpoint if it is not a lvm or luks volume
        if self.fstype not in ('luks', 'lvm', 'bde', 'raid', 'volumesystem') and \
                self.fstype in FILE_SYSTEM_TYPES:
            self._make_mountpoint()

        # Prepare mount command
        try:
            def call_mount(type, opts):
                cmd = ['mount', raw_path, self.mountpoint, '-t', type, '-o', opts]
                if not self.disk.read_write:
                    cmd[-1] += ',ro'

                _util.check_output_(cmd, stderr=subprocess.STDOUT)

            if self.fstype == 'ext':
                call_mount('ext4', 'noexec,noload,loop,offset=' + str(self.offset))

            elif self.fstype == 'ufs':
                call_mount('ufs', 'ufstype=ufs2,loop,offset=' + str(self.offset))

            elif self.fstype == 'ntfs':
                call_mount('ntfs', 'show_sys_files,noexec,force,loop,offset=' + str(self.offset))

            elif self.fstype == 'xfs':
                call_mount('xfs', 'norecovery,loop,offset=' + str(self.offset))

            elif self.fstype == 'hfs+':
                call_mount('hfsplus', 'force,loop,offset=' + str(self.offset) + ',sizelimit=' + str(self.size))

            elif self.fstype in ('iso', 'udf', 'squashfs', 'cramfs', 'minix', 'fat', 'hfs'):
                mnt_type = {'iso': 'iso9660', 'fat': 'vfat'}.get(self.fstype, self.fstype)
                call_mount(mnt_type, 'loop,offset=' + str(self.offset))

            elif self.fstype == 'vmfs':
                self._find_loopback()
                _util.check_call_(['vmfs-fuse', self.loopback, self.mountpoint], stdout=subprocess.PIPE)

            elif self.fstype == 'unknown':  # mounts without specifying the filesystem type
                cmd = ['mount', raw_path, self.mountpoint, '-o', 'loop,offset=' + str(self.offset)]
                if not self.disk.read_write:
                    cmd[-1] += ',ro'

                _util.check_call_(cmd, stdout=subprocess.PIPE)

            elif self.fstype == 'jffs2':
                self._open_jffs2()

            elif self.fstype == 'luks':
                self._open_luks_container()

            elif self.fstype == 'bde':
                self._open_bde_container()

            elif self.fstype == 'lvm':
                self._open_lvm()
                self.volumes.vstype = 'lvm'
                for _ in self.volumes.detect_volumes('lvm'):
                    pass

            elif self.fstype == 'raid':
                self._open_raid_volume()

            elif self.fstype == 'dir':
                os.rmdir(self.mountpoint)
                os.symlink(raw_path, self.mountpoint)

            elif self.fstype == 'volumesystem':
                for _ in self.volumes.detect_volumes():
                    pass

            else:
                try:
                    size = self.size / self.disk.block_size
                except TypeError:
                    size = self.size

                logger.warning("Unsupported filesystem {0} (type: {1}, block offset: {2}, length: {3})"
                               .format(self, self.fstype, self.offset / self.disk.block_size, size))
                raise UnsupportedFilesystemError(self.fstype)

            self.was_mounted = True
            self.is_mounted = True
        except Exception as e:
            logger.exception("Execution failed due to {} {}".format(type(e), e), exc_info=True)
            try:
                if self.mountpoint:
                    os.rmdir(self.mountpoint)
                    self.mountpoint = ""
                if self.loopback:
                    self.loopback = ""
            except Exception as e2:
                logger.exception("Clean-up failed", exc_info=True)

            if not isinstance(e, ImageMounterError):
                raise SubsystemError(e)
            else:
                raise

    def bindmount(self, mountpoint):
        """Bind mounts the volume to another mountpoint. Only works if the volume is already mounted.

        :raises NotMountedError: when the volume is not yet mounted
        :raises SubsystemError: when the underlying command failed
        """

        if not self.mountpoint:
            raise NotMountedError()
        try:
            _util.check_call_(['mount', '--bind', self.mountpoint, mountpoint], stdout=subprocess.PIPE)
            if 'bindmounts' in self._paths:
                self._paths['bindmounts'].append(mountpoint)
            else:
                self._paths['bindmounts'] = [mountpoint]
            return True
        except Exception as e:
            logger.exception("Error bind mounting {0}.".format(self))
            raise SubsystemError(e)

    def _open_luks_container(self):
        """Command that is an alternative to the :func:`mount` command that opens a LUKS container. The opened volume is
        added to the subvolume set of this volume. Requires the user to enter the key manually.

        TODO: add support for :attr:`keys`

        :return: the Volume contained in the LUKS container, or None on failure.
        :raises NoLoopbackAvailableError: when no free loopback could be found
        :raises IncorrectFilesystemError: when this is not a LUKS volume
        :raises SubsystemError: when the underlying command fails
        """

        # Open a loopback device
        self._find_loopback()

        # Check if this is a LUKS device
        # noinspection PyBroadException
        try:
            _util.check_call_(["cryptsetup", "isLuks", self.loopback], stderr=subprocess.STDOUT)
            # ret = 0 if isLuks
        except Exception:
            logger.warning("Not a LUKS volume")
            # clean the loopback device, we want this method to be clean as possible
            # noinspection PyBroadException
            try:
                self._free_loopback()
            except Exception:
                pass
            raise IncorrectFilesystemError()

        try:
            extra_args = []
            key = None
            if self.key:
                t, v = self.key.split(':', 1)
                if t == 'p':  # passphrase
                    key = v
                elif t == 'f':  # key-file
                    extra_args = ['--key-file', v]
                elif t == 'm':  # master-key-file
                    extra_args = ['--master-key-file', v]
            else:
                logger.warning("No key material provided for %s", self)
        except ValueError:
            logger.exception("Invalid key material provided (%s) for %s. Expecting [arg]:[value]", self.key, self)
            self._free_loopback()
            raise ArgumentError()

        # Open the LUKS container
        self._paths['luks'] = 'image_mounter_luks_' + str(random.randint(10000, 99999))

        # noinspection PyBroadException
        try:
            cmd = ["cryptsetup", "luksOpen", self.loopback, self._paths['luks']]
            cmd.extend(extra_args)
            if not self.disk.read_write:
                cmd.insert(1, '-r')

            if key is not None:
                logger.debug('$ {0}'.format(' '.join(cmd)))
                # for py 3.2+, we could have used input=, but that doesn't exist in py2.7.
                p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                p.communicate(key)
                p.wait()
                retcode = p.poll()
                if retcode:
                    raise KeyInvalidError()
            else:
                _util.check_call_(cmd)
        except ImageMounterError:
            del self._paths['luks']
            self._free_loopback()
            raise
        except Exception as e:
            del self._paths['luks']
            self._free_loopback()
            raise SubsystemError(e)

        size = None
        # noinspection PyBroadException
        try:
            result = _util.check_output_(["cryptsetup", "status", self._paths['luks']])
            for l in result.splitlines():
                if "size:" in l and "key" not in l:
                    size = int(l.replace("size:", "").replace("sectors", "").strip()) * self.disk.block_size
        except Exception:
            pass

        container = self.volumes._make_single_subvolume(flag='alloc', offset=0, size=size)
        container.info['fsdescription'] = 'LUKS Volume'

        return container

    def _open_bde_container(self):
        """Mounts a BDE container. Uses key material provided by the :attr:`keys` attribute. The key material should be
        provided in the same format as to :cmd:`bdemount`, used as follows:

        k:full volume encryption and tweak key
        p:passphrase
        r:recovery password
        s:file to startup key (.bek)

        :return: the Volume contained in the BDE container
        :raises ArgumentError: if the keys argument is invalid
        :raises SubsystemError: when the underlying command fails
        """

        self._paths['bde'] = tempfile.mkdtemp(prefix='image_mounter_bde_')

        try:
            if self.key:
                t, v = self.key.split(':', 1)
                key = ['-' + t, v]
            else:
                logger.warning("No key material provided for %s", self)
                key = []
        except ValueError:
            logger.exception("Invalid key material provided (%s) for %s. Expecting [arg]:[value]", self.key, self)
            raise ArgumentError()

        # noinspection PyBroadException
        try:
            cmd = ["bdemount", self.get_raw_path(), self._paths['bde'], '-o', str(self.offset)]
            cmd.extend(key)
            _util.check_call_(cmd)
        except Exception as e:
            del self._paths['bde']
            logger.exception("Failed mounting BDE volume %s.", self)
            raise SubsystemError(e)

        container = self.volumes._make_single_subvolume(flag='alloc', offset=0, size=self.size)
        container.info['fsdescription'] = 'BDE Volume'

        return container

    def _open_jffs2(self):
        """Perform specific operations to mount a JFFS2 image. This kind of image is sometimes used for things like
        bios images. so external tools are required but given this method you don't have to memorize anything and it
        works fast and easy.

        Note that this module might not yet work while mounting multiple images at the same time.
        """
        # we have to make a ram-device to store the image, we keep 20% overhead
        size_in_kb = int((self.size / 1024) * 1.2)
        _util.check_call_(['modprobe', '-v', 'mtd'])
        _util.check_call_(['modprobe', '-v', 'jffs2'])
        _util.check_call_(['modprobe', '-v', 'mtdram', 'total_size={}'.format(size_in_kb), 'erase_size=256'])
        _util.check_call_(['modprobe', '-v', 'mtdblock'])
        _util.check_call_(['dd', 'if=' + self.get_raw_path(), 'of=/dev/mtd0'])
        _util.check_call_(['mount', '-t', 'jffs2', '/dev/mtdblock0', self.mountpoint])

    def _open_lvm(self):
        """Performs mount actions on a LVM. Scans for active volume groups from the loopback device, activates it
        and fills :attr:`volumes` with the logical volumes.

        :raises NoLoopbackAvailableError: when no loopback was available
        :raises IncorrectFilesystemError: when the volume is not a volume group
        """
        os.environ['LVM_SUPPRESS_FD_WARNINGS'] = '1'

        # find free loopback device
        self._find_loopback()
        time.sleep(0.2)

        # Scan for new lvm volumes
        result = _util.check_output_(["lvm", "pvscan"])
        for l in result.splitlines():
            if self.loopback in l or (self.offset == 0 and self.get_raw_path() in l):
                for vg in re.findall(r'VG (\S+)', l):
                    self.info['volume_group'] = vg

        if not self.info.get('volume_group'):
            logger.warning("Volume is not a volume group. (Searching for %s)", self.loopback)
            raise IncorrectFilesystemError()

        # Enable lvm volumes
        _util.check_call_(["lvm", "vgchange", "-a", "y", self.info['volume_group']], stdout=subprocess.PIPE)

    def _open_raid_volume(self):
        """Add the volume to a RAID system. The RAID array is activated as soon as the array can be activated.

        :raises NoLoopbackAvailableError: if no loopback device was found
        """

        self._find_loopback()

        raid_status = None
        try:
            # use mdadm to mount the loopback to a md device
            # incremental and run as soon as available
            output = _util.check_output_(['mdadm', '-IR', self.loopback], stderr=subprocess.STDOUT)

            match = re.findall(r"attached to ([^ ,]+)", output)
            if match:
                self._paths['md'] = os.path.realpath(match[0])
                if 'which is already active' in output:
                    logger.info("RAID is already active in other volume, using %s", self._paths['md'])
                    raid_status = 'active'
                elif 'not enough to start' in output:
                    self._paths['md'] = self._paths['md'].replace("/dev/md/", "/dev/md")
                    logger.info("RAID volume added, but not enough to start %s", self._paths['md'])
                    raid_status = 'waiting'
                else:
                    logger.info("RAID started at {0}".format(self._paths['md']))
                    raid_status = 'active'
        except Exception as e:
            logger.exception("Failed mounting RAID.")
            raise SubsystemError(e)

        # search for the RAID volume
        for v in self.disk.parser.get_volumes():
            if v._paths.get("md") == self._paths['md'] and v.volumes:
                logger.debug("Adding existing volume %s to volume %s", v.volumes[0], self)
                v.volumes[0].info['raid_status'] = raid_status
                self.volumes.volumes.append(v.volumes[0])
                return v.volumes[0]
        else:
            logger.debug("Creating RAID volume for %s", self)
            container = self.volumes._make_single_subvolume(flag='alloc', offset=0, size=self.size)
            container.info['fsdescription'] = 'RAID Volume'
            container.info['raid_status'] = raid_status
            return container

    def get_volumes(self):
        """Recursively gets a list of all subvolumes and the current volume."""

        if self.volumes:
            volumes = []
            for v in self.volumes:
                volumes.extend(v.get_volumes())
            volumes.append(self)
            return volumes
        else:
            return [self]

    def _load_fsstat_data(self):
        """Using :command:`fsstat`, adds some additional information of the volume to the Volume."""

        if not _util.command_exists('fsstat'):
            logger.warning("fsstat is not installed, could not mount volume shadow copies")
            return

        process = None

        def stats_thread():
            try:
                cmd = ['fsstat', self.get_raw_path(), '-o', str(self.offset // self.disk.block_size)]
                logger.debug('$ {0}'.format(' '.join(cmd)))
                # noinspection PyShadowingNames
                process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

                for line in iter(process.stdout.readline, b''):
                    line = line.decode()
                    if line.startswith("File System Type:"):
                        self.info['statfstype'] = line[line.index(':') + 2:].strip()
                    elif line.startswith("Last Mount Point:") or line.startswith("Last mounted on:"):
                        self.info['lastmountpoint'] = line[line.index(':') + 2:].strip().replace("//", "/")
                    elif line.startswith("Volume Name:") and not self.info.get('label'):
                        self.info['label'] = line[line.index(':') + 2:].strip()
                    elif line.startswith("Version:"):
                        self.info['version'] = line[line.index(':') + 2:].strip()
                    elif line.startswith("Source OS:"):
                        self.info['version'] = line[line.index(':') + 2:].strip()
                    elif 'CYLINDER GROUP INFORMATION' in line:
                        # noinspection PyBroadException
                        try:
                            process.terminate()  # some attempt
                        except Exception:
                            pass
                        break

                if self.info.get('lastmountpoint') and self.info.get('label'):
                    self.info['label'] = "{0} ({1})".format(self.info['lastmountpoint'], self.info['label'])
                elif self.info.get('lastmountpoint') and not self.info.get('label'):
                    self.info['label'] = self.info['lastmountpoint']
                elif not self.info.get('lastmountpoint') and self.info.get('label') and \
                        self.info['label'].startswith("/"):  # e.g. /boot1
                    if self.info['label'].endswith("1"):
                        self.info['lastmountpoint'] = self.info['label'][:-1]
                    else:
                        self.info['lastmountpoint'] = self.info['label']

            except Exception as e:  # ignore any exceptions here.
                logger.exception("Error while obtaining stats.")
                pass

        thread = threading.Thread(target=stats_thread)
        thread.start()

        duration = 5  # longest possible duration for fsstat.
        thread.join(duration)
        if thread.is_alive():
            # noinspection PyBroadException
            try:
                process.terminate()
            except Exception:
                pass
            thread.join()
            logger.debug("Killed fsstat after {0}s".format(duration))

    def detect_mountpoint(self):
        """Attempts to detect the previous mountpoint if this was not done through :func:`load_fsstat_data`. This
        detection does some heuristic method on the mounted volume.
        """

        if self.info.get('lastmountpoint'):
            return self.info.get('lastmountpoint')
        if not self.mountpoint:
            return None

        result = None
        paths = os.listdir(self.mountpoint)
        if 'grub' in paths:
            result = '/boot'
        elif 'usr' in paths and 'var' in paths and 'root' in paths:
            result = '/'
        elif 'bin' in paths and 'lib' in paths and 'local' in paths and 'src' in paths and not 'usr' in paths:
            result = '/usr'
        elif 'bin' in paths and 'lib' in paths and 'local' not in paths and 'src' in paths and not 'usr' in paths:
            result = '/usr/local'
        elif 'lib' in paths and 'local' in paths and 'tmp' in paths and not 'var' in paths:
            result = '/var'
        # elif sum(['bin' in paths, 'boot' in paths, 'cdrom' in paths, 'dev' in paths, 'etc' in paths, 'home' in paths,
        #          'lib' in paths, 'lib64' in paths, 'media' in paths, 'mnt' in paths, 'opt' in paths,
        #          'proc' in paths, 'root' in paths, 'sbin' in paths, 'srv' in paths, 'sys' in paths, 'tmp' in paths,
        #          'usr' in paths, 'var' in paths]) > 11:
        #    result = '/'

        if result:
            self.info['lastmountpoint'] = result
            if not self.info.get('label'):
                self.info['label'] = self.info['lastmountpoint']
            logger.info("Detected mountpoint as {0} based on files in volume".format(self.info['lastmountpoint']))

        return result

    # noinspection PyBroadException
    def unmount(self):
        """Unounts the volume from the filesystem.

        :raises SubsystemError: if one of the underlying processes fails
        :raises CleanupError: if the cleanup fails
        """

        for volume in self.volumes:
            try:
                volume.unmount()
            except ImageMounterError:
                pass

        if self.is_mounted:
            logger.info("Unmounting volume %s", self)

        if self.loopback and self.info.get('volume_group'):
            _util.check_call_(["lvm", 'vgchange', '-a', 'n', self.info['volume_group']],
                              wrap_error=True, stdout=subprocess.PIPE)
            self.info['volume_group'] = ""

        if self.loopback and self._paths.get('luks'):
            _util.check_call_(['cryptsetup', 'luksClose', self._paths['luks']], wrap_error=True, stdout=subprocess.PIPE)
            del self._paths['luks']

        if self._paths.get('bde'):
            _util.clean_unmount(['fusermount', '-u'], self._paths['bde'])
            del self._paths['bde']

        if self._paths.get('md'):
            md_path = self._paths['md']
            del self._paths['md']  # removing it here to ensure we do not enter an infinite loop, will add it back later

            # MD arrays are a bit complicated, we also check all other volumes that are part of this array and
            # unmount them as well.
            logger.debug("All other volumes that use %s as well will also be unmounted", md_path)
            for v in self.disk.get_volumes():
                if v != self and v._paths.get('md') == md_path:
                    v.unmount()

            try:
                _util.check_output_(["mdadm", '--stop', md_path], stderr=subprocess.STDOUT)
            except Exception as e:
                self._paths['md'] = md_path
                raise SubsystemError(e)

        if self._paths.get('vss'):
            _util.clean_unmount(['fusermount', '-u'], self._paths['vss'])
            del self._paths['vss']

        if self.loopback:
            _util.check_call_(['losetup', '-d', self.loopback], wrap_error=True)
            self.loopback = ""

        if self._paths.get('bindmounts'):
            for mp in self._paths['bindmounts']:
                _util.clean_unmount(['umount'], mp, rmdir=False)
            del self._paths['bindmounts']

        if self.mountpoint:
            _util.clean_unmount(['umount'], self.mountpoint)
            self.mountpoint = ""

        if self._paths.get('carve'):
            try:
                shutil.rmtree(self._paths['carve'])
            except OSError as e:
                raise SubsystemError(e)
            else:
                del self._paths['carve']

        self.is_mounted = False
Ejemplo n.º 8
0
class Disk(object):
    """Representation of a disk, image file or anything else that can be considered a disk. """

    # noinspection PyUnusedLocal
    def __init__(
        self,
        parser,
        path,
        index=None,
        offset=0,
        block_size=BLOCK_SIZE,
        read_write=False,
        vstype="",
        disk_mounter="auto",
        volume_detector="auto",
    ):
        """Instantiation of this class does not automatically mount, detect or analyse the disk. You will need the
        :func:`init` method for this.

        Only use arguments offset and further as keyword arguments.

        :param parser: the parent parser
        :type parser: :class:`ImageParser`
        :param str path: the path of the Disk
        :param str index: the base index of this Disk
        :param int offset: offset of the disk where the volume (system) resides
        :param int block_size:
        :param bool read_write: indicates whether the disk should be mounted with a read-write cache enabled
        :param str vstype: the volume system type to use.
        :param str disk_mounter: the method to mount the base image with
        :param str volume_detector: the volume system detection method to use
        """

        self.parser = parser

        # Find the type and the paths
        path = os.path.expandvars(os.path.expanduser(path))
        self.paths = sorted(_util.expand_path(path))

        self.offset = offset
        self.block_size = block_size
        self.read_write = read_write
        self.disk_mounter = disk_mounter or "auto"
        self.index = index

        self._name = os.path.split(path)[1]
        self._paths = {}
        self.rwpath = ""
        self.mountpoint = ""
        self.volumes = VolumeSystem(parent=self, volume_detector=volume_detector, vstype=vstype)

        self.was_mounted = False
        self.is_mounted = False

        self._disktype = defaultdict(dict)

    def __unicode__(self):
        return self._name

    def __str__(self):
        return self.__unicode__()

    def __getitem__(self, item):
        return self.volumes[item]

    def get_disk_type(self):
        if _util.is_encase(self.paths[0]):
            return "encase"
        elif _util.is_vmware(self.paths[0]):
            return "vmdk"
        elif _util.is_compressed(self.paths[0]):
            return "compressed"
        else:
            return "dd"

    def _get_mount_methods(self, disk_type):
        """Finds which mount methods are suitable for the specified disk type. Returns a list of all suitable mount
        methods.
        """
        if self.disk_mounter == "auto":
            methods = []

            def add_method_if_exists(method):
                if (method == "avfs" and _util.command_exists("avfsd")) or _util.command_exists(method):
                    methods.append(method)

            if self.read_write:
                add_method_if_exists("xmount")
            else:
                if disk_type == "encase":
                    add_method_if_exists("ewfmount")
                elif disk_type == "vmdk":
                    add_method_if_exists("vmware-mount")
                    add_method_if_exists("affuse")
                elif disk_type == "dd":
                    add_method_if_exists("affuse")
                elif disk_type == "compressed":
                    add_method_if_exists("avfs")
                add_method_if_exists("xmount")
        else:
            methods = [self.disk_mounter]
        return methods

    def _mount_avfs(self):
        """Mounts the AVFS filesystem."""

        self._paths["avfs"] = tempfile.mkdtemp(prefix="image_mounter_avfs_")

        # start by calling the mountavfs command to initialize avfs
        _util.check_call_(["avfsd", self._paths["avfs"], "-o", "allow_other"], stdout=subprocess.PIPE)

        # no multifile support for avfs
        avfspath = self._paths["avfs"] + "/" + os.path.abspath(self.paths[0]) + "#"
        targetraw = os.path.join(self.mountpoint, "avfs")

        os.symlink(avfspath, targetraw)
        logger.debug("Symlinked {} with {}".format(avfspath, targetraw))
        raw_path = self.get_raw_path()
        logger.debug("Raw path to avfs is {}".format(raw_path))
        if raw_path is None:
            raise MountpointEmptyError()

    def mount(self):
        """Mounts the base image on a temporary location using the mount method stored in :attr:`method`. If mounting
        was successful, :attr:`mountpoint` is set to the temporary mountpoint.

        If :attr:`read_write` is enabled, a temporary read-write cache is also created and stored in :attr:`rwpath`.

        :return: whether the mounting was successful
        :rtype: bool
        """

        if self.parser.casename:
            self.mountpoint = tempfile.mkdtemp(prefix="image_mounter_", suffix="_" + self.parser.casename)
        else:
            self.mountpoint = tempfile.mkdtemp(prefix="image_mounter_")

        if self.read_write:
            self.rwpath = tempfile.mkstemp(prefix="image_mounter_rw_cache_")[1]

        disk_type = self.get_disk_type()
        methods = self._get_mount_methods(disk_type)

        cmds = []
        for method in methods:
            if method == "avfs":  # avfs does not participate in the fallback stuff, unfortunately
                self._mount_avfs()
                self.disk_mounter = method
                self.was_mounted = True
                self.is_mounted = True
                return

            elif method == "dummy":
                os.rmdir(self.mountpoint)
                self.mountpoint = ""
                logger.debug("Raw path to dummy is {}".format(self.get_raw_path()))
                self.disk_mounter = method
                self.was_mounted = True
                self.is_mounted = True
                return

            elif method == "xmount":
                cmds.append(["xmount", "--in", "ewf" if disk_type == "encase" else "dd"])
                if self.read_write:
                    cmds[-1].extend(["--rw", self.rwpath])
                cmds[-1].extend(self.paths)  # specify all paths, xmount needs this :(
                cmds[-1].append(self.mountpoint)

            elif method == "affuse":
                cmds.extend(
                    [
                        ["affuse", "-o", "allow_other", self.paths[0], self.mountpoint],
                        ["affuse", self.paths[0], self.mountpoint],
                    ]
                )

            elif method == "ewfmount":
                cmds.extend(
                    [
                        ["ewfmount", "-X", "allow_other", self.paths[0], self.mountpoint],
                        ["ewfmount", self.paths[0], self.mountpoint],
                    ]
                )

            elif method == "vmware-mount":
                cmds.append(["vmware-mount", "-r", "-f", self.paths[0], self.mountpoint])

            else:
                raise ArgumentError("Unknown mount method {0}".format(self.disk_mounter))

        for cmd in cmds:
            # noinspection PyBroadException
            try:
                _util.check_call_(cmd, stdout=subprocess.PIPE)
                # mounting does not seem to be instant, add a timer here
                time.sleep(0.1)
            except Exception:
                logger.warning("Could not mount {0}, trying other method".format(self.paths[0]), exc_info=True)
                continue
            else:
                raw_path = self.get_raw_path()
                logger.debug("Raw path to disk is {}".format(raw_path))
                self.disk_mounter = cmd[0]

                if raw_path is None:
                    raise MountpointEmptyError()
                self.was_mounted = True
                self.is_mounted = True
                return

        logger.error("Unable to mount {0}".format(self.paths[0]))
        os.rmdir(self.mountpoint)
        self.mountpoint = ""
        raise MountError()

    def get_raw_path(self):
        """Returns the raw path to the mounted disk image, i.e. the raw :file:`.dd`, :file:`.raw` or :file:`ewf1`
        file.

        :rtype: str
        """

        if self.disk_mounter == "dummy":
            return self.paths[0]
        else:
            if self.disk_mounter == "avfs" and os.path.isdir(os.path.join(self.mountpoint, "avfs")):
                logger.debug("AVFS mounted as a directory, will look in directory for (random) file.")
                # there is no support for disks inside disks, so this will fail to work for zips containing
                # E01 files or so.
                searchdirs = (os.path.join(self.mountpoint, "avfs"), self.mountpoint)
            else:
                searchdirs = (self.mountpoint,)

            raw_path = []
            for searchdir in searchdirs:
                # avfs: apparently it is not a dir
                for pattern in ["*.dd", "*.iso", "*.raw", "*.dmg", "ewf1", "flat", "avfs"]:
                    raw_path.extend(glob.glob(os.path.join(searchdir, pattern)))

            if not raw_path:
                logger.warning("No viable mount file found in {}.".format(searchdirs))
                return None
            return raw_path[0]

    def get_fs_path(self):
        """Returns the path to the filesystem. Most of the times this is the image file, but may instead also return
        the MD device or loopback device the filesystem is mounted to.

        :rtype: str
        """

        if self._paths.get("md"):
            return self._paths["md"]
        else:
            return self.get_raw_path()

    def detect_volumes(self, single=None):
        """Generator that detects the volumes from the Disk, using one of two methods:

        * Single volume: the entire Disk is a single volume
        * Multiple volumes: the Disk is a volume system

        :param single: If *single* is :const:`True`, this method will call :Func:`init_single_volumes`.
                       If *single* is False, only :func:`init_multiple_volumes` is called. If *single* is None,
                       :func:`init_multiple_volumes` is always called, being followed by :func:`init_single_volume`
                       if no volumes were detected.
        """
        # prevent adding the same volumes twice
        if self.volumes.has_detected:
            for v in self.volumes:
                yield v

        elif single:
            for v in self.volumes.detect_volumes(method="single"):
                yield v

        else:
            # if single == False or single == None, loop over all volumes
            amount = 0
            try:
                for v in self.volumes.detect_volumes():
                    amount += 1
                    yield v
            except ImageMounterError:
                pass  # ignore and continue to single mount

            # if single == None and no volumes were mounted, use single_volume
            if single is None and amount == 0:
                logger.info("Detecting as single volume instead")
                for v in self.volumes.detect_volumes(method="single", force=True):
                    yield v

    def init(self, single=None, only_mount=None, swallow_exceptions=True):
        """Calls several methods required to perform a full initialisation: :func:`mount`, and
        :func:`mount_volumes` and yields all detected volumes.

        :param bool|None single: indicates whether the disk should be mounted as a single disk, not as a single disk or
            whether it should try both (defaults to :const:`None`)
        :rtype: generator
        """

        self.mount()
        self.volumes.preload_volume_data()

        for v in self.init_volumes(single, only_mount=only_mount, swallow_exceptions=swallow_exceptions):
            yield v

    def init_volumes(self, single=None, only_mount=None, swallow_exceptions=True):
        """Generator that detects and mounts all volumes in the disk.

        :param single: If *single* is :const:`True`, this method will call :Func:`init_single_volumes`.
                       If *single* is False, only :func:`init_multiple_volumes` is called. If *single* is None,
                       :func:`init_multiple_volumes` is always called, being followed by :func:`init_single_volume`
                       if no volumes were detected.
        :param list only_mount: If set, must be a list of volume indexes that are only mounted.
        :param bool swallow_exceptions: If True, Exceptions are not raised but rather set on the instance.
        """

        for volume in self.detect_volumes(single=single):
            for vol in volume.init(only_mount=only_mount, swallow_exceptions=swallow_exceptions):
                yield vol

    def get_volumes(self):
        """Gets a list of all volumes in this disk, including volumes that are contained in other volumes."""

        volumes = []
        for v in self.volumes:
            volumes.extend(v.get_volumes())
        return volumes

    def rw_active(self):
        """Indicates whether anything has been written to a read-write cache."""

        return self.rwpath and os.path.getsize(self.rwpath)

    def unmount(self, remove_rw=False):
        """Removes all ties of this disk to the filesystem, so the image can be unmounted successfully.

        :raises SubsystemError: when one of the underlying commands fails. Some are swallowed.
        :raises CleanupError: when actual cleanup fails. Some are swallowed.
        """

        for m in list(sorted(self.volumes, key=lambda v: v.mountpoint or "", reverse=True)):
            try:
                m.unmount()
            except ImageMounterError:
                logger.warning("Error unmounting volume {0}".format(m.mountpoint))

        if self.mountpoint:
            _util.clean_unmount(["fusermount", "-u"], self.mountpoint)

        if self._paths.get("avfs"):
            _util.clean_unmount(["fusermount", "-u"], self._paths["avfs"])

        if self.rw_active() and remove_rw:
            os.remove(self.rwpath)

        self.is_mounted = False
Ejemplo n.º 9
0
class Volume(object):
    """Information about a volume. Note that every detected volume gets their own Volume object, though it may or may
    not be mounted. This can be seen through the :attr:`mountpoint` attribute -- if it is not set, perhaps the
    :attr:`exception` attribute is set with an exception.
    """

    def __init__(self, disk, parent=None, index="0", size=0, offset=0, flag='alloc', slot=0, fstype="", key="",
                 vstype='', volume_detector='auto'):
        """Creates a Volume object that is not mounted yet.

        Only use arguments as keyword arguments.

        :param disk: the parent disk
        :type disk: :class:`Disk`
        :param parent: the parent volume or disk.
        :param str index: the volume index within its volume system, see the attribute documentation.
        :param int size: the volume size, see the attribute documentation.
        :param int offset: the volume offset, see the attribute documentation.
        :param str flag: the volume flag, see the attribute documentation.
        :param int slot: the volume slot, see the attribute documentation.
        :param str fstype: the fstype you wish to use for this Volume. May be ?<fstype> as a fallback value. If not
                           specified, will be retrieved from the ImageParser instance instead.
        :param str key: the key to use for this Volume.
        :param str vstype: the volume system type to use.
        :param str volume_detector: the volume system detection method to use
        """

        self.parent = parent
        self.disk = disk

        # Should be filled somewhere
        self.size = size
        self.offset = offset
        self.index = index
        self.slot = slot
        self.flag = flag
        self.block_size = self.disk.block_size

        self.volumes = VolumeSystem(parent=self, vstype=vstype, volume_detector=volume_detector)

        self.fstype = fstype
        self._get_fstype_from_parser(fstype)

        if key:
            self.key = key
        elif self.index in self.disk.parser.keys:
            self.key = self.disk.parser.keys[self.index]
        else:
            self.key = ""

        self.info = {}
        self._paths = {}

        self.mountpoint = ""
        self.loopback = ""
        self.was_mounted = False
        self.is_mounted = False

    def __unicode__(self):
        return '{0}:{1}'.format(self.index, self.info.get('fsdescription') or '-')

    def __str__(self):
        return str(self.__unicode__())

    def __getitem__(self, item):
        return self.volumes[item]

    def _get_fstype_from_parser(self, fstype=None):
        """Load fstype information from the parser instance."""
        if fstype:
            self.fstype = fstype
        elif self.index in self.disk.parser.fstypes:
            self.fstype = self.disk.parser.fstypes[self.index]
        elif '*' in self.disk.parser.fstypes:
            self.fstype = self.disk.parser.fstypes['*']
        elif '?' in self.disk.parser.fstypes and self.disk.parser.fstypes['?'] is not None:
            self.fstype = "?" + self.disk.parser.fstypes['?']
        else:
            self.fstype = ""

        if self.fstype in VOLUME_SYSTEM_TYPES:
            self.volumes.vstype = self.fstype
            self.fstype = 'volumesystem'

    def get_description(self, with_size=True, with_index=True):
        """Obtains a generic description of the volume, containing the file system type, index, label and NTFS version.
        If *with_size* is provided, the volume size is also included.
        """

        desc = ''

        if with_size and self.size:
            desc += '{0} '.format(self.get_formatted_size())

        s = self.info.get('statfstype') or self.info.get('fsdescription') or '-'
        if with_index:
            desc += '{1}:{0}'.format(s, self.index)
        else:
            desc += s

        if self.info.get('label'):
            desc += ' {0}'.format(self.info.get('label'))

        if self.info.get('version'):  # NTFS
            desc += ' [{0}]'.format(self.info.get('version'))

        return desc

    def get_formatted_size(self):
        """Obtains the size of the volume in a human-readable format (i.e. in TiBs, GiBs or MiBs)."""

        if self.size is not None:
            if self.size < 1024:
                return "{0} B".format(self.size)
            elif self.size < 1024 ** 2:
                return "{0} KiB".format(round(self.size / 1024, 2))
            elif self.size < 1024 ** 3:
                return "{0} MiB".format(round(self.size / 1024 ** 2, 2))
            elif self.size < 1024 ** 4:
                return "{0} GiB".format(round(self.size / 1024 ** 3, 2))
            else:
                return "{0} TiB".format(round(self.size / 1024 ** 4, 2))
        else:
            return self.size

    def _get_blkid_type(self):
        """Retrieves the FS type from the blkid command."""
        try:
            result = _util.check_output_(['blkid', '-p', '-O', str(self.offset), self.get_raw_path()])
            if not result:
                return None

            # noinspection PyTypeChecker
            blkid_result = dict(re.findall(r'([A-Z]+)="(.+?)"', result))

            self.info['blkid_data'] = blkid_result

            if 'PTTYPE' in blkid_result and 'TYPE' not in blkid_result:
                return blkid_result.get('PTTYPE')
            else:
                return blkid_result.get('TYPE')

        except Exception:
            return None  # returning None is better here, since we do not care about the exception in determine_fs_type

    def _get_magic_type(self):
        """Checks the volume for its magic bytes and returns the magic."""

        with io.open(self.disk.get_fs_path(), "rb") as file:
            file.seek(self.offset)
            fheader = file.read(min(self.size, 4096) if self.size else 4096)

        # TODO fallback to img-cat image -s blocknum | file -
        # if we were able to load the module magic
        try:
            # noinspection PyUnresolvedReferences
            import magic

            if hasattr(magic, 'from_buffer'):
                # using https://github.com/ahupp/python-magic
                logger.debug("Using python-magic Python package for file type magic")
                result = magic.from_buffer(fheader).decode()
                self.info['magic_data'] = result
                return result

            elif hasattr(magic, 'open'):
                # using Magic file extensions by Rueben Thomas (Ubuntu python-magic module)
                logger.debug("Using python-magic system package for file type magic")
                ms = magic.open(magic.NONE)
                ms.load()
                result = ms.buffer(fheader)
                ms.close()
                self.info['magic_data'] = result
                return result

            else:
                logger.warning("The python-magic module is not available, but another module named magic was found.")

        except ImportError:
            logger.warning("The python-magic module is not available.")
        except AttributeError:
            logger.warning("The python-magic module is not available, but another module named magic was found.")
        return None  # returning None is better here, since we do not care about the exception in determine_fs_type

    def get_raw_path(self, include_self=False):
        """Retrieves the base mount path of the volume. Typically equals to :func:`Disk.get_fs_path` but may also be the
        path to a logical volume. This is used to determine the source path for a mount call.

        The value returned is normally based on the parent's paths, e.g. if this volume is mounted to a more specific
        path, only its children return the more specific path, this volume itself will keep returning the same path.
        This makes for consistent use of the offset attribute. If you do not need this behaviour, you can override this
        with the include_self argument.
        """

        v = self
        if not include_self:
            if v.parent and v.parent != self.disk:
                v = v.parent
            else:
                return self.disk.get_fs_path()

        while True:
            if v._paths.get('lv'):
                return v._paths['lv']
            elif v._paths.get('bde'):
                return v._paths['bde'] + '/bde1'
            elif v._paths.get('luks'):
                return '/dev/mapper/' + v._paths['luks']
            elif v._paths.get('md'):
                return v._paths['md']
            elif v._paths.get('vss_store'):
                return v._paths['vss_store']

            # Only if the volume has a parent that is not a disk, we try to check the parent for a location.
            if v.parent and v.parent != self.disk:
                v = v.parent
            else:
                break
        return self.disk.get_fs_path()

    def get_safe_label(self):
        """Returns a label that is safe to add to a path in the mountpoint for this volume."""

        if self.info.get('label') == '/':
            return 'root'

        suffix = re.sub(r"[/ \(\)]+", "_", self.info.get('label')) if self.info.get('label') else ""
        if suffix and suffix[0] == '_':
            suffix = suffix[1:]
        if len(suffix) > 2 and suffix[-1] == '_':
            suffix = suffix[:-1]
        return suffix

    def carve(self, freespace=True):
        """Call this method to carve the free space of the volume for (deleted) files. Note that photorec has its
        own interface that temporarily takes over the shell.

        :param freespace: indicates whether the entire volume should be carved (False) or only the free space (True)
        :type freespace: bool
        :return: string to the path where carved data is available
        :raises CommandNotFoundError: if the underlying command does not exist
        :raises SubsystemError: if the underlying command fails
        :raises NoMountpointAvailableError: if there is no mountpoint available
        :raises NoLoopbackAvailableError: if there is no loopback available (only when volume has no slot number)
        """

        if not _util.command_exists('photorec'):
            logger.warning("photorec is not installed, could not carve volume")
            raise CommandNotFoundError("photorec")

        self._make_mountpoint(var_name='carve', suffix="carve", in_paths=True)

        # if no slot, we need to make a loopback that we can use to carve the volume
        loopback_was_created_for_carving = False
        if not self.slot:
            if not self.loopback:
                self._find_loopback()
                #Can't carve if volume has no slot number and can't be mounted on loopback.
                loopback_was_created_for_carving = True

            # noinspection PyBroadException
            try:
                _util.check_call_(["photorec", "/d", self._paths['carve'] + os.sep, "/cmd", self.loopback,
                                  ("freespace," if freespace else "") + "search"])

                # clean out the loop device if we created it specifically for carving
                if loopback_was_created_for_carving:
                    # noinspection PyBroadException
                    try:
                        _util.check_call_(['losetup', '-d', self.loopback])
                    except Exception:
                        pass
                    else:
                        self.loopback = ""

                return self._paths['carve']
            except Exception as e:
                logger.exception("Failed carving the volume.")
                raise SubsystemError(e)
        else:
            # noinspection PyBroadException
            try:
                _util.check_call_(["photorec", "/d", self._paths['carve'] + os.sep, "/cmd", self.get_raw_path(),
                                  str(self.slot) + (",freespace" if freespace else "") + ",search"])
                return self._paths['carve']

            except Exception as e:
                logger.exception("Failed carving the volume.")
                raise SubsystemError(e)

    def detect_volume_shadow_copies(self):
        """Method to call vshadowmount and mount NTFS volume shadow copies.

        :return: iterable with the :class:`Volume` objects of the VSS
        :raises CommandNotFoundError: if the underlying command does not exist
        :raises SubSystemError: if the underlying command fails
        :raises NoMountpointAvailableError: if there is no mountpoint available
        """

        if not _util.command_exists('vshadowmount'):
            logger.warning("vshadowmount is not installed, could not mount volume shadow copies")
            raise CommandNotFoundError('vshadowmount')

        self._make_mountpoint(var_name='vss', suffix="vss", in_paths=True)

        try:
            _util.check_call_(["vshadowmount", "-o", str(self.offset), self.get_raw_path(), self._paths['vss']])
        except Exception as e:
            logger.exception("Failed mounting the volume shadow copies.")
            raise SubsystemError(e)
        else:
            return self.volumes.detect_volumes(vstype='vss')

    def _should_mount(self, only_mount=None):
        """Indicates whether this volume should be mounted. Internal method, used by imount.py"""

        return only_mount is None or \
            self.index in only_mount or \
            self.info.get('lastmountpoint') in only_mount or \
            self.info.get('label') in only_mount

    def init(self, only_mount=None, swallow_exceptions=True):
        """Generator that mounts this volume and either yields itself or recursively generates its subvolumes.

        More specifically, this function will call :func:`load_fsstat_data` (iff *no_stats* is False), followed by
        :func:`mount`, followed by a call to :func:`detect_mountpoint`, after which ``self`` is yielded, or the result
        of the :func:`init` call on each subvolume is yielded

        :param only_mount: if specified, only volume indexes in this list are mounted. Volume indexes are strings.
        :param swallow_exceptions: if True, any error occuring when mounting the volume is swallowed and added as an
            exception attribute to the yielded objects.
        """
        if swallow_exceptions:
            self.exception = None

        try:
            if not self._should_mount(only_mount):
                yield self
                return

            if not self.init_volume():
                yield self
                return

        except ImageMounterError as e:
            if swallow_exceptions:
                self.exception = e
            else:
                raise

        if not self.volumes:
            yield self
        else:
            for v in self.volumes:
                for s in v.init(only_mount, swallow_exceptions):
                    yield s

    def init_volume(self, fstype=None):
        """Initializes a single volume. You should use this method instead of :func:`mount` if you want some sane checks
        before mounting.
        """

        logger.debug("Initializing volume {0}".format(self))

        if not self._should_mount():
            return False

        if self.flag != 'alloc':
            return False

        if self.info.get('raid_status') == 'waiting':
            logger.info("RAID array %s not ready for mounting", self)
            return False

        if self.is_mounted:
            logger.info("%s is currently mounted, not mounting it again", self)
            return False

        logger.info("Mounting volume {0}".format(self))
        self.mount(fstype=fstype)
        self.detect_mountpoint()

        return True

    def _make_mountpoint(self, casename=None, var_name='mountpoint', suffix='', in_paths=False):
        """Creates a directory that can be used as a mountpoint. The directory is stored in :attr:`mountpoint`,
        or the varname as specified by the argument. If in_paths is True, the path is stored in the :attr:`_paths`
        attribute instead.

        :returns: the mountpoint path
        :raises NoMountpointAvailableError: if no mountpoint could be made
        """
        parser = self.disk.parser

        if parser.mountdir and not os.path.exists(parser.mountdir):
            os.makedirs(parser.mountdir)

        if parser.pretty:
            md = parser.mountdir or tempfile.gettempdir()
            case_name = casename or self.disk.parser.casename or \
                        ".".join(os.path.basename(self.disk.paths[0]).split('.')[0:-1]) or \
                        os.path.basename(self.disk.paths[0])
            if self.disk.parser.casename == case_name:  # the casename is already in the path in this case
                pretty_label = "{0}-{1}".format(self.index, self.get_safe_label() or self.fstype or 'volume')
            else:
                pretty_label = "{0}-{1}-{2}".format(case_name, self.index,
                                                    self.get_safe_label() or self.fstype or 'volume')
            if suffix:
                pretty_label += "-" + suffix
            path = os.path.join(md, pretty_label)

            # check if path already exists, otherwise try to find another nice path
            if os.path.exists(path):
                for i in range(2, 100):
                    path = os.path.join(md, pretty_label + "-" + str(i))
                    if not os.path.exists(path):
                        break
                else:
                    logger.error("Could not find free mountdir.")
                    raise NoMountpointAvailableError()

            # noinspection PyBroadException
            try:
                os.mkdir(path, 777)
                if in_paths:
                    self._paths[var_name] = path
                else:
                    setattr(self, var_name, path)
                return path
            except Exception:
                logger.exception("Could not create mountdir.")
                raise NoMountpointAvailableError()
        else:
            t = tempfile.mkdtemp(prefix='im_' + self.index + '_',
                                 suffix='_' + self.get_safe_label() + ("_" + suffix if suffix else ""),
                                 dir=parser.mountdir)
            if in_paths:
                self._paths[var_name] = t
            else:
                setattr(self, var_name, t)
            return t

    def _find_loopback(self, use_loopback=True, var_name='loopback'):
        """Finds a free loopback device that can be used. The loopback is stored in :attr:`loopback`. If *use_loopback*
        is True, the loopback will also be used directly.

        :returns: the loopback address
        :raises NoLoopbackAvailableError: if no loopback could be found
        """

        # noinspection PyBroadException
        try:
            loopback = _util.check_output_(['losetup', '-f']).strip()
            setattr(self, var_name, loopback)
        except Exception:
            logger.warning("No free loopback device found.", exc_info=True)
            raise NoLoopbackAvailableError()

        # noinspection PyBroadException
        if use_loopback:
            try:
                cmd = ['losetup', '-o', str(self.offset), '--sizelimit', str(self.size),
                       loopback, self.get_raw_path()]
                if not self.disk.read_write:
                    cmd.insert(1, '-r')
                _util.check_call_(cmd, stdout=subprocess.PIPE)
            except Exception:
                logger.exception("Loopback device could not be mounted.")
                raise NoLoopbackAvailableError()
        return loopback

    def _free_loopback(self, var_name='loopback'):
        if getattr(self, var_name):
            _util.check_call_(['losetup', '-d', getattr(self, var_name)], wrap_error=True)
            setattr(self, var_name, "")

    def determine_fs_type(self):
        """Determines the FS type for this partition. This function is used internally to determine which mount system
        to use, based on the file system description. Return values include *ext*, *ufs*, *ntfs*, *lvm* and *luks*.

        Note: does not do anything if fstype is already set to something sensible.
        """

        fstype_fallback = self.fstype[1:] if self.fstype and self.fstype.startswith("?") else ""

        # Determine fs type. If forced, always use provided type.
        if self.fstype in FILE_SYSTEM_TYPES:
            pass  # already correctly set
        elif self.fstype in VOLUME_SYSTEM_TYPES:
            self.volumes.vstype = self.fstype
            self.fstype = 'volumesystem'
        else:
            last_resort = None  # use this if we can't determine the FS type more reliably
            # we have two possible sources for determining the FS type: the description given to us by the detection
            # method, and the type given to us by the stat function
            for fsdesc in (self.info.get('fsdescription'), self.info.get('guid'),
                           self._get_blkid_type, self.info.get('statfstype'), self._get_magic_type):
                # For efficiency reasons, not all functions are called instantly.
                if callable(fsdesc):
                    fsdesc = fsdesc()
                logger.debug("Trying to determine fs type from '{}'".format(fsdesc))
                if not fsdesc:
                    continue
                fsdesc = fsdesc.lower()

                # for the purposes of this function, logical volume is nothing, and 'primary' is rather useless info
                if fsdesc in ('logical volume', 'luks volume', 'bde volume', 'raid volume',
                              'primary', 'basic data partition', 'vss store'):
                    continue

                if fsdesc == 'directory':
                    self.fstype = 'dir'  # dummy fs type
                elif re.search(r'\bext[0-9]*\b', fsdesc):
                    self.fstype = 'ext'
                elif 'bsd' in fsdesc or 'ufs 2' in fsdesc:
                    self.fstype = 'ufs'
                elif '0x07' in fsdesc or 'ntfs' in fsdesc:
                    self.fstype = 'ntfs'
                elif '0x8e' in fsdesc or 'lvm' in fsdesc:
                    self.fstype = 'lvm'
                elif 'hfs+' in fsdesc:
                    self.fstype = 'hfs+'
                elif 'hfs' in fsdesc:
                    self.fstype = 'hfs'
                elif 'luks' in fsdesc:
                    self.fstype = 'luks'
                elif 'fat' in fsdesc or 'efi system partition' in fsdesc:
                    # based on http://en.wikipedia.org/wiki/EFI_System_partition, efi is always fat.
                    self.fstype = 'fat'
                elif 'iso 9660' in fsdesc:
                    self.fstype = 'iso'
                elif 'linux compressed rom file system' in fsdesc or 'cramfs' in fsdesc:
                    self.fstype = 'cramfs'
                elif fsdesc.startswith("sgi xfs") or re.search(r'\bxfs\b', fsdesc):
                    self.fstype = "xfs"
                elif 'swap file' in fsdesc or 'linux swap' in fsdesc or 'linux-swap' in fsdesc:
                    self.fstype = 'swap'
                elif 'squashfs' in fsdesc:
                    self.fstype = 'squashfs'
                elif "jffs2" in fsdesc:
                    self.fstype = 'jffs2'
                elif "minix filesystem" in fsdesc:
                    self.fstype = 'minix'
                elif 'vmfs_volume_member' in fsdesc:
                    self.fstype = 'vmfs'
                elif 'linux_raid_member' in fsdesc or 'linux software raid' in fsdesc:
                    self.fstype = 'raid'
                elif "dos/mbr boot sector" in fsdesc:
                    self.fstype = 'volumesystem'
                    self.volumes.vstype = 'detect'
                elif fsdesc in FILE_SYSTEM_TYPES:
                    # fallback for stupid cases where we can not determine 'ufs' from the fsdesc 'ufs'
                    self.fstype = fsdesc
                elif fsdesc in VOLUME_SYSTEM_TYPES:
                    self.fstype = 'volumesystem'
                    self.volumes.vstype = fsdesc
                elif fsdesc.upper() in FILE_SYSTEM_GUIDS:
                    # this is a bit of a workaround for the fill_guid method
                    self.fstype = FILE_SYSTEM_GUIDS[fsdesc.upper()]
                elif '0x83' in fsdesc:
                    # this is a linux mount, but we can't figure out which one.
                    # we hand it off to the OS, maybe it can try something.
                    # if we use last_resort for more enhanced stuff, we may need to check if we are not setting
                    # it to something less specific here
                    last_resort = 'unknown'
                    continue
                else:
                    continue  # this loop failed

                logger.info("Detected {0} as {1}".format(fsdesc, self.fstype))
                break  # we found something
            else:  # we found nothing
                # if last_resort is something more sensible than unknown, we use that
                # if we have specified a fsfallback which is not set to None, we use that
                # if last_resort is unknown or the fallback is not None, we use unknown
                if last_resort and last_resort != 'unknown':
                    self.fstype = last_resort
                elif fstype_fallback:
                    self.fstype = fstype_fallback
                elif last_resort == 'unknown' or not fstype_fallback:
                    self.fstype = 'unknown'

        return self.fstype

    def mount(self, fstype=None):
        """Based on the file system type as determined by :func:`determine_fs_type`, the proper mount command is executed
        for this volume. The volume is mounted in a temporary path (or a pretty path if :attr:`pretty` is enabled) in
        the mountpoint as specified by :attr:`mountpoint`.

        If the file system type is a LUKS container or LVM, additional methods may be called, adding subvolumes to
        :attr:`volumes`

        :raises NotMountedError: if the parent volume/disk is not mounted
        :raises NoMountpointAvailableError: if no mountpoint was found
        :raises NoLoopbackAvailableError: if no loopback device was found
        :raises UnsupportedFilesystemError: if the fstype is not supported for mounting
        :raises SubsystemError: if one of the underlying commands failed
        """

        if not self.parent.is_mounted:
            raise NotMountedError(self.parent)

        raw_path = self.get_raw_path()
        if fstype is None:
            fstype = self.determine_fs_type()
        self._load_fsstat_data()

        # we need a mountpoint if it is not a lvm or luks volume
        if fstype not in ('luks', 'lvm', 'bde', 'raid', 'volumesystem') and \
                fstype in FILE_SYSTEM_TYPES:
            self._make_mountpoint()

        # Prepare mount command
        try:
            def call_mount(type, opts):
                cmd = ['mount', raw_path, self.mountpoint, '-t', type, '-o', opts]
                if not self.disk.read_write:
                    cmd[-1] += ',ro'

                _util.check_output_(cmd, stderr=subprocess.STDOUT)

            if fstype == 'ext':
                call_mount('ext4', 'noexec,noload,loop,offset=' + str(self.offset))

            elif fstype == 'ufs':
                call_mount('ufs', 'ufstype=ufs2,loop,offset=' + str(self.offset))

            elif fstype == 'ntfs':
                call_mount('ntfs', 'show_sys_files,noexec,force,loop,offset=' + str(self.offset))

            elif fstype == 'xfs':
                call_mount('xfs', 'norecovery,loop,offset=' + str(self.offset))

            elif fstype == 'hfs+':
                call_mount('hfsplus', 'force,loop,offset=' + str(self.offset) + ',sizelimit=' + str(self.size))

            elif fstype in ('iso', 'udf', 'squashfs', 'cramfs', 'minix', 'fat', 'hfs'):
                mnt_type = {'iso': 'iso9660', 'fat': 'vfat'}.get(fstype, fstype)
                call_mount(mnt_type, 'loop,offset=' + str(self.offset))

            elif fstype == 'vmfs':
                self._find_loopback()
                _util.check_call_(['vmfs-fuse', self.loopback, self.mountpoint], stdout=subprocess.PIPE)

            elif fstype == 'unknown':  # mounts without specifying the filesystem type
                cmd = ['mount', raw_path, self.mountpoint, '-o', 'loop,offset=' + str(self.offset)]
                if not self.disk.read_write:
                    cmd[-1] += ',ro'

                _util.check_call_(cmd, stdout=subprocess.PIPE)

            elif fstype == 'jffs2':
                self._open_jffs2()

            elif fstype == 'luks':
                self._open_luks_container()

            elif fstype == 'bde':
                self._open_bde_container()

            elif fstype == 'lvm':
                self._open_lvm()
                self.volumes.vstype = 'lvm'
                for _ in self.volumes.detect_volumes('lvm'):
                    pass

            elif fstype == 'raid':
                self._open_raid_volume()

            elif fstype == 'dir':
                os.rmdir(self.mountpoint)
                os.symlink(raw_path, self.mountpoint)

            elif fstype == 'volumesystem':
                for _ in self.volumes.detect_volumes():
                    pass

            else:
                try:
                    size = self.size // self.disk.block_size
                except TypeError:
                    size = self.size

                logger.warning("Unsupported filesystem {0} (type: {1}, block offset: {2}, length: {3})"
                               .format(self, fstype, self.offset // self.disk.block_size, size))
                raise UnsupportedFilesystemError(fstype)

            self.was_mounted = True
            self.is_mounted = True
            self.fstype = fstype
        except Exception as e:
            logger.exception("Execution failed due to {} {}".format(type(e), e), exc_info=True)
            try:
                if self.mountpoint:
                    os.rmdir(self.mountpoint)
                    self.mountpoint = ""
                if self.loopback:
                    self.loopback = ""
            except Exception as e2:
                logger.exception("Clean-up failed", exc_info=True)

            if not isinstance(e, ImageMounterError):
                raise SubsystemError(e)
            else:
                raise

    def bindmount(self, mountpoint):
        """Bind mounts the volume to another mountpoint. Only works if the volume is already mounted.

        :raises NotMountedError: when the volume is not yet mounted
        :raises SubsystemError: when the underlying command failed
        """

        if not self.mountpoint:
            raise NotMountedError(self)
        try:
            _util.check_call_(['mount', '--bind', self.mountpoint, mountpoint], stdout=subprocess.PIPE)
            if 'bindmounts' in self._paths:
                self._paths['bindmounts'].append(mountpoint)
            else:
                self._paths['bindmounts'] = [mountpoint]
            return True
        except Exception as e:
            logger.exception("Error bind mounting {0}.".format(self))
            raise SubsystemError(e)

    def _open_luks_container(self):
        """Command that is an alternative to the :func:`mount` command that opens a LUKS container. The opened volume is
        added to the subvolume set of this volume. Requires the user to enter the key manually.

        TODO: add support for :attr:`keys`

        :return: the Volume contained in the LUKS container, or None on failure.
        :raises NoLoopbackAvailableError: when no free loopback could be found
        :raises IncorrectFilesystemError: when this is not a LUKS volume
        :raises SubsystemError: when the underlying command fails
        """

        # Open a loopback device
        self._find_loopback()

        # Check if this is a LUKS device
        # noinspection PyBroadException
        try:
            _util.check_call_(["cryptsetup", "isLuks", self.loopback], stderr=subprocess.STDOUT)
            # ret = 0 if isLuks
        except Exception:
            logger.warning("Not a LUKS volume")
            # clean the loopback device, we want this method to be clean as possible
            # noinspection PyBroadException
            try:
                self._free_loopback()
            except Exception:
                pass
            raise IncorrectFilesystemError()

        try:
            extra_args = []
            key = None
            if self.key:
                t, v = self.key.split(':', 1)
                if t == 'p':  # passphrase
                    key = v
                elif t == 'f':  # key-file
                    extra_args = ['--key-file', v]
                elif t == 'm':  # master-key-file
                    extra_args = ['--master-key-file', v]
            else:
                logger.warning("No key material provided for %s", self)
        except ValueError:
            logger.exception("Invalid key material provided (%s) for %s. Expecting [arg]:[value]", self.key, self)
            self._free_loopback()
            raise ArgumentError()

        # Open the LUKS container
        self._paths['luks'] = 'image_mounter_luks_' + str(random.randint(10000, 99999))

        # noinspection PyBroadException
        try:
            cmd = ["cryptsetup", "luksOpen", self.loopback, self._paths['luks']]
            cmd.extend(extra_args)
            if not self.disk.read_write:
                cmd.insert(1, '-r')

            if key is not None:
                logger.debug('$ {0}'.format(' '.join(cmd)))
                # for py 3.2+, we could have used input=, but that doesn't exist in py2.7.
                p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                p.communicate(key)
                p.wait()
                retcode = p.poll()
                if retcode:
                    raise KeyInvalidError()
            else:
                _util.check_call_(cmd)
        except ImageMounterError:
            del self._paths['luks']
            self._free_loopback()
            raise
        except Exception as e:
            del self._paths['luks']
            self._free_loopback()
            raise SubsystemError(e)

        size = None
        # noinspection PyBroadException
        try:
            result = _util.check_output_(["cryptsetup", "status", self._paths['luks']])
            for l in result.splitlines():
                if "size:" in l and "key" not in l:
                    size = int(l.replace("size:", "").replace("sectors", "").strip()) * self.disk.block_size
        except Exception:
            pass

        container = self.volumes._make_single_subvolume(flag='alloc', offset=0, size=size)
        container.info['fsdescription'] = 'LUKS Volume'

        return container

    def _open_bde_container(self):
        """Mounts a BDE container. Uses key material provided by the :attr:`keys` attribute. The key material should be
        provided in the same format as to :cmd:`bdemount`, used as follows:

        k:full volume encryption and tweak key
        p:passphrase
        r:recovery password
        s:file to startup key (.bek)

        :return: the Volume contained in the BDE container
        :raises ArgumentError: if the keys argument is invalid
        :raises SubsystemError: when the underlying command fails
        """

        self._paths['bde'] = tempfile.mkdtemp(prefix='image_mounter_bde_')

        try:
            if self.key:
                t, v = self.key.split(':', 1)
                key = ['-' + t, v]
            else:
                logger.warning("No key material provided for %s", self)
                key = []
        except ValueError:
            logger.exception("Invalid key material provided (%s) for %s. Expecting [arg]:[value]", self.key, self)
            raise ArgumentError()

        # noinspection PyBroadException
        try:
            cmd = ["bdemount", self.get_raw_path(), self._paths['bde'], '-o', str(self.offset)]
            cmd.extend(key)
            _util.check_call_(cmd)
        except Exception as e:
            del self._paths['bde']
            logger.exception("Failed mounting BDE volume %s.", self)
            raise SubsystemError(e)

        container = self.volumes._make_single_subvolume(flag='alloc', offset=0, size=self.size)
        container.info['fsdescription'] = 'BDE Volume'

        return container

    def _open_jffs2(self):
        """Perform specific operations to mount a JFFS2 image. This kind of image is sometimes used for things like
        bios images. so external tools are required but given this method you don't have to memorize anything and it
        works fast and easy.

        Note that this module might not yet work while mounting multiple images at the same time.
        """
        # we have to make a ram-device to store the image, we keep 20% overhead
        size_in_kb = int((self.size / 1024) * 1.2)
        _util.check_call_(['modprobe', '-v', 'mtd'])
        _util.check_call_(['modprobe', '-v', 'jffs2'])
        _util.check_call_(['modprobe', '-v', 'mtdram', 'total_size={}'.format(size_in_kb), 'erase_size=256'])
        _util.check_call_(['modprobe', '-v', 'mtdblock'])
        _util.check_call_(['dd', 'if=' + self.get_raw_path(), 'of=/dev/mtd0'])
        _util.check_call_(['mount', '-t', 'jffs2', '/dev/mtdblock0', self.mountpoint])

    def _open_lvm(self):
        """Performs mount actions on a LVM. Scans for active volume groups from the loopback device, activates it
        and fills :attr:`volumes` with the logical volumes.

        :raises NoLoopbackAvailableError: when no loopback was available
        :raises IncorrectFilesystemError: when the volume is not a volume group
        """
        os.environ['LVM_SUPPRESS_FD_WARNINGS'] = '1'

        # find free loopback device
        self._find_loopback()
        time.sleep(0.2)

        # Scan for new lvm volumes
        result = _util.check_output_(["lvm", "pvscan"])
        for l in result.splitlines():
            if self.loopback in l or (self.offset == 0 and self.get_raw_path() in l):
                for vg in re.findall(r'VG (\S+)', l):
                    self.info['volume_group'] = vg

        if not self.info.get('volume_group'):
            logger.warning("Volume is not a volume group. (Searching for %s)", self.loopback)
            raise IncorrectFilesystemError()

        # Enable lvm volumes
        _util.check_call_(["lvm", "vgchange", "-a", "y", self.info['volume_group']], stdout=subprocess.PIPE)

    def _open_raid_volume(self):
        """Add the volume to a RAID system. The RAID array is activated as soon as the array can be activated.

        :raises NoLoopbackAvailableError: if no loopback device was found
        """

        self._find_loopback()

        raid_status = None
        try:
            # use mdadm to mount the loopback to a md device
            # incremental and run as soon as available
            output = _util.check_output_(['mdadm', '-IR', self.loopback], stderr=subprocess.STDOUT)

            match = re.findall(r"attached to ([^ ,]+)", output)
            if match:
                self._paths['md'] = os.path.realpath(match[0])
                if 'which is already active' in output:
                    logger.info("RAID is already active in other volume, using %s", self._paths['md'])
                    raid_status = 'active'
                elif 'not enough to start' in output:
                    self._paths['md'] = self._paths['md'].replace("/dev/md/", "/dev/md")
                    logger.info("RAID volume added, but not enough to start %s", self._paths['md'])
                    raid_status = 'waiting'
                else:
                    logger.info("RAID started at {0}".format(self._paths['md']))
                    raid_status = 'active'
        except Exception as e:
            logger.exception("Failed mounting RAID.")
            raise SubsystemError(e)

        # search for the RAID volume
        for v in self.disk.parser.get_volumes():
            if v._paths.get("md") == self._paths['md'] and v.volumes:
                logger.debug("Adding existing volume %s to volume %s", v.volumes[0], self)
                v.volumes[0].info['raid_status'] = raid_status
                self.volumes.volumes.append(v.volumes[0])
                return v.volumes[0]
        else:
            logger.debug("Creating RAID volume for %s", self)
            container = self.volumes._make_single_subvolume(flag='alloc', offset=0, size=self.size)
            container.info['fsdescription'] = 'RAID Volume'
            container.info['raid_status'] = raid_status
            return container

    def get_volumes(self):
        """Recursively gets a list of all subvolumes and the current volume."""

        if self.volumes:
            volumes = []
            for v in self.volumes:
                volumes.extend(v.get_volumes())
            volumes.append(self)
            return volumes
        else:
            return [self]

    def _load_fsstat_data(self):
        """Using :command:`fsstat`, adds some additional information of the volume to the Volume."""

        if not _util.command_exists('fsstat'):
            logger.warning("fsstat is not installed, could not mount volume shadow copies")
            return

        process = None

        def stats_thread():
            try:
                cmd = ['fsstat', self.get_raw_path(), '-o', str(self.offset // self.disk.block_size)]
                logger.debug('$ {0}'.format(' '.join(cmd)))
                # noinspection PyShadowingNames
                process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

                for line in iter(process.stdout.readline, b''):
                    line = line.decode()
                    if line.startswith("File System Type:"):
                        self.info['statfstype'] = line[line.index(':') + 2:].strip()
                    elif line.startswith("Last Mount Point:") or line.startswith("Last mounted on:"):
                        self.info['lastmountpoint'] = line[line.index(':') + 2:].strip().replace("//", "/")
                    elif line.startswith("Volume Name:") and not self.info.get('label'):
                        self.info['label'] = line[line.index(':') + 2:].strip()
                    elif line.startswith("Version:"):
                        self.info['version'] = line[line.index(':') + 2:].strip()
                    elif line.startswith("Source OS:"):
                        self.info['version'] = line[line.index(':') + 2:].strip()
                    elif 'CYLINDER GROUP INFORMATION' in line:
                        # noinspection PyBroadException
                        try:
                            process.terminate()  # some attempt
                        except Exception:
                            pass
                        break

                if self.info.get('lastmountpoint') and self.info.get('label'):
                    self.info['label'] = "{0} ({1})".format(self.info['lastmountpoint'], self.info['label'])
                elif self.info.get('lastmountpoint') and not self.info.get('label'):
                    self.info['label'] = self.info['lastmountpoint']
                elif not self.info.get('lastmountpoint') and self.info.get('label') and \
                        self.info['label'].startswith("/"):  # e.g. /boot1
                    if self.info['label'].endswith("1"):
                        self.info['lastmountpoint'] = self.info['label'][:-1]
                    else:
                        self.info['lastmountpoint'] = self.info['label']

            except Exception as e:  # ignore any exceptions here.
                logger.exception("Error while obtaining stats.")
                pass

        thread = threading.Thread(target=stats_thread)
        thread.start()

        duration = 5  # longest possible duration for fsstat.
        thread.join(duration)
        if thread.is_alive():
            # noinspection PyBroadException
            try:
                process.terminate()
            except Exception:
                pass
            thread.join()
            logger.debug("Killed fsstat after {0}s".format(duration))

    def detect_mountpoint(self):
        """Attempts to detect the previous mountpoint if this was not done through :func:`load_fsstat_data`. This
        detection does some heuristic method on the mounted volume.
        """

        if self.info.get('lastmountpoint'):
            return self.info.get('lastmountpoint')
        if not self.mountpoint:
            return None

        result = None
        paths = os.listdir(self.mountpoint)
        if 'grub' in paths:
            result = '/boot'
        elif 'usr' in paths and 'var' in paths and 'root' in paths:
            result = '/'
        elif 'bin' in paths and 'lib' in paths and 'local' in paths and 'src' in paths and not 'usr' in paths:
            result = '/usr'
        elif 'bin' in paths and 'lib' in paths and 'local' not in paths and 'src' in paths and not 'usr' in paths:
            result = '/usr/local'
        elif 'lib' in paths and 'local' in paths and 'tmp' in paths and not 'var' in paths:
            result = '/var'
        # elif sum(['bin' in paths, 'boot' in paths, 'cdrom' in paths, 'dev' in paths, 'etc' in paths, 'home' in paths,
        #          'lib' in paths, 'lib64' in paths, 'media' in paths, 'mnt' in paths, 'opt' in paths,
        #          'proc' in paths, 'root' in paths, 'sbin' in paths, 'srv' in paths, 'sys' in paths, 'tmp' in paths,
        #          'usr' in paths, 'var' in paths]) > 11:
        #    result = '/'

        if result:
            self.info['lastmountpoint'] = result
            if not self.info.get('label'):
                self.info['label'] = self.info['lastmountpoint']
            logger.info("Detected mountpoint as {0} based on files in volume".format(self.info['lastmountpoint']))

        return result

    # noinspection PyBroadException
    def unmount(self):
        """Unounts the volume from the filesystem.

        :raises SubsystemError: if one of the underlying processes fails
        :raises CleanupError: if the cleanup fails
        """

        for volume in self.volumes:
            try:
                volume.unmount()
            except ImageMounterError:
                pass

        if self.is_mounted:
            logger.info("Unmounting volume %s", self)

        if self.loopback and self.info.get('volume_group'):
            _util.check_call_(["lvm", 'vgchange', '-a', 'n', self.info['volume_group']],
                              wrap_error=True, stdout=subprocess.PIPE)
            self.info['volume_group'] = ""

        if self.loopback and self._paths.get('luks'):
            _util.check_call_(['cryptsetup', 'luksClose', self._paths['luks']], wrap_error=True, stdout=subprocess.PIPE)
            del self._paths['luks']

        if self._paths.get('bde'):
            _util.clean_unmount(['fusermount', '-u'], self._paths['bde'])
            del self._paths['bde']

        if self._paths.get('md'):
            md_path = self._paths['md']
            del self._paths['md']  # removing it here to ensure we do not enter an infinite loop, will add it back later

            # MD arrays are a bit complicated, we also check all other volumes that are part of this array and
            # unmount them as well.
            logger.debug("All other volumes that use %s as well will also be unmounted", md_path)
            for v in self.disk.get_volumes():
                if v != self and v._paths.get('md') == md_path:
                    v.unmount()

            try:
                _util.check_output_(["mdadm", '--stop', md_path], stderr=subprocess.STDOUT)
            except Exception as e:
                self._paths['md'] = md_path
                raise SubsystemError(e)

        if self._paths.get('vss'):
            _util.clean_unmount(['fusermount', '-u'], self._paths['vss'])
            del self._paths['vss']

        if self.loopback:
            _util.check_call_(['losetup', '-d', self.loopback], wrap_error=True)
            self.loopback = ""

        if self._paths.get('bindmounts'):
            for mp in self._paths['bindmounts']:
                _util.clean_unmount(['umount'], mp, rmdir=False)
            del self._paths['bindmounts']

        if self.mountpoint:
            _util.clean_unmount(['umount'], self.mountpoint)
            self.mountpoint = ""

        if self._paths.get('carve'):
            try:
                shutil.rmtree(self._paths['carve'])
            except OSError as e:
                raise SubsystemError(e)
            else:
                del self._paths['carve']

        self.is_mounted = False
Ejemplo n.º 10
0
class Volume(object):
    """Information about a volume. Note that every detected volume gets their own Volume object, though it may or may
    not be mounted. This can be seen through the :attr:`mountpoint` attribute -- if it is not set, perhaps the
    :attr:`exception` attribute is set with an exception.
    """

    def __init__(self, disk, parent=None, index="0", size=0, offset=0, flag='alloc', slot=0, fstype=None, key="",
                 vstype='', volume_detector='auto'):
        """Creates a Volume object that is not mounted yet.

        Only use arguments as keyword arguments.

        :param disk: the parent disk
        :type disk: :class:`Disk`
        :param parent: the parent volume or disk.
        :param str index: the volume index within its volume system, see the attribute documentation.
        :param int size: the volume size, see the attribute documentation.
        :param int offset: the volume offset, see the attribute documentation.
        :param str flag: the volume flag, see the attribute documentation.
        :param int slot: the volume slot, see the attribute documentation.
        :param FileSystemType fstype: the fstype you wish to use for this Volume.
            If not specified, will be retrieved from the ImageParser instance instead.
        :param str key: the key to use for this Volume.
        :param str vstype: the volume system type to use.
        :param str volume_detector: the volume system detection method to use
        """

        self.parent = parent
        self.disk = disk

        # Should be filled somewhere
        self.size = size
        self.offset = offset
        self.index = index
        self.slot = slot
        self.flag = flag
        self.block_size = self.disk.block_size

        self.volumes = VolumeSystem(parent=self, vstype=vstype, volume_detector=volume_detector)

        self._get_fstype_from_parser(fstype)

        if key:
            self.key = key
        elif self.index in self.disk.parser.keys:
            self.key = self.disk.parser.keys[self.index]
        elif '*' in self.disk.parser.keys:
            self.key = self.disk.parser.keys['*']
        else:
            self.key = ""

        self.info = {}
        self._paths = {}

        self.mountpoint = ""
        self.loopback = ""
        self.was_mounted = False
        self.is_mounted = False

    def __unicode__(self):
        return '{0}:{1}'.format(self.index, self.info.get('fsdescription') or '-')

    def __str__(self):
        return str(self.__unicode__())

    def __getitem__(self, item):
        return self.volumes[item]

    @property
    def numeric_index(self):
        try:
            return tuple([int(x) for x in self.index.split(".")])
        except ValueError:
            return ()

    def _get_fstype_from_parser(self, fstype=None):
        """Load fstype information from the parser instance."""
        if fstype:
            self.fstype = fstype
        elif self.index in self.disk.parser.fstypes:
            self.fstype = self.disk.parser.fstypes[self.index]
        elif '*' in self.disk.parser.fstypes:
            self.fstype = self.disk.parser.fstypes['*']
        elif '?' in self.disk.parser.fstypes and self.disk.parser.fstypes['?'] is not None:
            self.fstype = "?" + self.disk.parser.fstypes['?']
        else:
            self.fstype = ""

        if self.fstype in VOLUME_SYSTEM_TYPES:
            self.volumes.vstype = self.fstype
            self.fstype = 'volumesystem'

        # convert fstype from string to a FileSystemType object
        if not isinstance(self.fstype, filesystems.FileSystemType):
            if self.fstype.startswith("?"):
                fallback = FILE_SYSTEM_TYPES[self.fstype[1:]]
                self.fstype = filesystems.FallbackFileSystemType(fallback)
            else:
                self.fstype = FILE_SYSTEM_TYPES[self.fstype]

    def get_description(self, with_size=True, with_index=True):
        """Obtains a generic description of the volume, containing the file system type, index, label and NTFS version.
        If *with_size* is provided, the volume size is also included.
        """

        desc = ''

        if with_size and self.size:
            desc += '{0} '.format(self.get_formatted_size())

        s = self.info.get('statfstype') or self.info.get('fsdescription') or '-'
        if with_index:
            desc += '{1}:{0}'.format(s, self.index)
        else:
            desc += s

        if self.info.get('label'):
            desc += ' {0}'.format(self.info.get('label'))

        if self.info.get('version'):  # NTFS
            desc += ' [{0}]'.format(self.info.get('version'))

        return desc

    def get_formatted_size(self):
        """Obtains the size of the volume in a human-readable format (i.e. in TiBs, GiBs or MiBs)."""

        if self.size is not None:
            if self.size < 1024:
                return "{0} B".format(self.size)
            elif self.size < 1024 ** 2:
                return "{0} KiB".format(round(self.size / 1024, 2))
            elif self.size < 1024 ** 3:
                return "{0} MiB".format(round(self.size / 1024 ** 2, 2))
            elif self.size < 1024 ** 4:
                return "{0} GiB".format(round(self.size / 1024 ** 3, 2))
            else:
                return "{0} TiB".format(round(self.size / 1024 ** 4, 2))
        else:
            return self.size

    @dependencies.require(dependencies.blkid, none_on_failure=True)
    def _get_blkid_type(self):
        """Retrieves the FS type from the blkid command."""
        try:
            result = _util.check_output_(['blkid', '-p', '-O', str(self.offset), self.get_raw_path()])
            if not result:
                return None

            # noinspection PyTypeChecker
            blkid_result = dict(re.findall(r'([A-Z]+)="(.+?)"', result))

            self.info['blkid_data'] = blkid_result

            if 'PTTYPE' in blkid_result and 'TYPE' not in blkid_result:
                return blkid_result.get('PTTYPE')
            else:
                return blkid_result.get('TYPE')

        except Exception:
            return None  # returning None is better here, since we do not care about the exception in determine_fs_type

    @dependencies.require(dependencies.magic, none_on_failure=True)
    def _get_magic_type(self):
        """Checks the volume for its magic bytes and returns the magic."""

        try:
            with io.open(self.disk.get_fs_path(), "rb") as file:
                file.seek(self.offset)
                fheader = file.read(min(self.size, 4096) if self.size else 4096)
        except IOError:
            logger.exception("Failed reading first 4K bytes from volume.")
            return None

        # TODO fallback to img-cat image -s blocknum | file -
        # if we were able to load the module magic
        try:
            # noinspection PyUnresolvedReferences
            import magic

            if hasattr(magic, 'from_buffer'):
                # using https://github.com/ahupp/python-magic
                logger.debug("Using python-magic Python package for file type magic")
                result = magic.from_buffer(fheader).decode()
                self.info['magic_data'] = result
                return result

            elif hasattr(magic, 'open'):
                # using Magic file extensions by Rueben Thomas (Ubuntu python-magic module)
                logger.debug("Using python-magic system package for file type magic")
                ms = magic.open(magic.NONE)
                ms.load()
                result = ms.buffer(fheader)
                ms.close()
                self.info['magic_data'] = result
                return result

            else:
                logger.warning("The python-magic module is not available, but another module named magic was found.")

        except ImportError:
            logger.warning("The python-magic module is not available.")
        except AttributeError:
            logger.warning("The python-magic module is not available, but another module named magic was found.")
        return None  # returning None is better here, since we do not care about the exception in determine_fs_type

    def get_raw_path(self, include_self=False):
        """Retrieves the base mount path of the volume. Typically equals to :func:`Disk.get_fs_path` but may also be the
        path to a logical volume. This is used to determine the source path for a mount call.

        The value returned is normally based on the parent's paths, e.g. if this volume is mounted to a more specific
        path, only its children return the more specific path, this volume itself will keep returning the same path.
        This makes for consistent use of the offset attribute. If you do not need this behaviour, you can override this
        with the include_self argument.

        This behavior, however, is not retained for paths that directly affect the volume itself, not the child volumes.
        This includes VSS stores and LV volumes.
        """

        v = self
        if not include_self:
            # lv / vss_store are exceptions, as it covers the volume itself, not the child volume
            if v._paths.get('lv'):
                return v._paths['lv']
            elif v._paths.get('vss_store'):
                return v._paths['vss_store']
            elif v.parent and v.parent != self.disk:
                v = v.parent
            else:
                return self.disk.get_fs_path()

        while True:
            if v._paths.get('lv'):
                return v._paths['lv']
            elif v._paths.get('bde'):
                return v._paths['bde'] + '/bde1'
            elif v._paths.get('luks'):
                return '/dev/mapper/' + v._paths['luks']
            elif v._paths.get('md'):
                return v._paths['md']
            elif v._paths.get('vss_store'):
                return v._paths['vss_store']

            # Only if the volume has a parent that is not a disk, we try to check the parent for a location.
            if v.parent and v.parent != self.disk:
                v = v.parent
            else:
                break
        return self.disk.get_fs_path()

    def get_safe_label(self):
        """Returns a label that is safe to add to a path in the mountpoint for this volume."""

        if self.info.get('label') == '/':
            return 'root'

        suffix = re.sub(r"[/ \(\)]+", "_", self.info.get('label')) if self.info.get('label') else ""
        if suffix and suffix[0] == '_':
            suffix = suffix[1:]
        if len(suffix) > 2 and suffix[-1] == '_':
            suffix = suffix[:-1]
        return suffix

    @dependencies.require(dependencies.photorec)
    def carve(self, freespace=True):
        """Call this method to carve the free space of the volume for (deleted) files. Note that photorec has its
        own interface that temporarily takes over the shell.

        :param freespace: indicates whether the entire volume should be carved (False) or only the free space (True)
        :type freespace: bool
        :return: string to the path where carved data is available
        :raises CommandNotFoundError: if the underlying command does not exist
        :raises SubsystemError: if the underlying command fails
        :raises NoMountpointAvailableError: if there is no mountpoint available
        :raises NoLoopbackAvailableError: if there is no loopback available (only when volume has no slot number)
        """

        self._make_mountpoint(var_name='carve', suffix="carve", in_paths=True)

        # if no slot, we need to make a loopback that we can use to carve the volume
        loopback_was_created_for_carving = False
        if not self.slot:
            if not self.loopback:
                self._find_loopback()
                # Can't carve if volume has no slot number and can't be mounted on loopback.
                loopback_was_created_for_carving = True

            # noinspection PyBroadException
            try:
                _util.check_call_(["photorec", "/d", self._paths['carve'] + os.sep, "/cmd", self.loopback,
                                  ("freespace," if freespace else "") + "search"])

                # clean out the loop device if we created it specifically for carving
                if loopback_was_created_for_carving:
                    # noinspection PyBroadException
                    try:
                        _util.check_call_(['losetup', '-d', self.loopback])
                    except Exception:
                        pass
                    else:
                        self.loopback = ""

                return self._paths['carve']
            except Exception as e:
                logger.exception("Failed carving the volume.")
                raise SubsystemError(e)
        else:
            # noinspection PyBroadException
            try:
                _util.check_call_(["photorec", "/d", self._paths['carve'] + os.sep, "/cmd", self.get_raw_path(),
                                  str(self.slot) + (",freespace" if freespace else "") + ",search"])
                return self._paths['carve']

            except Exception as e:
                logger.exception("Failed carving the volume.")
                raise SubsystemError(e)

    @dependencies.require(dependencies.vshadowmount)
    def detect_volume_shadow_copies(self):
        """Method to call vshadowmount and mount NTFS volume shadow copies.

        :return: iterable with the :class:`Volume` objects of the VSS
        :raises CommandNotFoundError: if the underlying command does not exist
        :raises SubSystemError: if the underlying command fails
        :raises NoMountpointAvailableError: if there is no mountpoint available
        """

        self._make_mountpoint(var_name='vss', suffix="vss", in_paths=True)

        try:
            _util.check_call_(["vshadowmount", "-o", str(self.offset), self.get_raw_path(), self._paths['vss']])
        except Exception as e:
            logger.exception("Failed mounting the volume shadow copies.")
            raise SubsystemError(e)
        else:
            return self.volumes.detect_volumes(vstype='vss')

    def _should_mount(self, only_mount=None, skip_mount=None):
        """Indicates whether this volume should be mounted. Internal method, used by imount.py"""

        om = only_mount is None or \
            self.index in only_mount or \
            self.info.get('lastmountpoint') in only_mount or \
            self.info.get('label') in only_mount
        sm = skip_mount is None or \
            (self.index not in skip_mount and
             self.info.get('lastmountpoint') not in skip_mount and
             self.info.get('label') not in skip_mount)
        return om and sm

    def init(self, only_mount=None, skip_mount=None, swallow_exceptions=True):
        """Generator that mounts this volume and either yields itself or recursively generates its subvolumes.

        More specifically, this function will call :func:`load_fsstat_data` (iff *no_stats* is False), followed by
        :func:`mount`, followed by a call to :func:`detect_mountpoint`, after which ``self`` is yielded, or the result
        of the :func:`init` call on each subvolume is yielded

        :param only_mount: if specified, only volume indexes in this list are mounted. Volume indexes are strings.
        :param skip_mount: if specified, volume indexes in this list are not mounted.
        :param swallow_exceptions: if True, any error occuring when mounting the volume is swallowed and added as an
            exception attribute to the yielded objects.
        """
        if swallow_exceptions:
            self.exception = None

        try:
            if not self._should_mount(only_mount, skip_mount):
                yield self
                return

            if not self.init_volume():
                yield self
                return

        except ImageMounterError as e:
            if swallow_exceptions:
                self.exception = e
            else:
                raise

        if not self.volumes:
            yield self
        else:
            for v in self.volumes:
                for s in v.init(only_mount, skip_mount, swallow_exceptions):
                    yield s

    def init_volume(self, fstype=None):
        """Initializes a single volume. You should use this method instead of :func:`mount` if you want some sane checks
        before mounting.
        """

        logger.debug("Initializing volume {0}".format(self))

        if not self._should_mount():
            return False

        if self.flag != 'alloc':
            return False

        if self.info.get('raid_status') == 'waiting':
            logger.info("RAID array %s not ready for mounting", self)
            return False

        if self.is_mounted:
            logger.info("%s is currently mounted, not mounting it again", self)
            return False

        logger.info("Mounting volume {0}".format(self))
        self.mount(fstype=fstype)
        self.detect_mountpoint()

        return True

    def _make_mountpoint(self, casename=None, var_name='mountpoint', suffix='', in_paths=False):
        """Creates a directory that can be used as a mountpoint. The directory is stored in :attr:`mountpoint`,
        or the varname as specified by the argument. If in_paths is True, the path is stored in the :attr:`_paths`
        attribute instead.

        :returns: the mountpoint path
        :raises NoMountpointAvailableError: if no mountpoint could be made
        """
        parser = self.disk.parser

        if parser.mountdir and not os.path.exists(parser.mountdir):
            os.makedirs(parser.mountdir)

        if parser.pretty:
            md = parser.mountdir or tempfile.gettempdir()
            case_name = casename or self.disk.parser.casename or \
                ".".join(os.path.basename(self.disk.paths[0]).split('.')[0:-1]) or \
                os.path.basename(self.disk.paths[0])

            if self.disk.parser.casename == case_name:  # the casename is already in the path in this case
                pretty_label = "{0}-{1}".format(self.index, self.get_safe_label() or self.fstype or 'volume')
            else:
                pretty_label = "{0}-{1}-{2}".format(case_name, self.index,
                                                    self.get_safe_label() or self.fstype or 'volume')
            if suffix:
                pretty_label += "-" + suffix
            path = os.path.join(md, pretty_label)

            # check if path already exists, otherwise try to find another nice path
            if os.path.exists(path):
                for i in range(2, 100):
                    path = os.path.join(md, pretty_label + "-" + str(i))
                    if not os.path.exists(path):
                        break
                else:
                    logger.error("Could not find free mountdir.")
                    raise NoMountpointAvailableError()

            # noinspection PyBroadException
            try:
                os.mkdir(path, 777)
                if in_paths:
                    self._paths[var_name] = path
                else:
                    setattr(self, var_name, path)
                return path
            except Exception:
                logger.exception("Could not create mountdir.")
                raise NoMountpointAvailableError()
        else:
            t = tempfile.mkdtemp(prefix='im_' + self.index + '_',
                                 suffix='_' + self.get_safe_label() + ("_" + suffix if suffix else ""),
                                 dir=parser.mountdir)
            if in_paths:
                self._paths[var_name] = t
            else:
                setattr(self, var_name, t)
            return t

    def _clear_mountpoint(self):
        """Clears a created mountpoint. Does not unmount it, merely deletes it."""

        if self.mountpoint:
            os.rmdir(self.mountpoint)
            self.mountpoint = ""

    def _find_loopback(self, use_loopback=True, var_name='loopback'):
        """Finds a free loopback device that can be used. The loopback is stored in :attr:`loopback`. If *use_loopback*
        is True, the loopback will also be used directly.

        :returns: the loopback address
        :raises NoLoopbackAvailableError: if no loopback could be found
        """

        # noinspection PyBroadException
        try:
            loopback = _util.check_output_(['losetup', '-f']).strip()
            setattr(self, var_name, loopback)
        except Exception:
            logger.warning("No free loopback device found.", exc_info=True)
            raise NoLoopbackAvailableError()

        # noinspection PyBroadException
        if use_loopback:
            try:
                cmd = ['losetup', '-o', str(self.offset), '--sizelimit', str(self.size),
                       loopback, self.get_raw_path()]
                if not self.disk.read_write:
                    cmd.insert(1, '-r')
                _util.check_call_(cmd, stdout=subprocess.PIPE)
            except Exception:
                logger.exception("Loopback device could not be mounted.")
                raise NoLoopbackAvailableError()
        return loopback

    def _free_loopback(self, var_name='loopback'):
        if getattr(self, var_name):
            _util.check_call_(['losetup', '-d', getattr(self, var_name)], wrap_error=True)
            setattr(self, var_name, "")

    def determine_fs_type(self):
        """Determines the FS type for this partition. This function is used internally to determine which mount system
        to use, based on the file system description. Return values include *ext*, *ufs*, *ntfs*, *lvm* and *luks*.

        Note: does not do anything if fstype is already set to something sensible.
        """

        fstype_fallback = None
        if isinstance(self.fstype, filesystems.FallbackFileSystemType):
            fstype_fallback = self.fstype.fallback
        elif isinstance(self.fstype, filesystems.FileSystemType):
            return self.fstype

        result = collections.Counter()

        for source, description in (('fsdescription', self.info.get('fsdescription')),
                                    ('guid', self.info.get('guid')),
                                    ('blikid', self._get_blkid_type),
                                    ('magic', self._get_magic_type)):
            # For efficiency reasons, not all functions are called instantly.
            if callable(description):
                description = description()

            logger.debug("Trying to determine fs type from {} '{}'".format(source, description))
            if not description:
                continue

            # Iterate over all results and update the certainty of all FS types
            for type in FILE_SYSTEM_TYPES.values():
                result.update(type.detect(source, description))

            # Now sort the results by their certainty
            logger.debug("Current certainty levels: {}".format(result))

            # If we have not found any candidates, we continue
            if not result:
                continue

            # If we have candidates of which we are not entirely certain, we just continue
            max_res = result.most_common(1)[0][1]
            if max_res < 50:
                logger.debug("Highest certainty item is lower than 50, continuing...")
            # If we have multiple candidates with the same score, we just continue
            elif len([True for type, certainty in result.items() if certainty == max_res]) > 1:
                logger.debug("Multiple items with highest certainty level, so continuing...")
            else:
                self.fstype = result.most_common(1)[0][0]
                return self.fstype

        # Now be more lax with the fallback:
        if result:
            max_res = result.most_common(1)[0][1]
            if max_res > 0:
                self.fstype = result.most_common(1)[0][0]
                return self.fstype
        if fstype_fallback:
            self.fstype = fstype_fallback
            return self.fstype

    def mount(self, fstype=None):
        """Based on the file system type as determined by :func:`determine_fs_type`, the proper mount command is executed
        for this volume. The volume is mounted in a temporary path (or a pretty path if :attr:`pretty` is enabled) in
        the mountpoint as specified by :attr:`mountpoint`.

        If the file system type is a LUKS container or LVM, additional methods may be called, adding subvolumes to
        :attr:`volumes`

        :raises NotMountedError: if the parent volume/disk is not mounted
        :raises NoMountpointAvailableError: if no mountpoint was found
        :raises NoLoopbackAvailableError: if no loopback device was found
        :raises UnsupportedFilesystemError: if the fstype is not supported for mounting
        :raises SubsystemError: if one of the underlying commands failed
        """

        if not self.parent.is_mounted:
            raise NotMountedError(self.parent)

        if fstype is None:
            fstype = self.determine_fs_type()
        self._load_fsstat_data()

        # Prepare mount command
        try:
            fstype.mount(self)

            self.was_mounted = True
            self.is_mounted = True
            self.fstype = fstype

        except Exception as e:
            logger.exception("Execution failed due to {} {}".format(type(e), e), exc_info=True)
            if not isinstance(e, ImageMounterError):
                raise SubsystemError(e)
            else:
                raise

    def bindmount(self, mountpoint):
        """Bind mounts the volume to another mountpoint. Only works if the volume is already mounted.

        :raises NotMountedError: when the volume is not yet mounted
        :raises SubsystemError: when the underlying command failed
        """

        if not self.mountpoint:
            raise NotMountedError(self)
        try:
            _util.check_call_(['mount', '--bind', self.mountpoint, mountpoint], stdout=subprocess.PIPE)
            if 'bindmounts' in self._paths:
                self._paths['bindmounts'].append(mountpoint)
            else:
                self._paths['bindmounts'] = [mountpoint]
            return True
        except Exception as e:
            logger.exception("Error bind mounting {0}.".format(self))
            raise SubsystemError(e)

    def get_volumes(self):
        """Recursively gets a list of all subvolumes and the current volume."""

        if self.volumes:
            volumes = []
            for v in self.volumes:
                volumes.extend(v.get_volumes())
            volumes.append(self)
            return volumes
        else:
            return [self]

    @dependencies.require(dependencies.fsstat, none_on_failure=True)
    def _load_fsstat_data(self, timeout=3):
        """Using :command:`fsstat`, adds some additional information of the volume to the Volume."""

        def stats_thread():
            try:
                cmd = ['fsstat', self.get_raw_path(), '-o', str(self.offset // self.disk.block_size)]

                # Setting the fstype explicitly makes fsstat much faster and more reliable
                # In some versions, the auto-detect yaffs2 check takes ages for large images
                fstype = {
                    "ntfs": "ntfs", "fat": "fat", "ext": "ext", "iso": "iso9660", "hfs+": "hfs",
                    "ufs": "ufs", "swap": "swap", "exfat": "exfat",
                }.get(self.fstype, None)
                if fstype:
                    cmd.extend(["-f", fstype])

                logger.debug('$ {0}'.format(' '.join(cmd)))
                stats_thread.process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

                for line in iter(stats_thread.process.stdout.readline, b''):
                    line = line.decode('utf-8')
                    logger.debug('< {0}'.format(line))
                    if line.startswith("File System Type:"):
                        self.info['statfstype'] = line[line.index(':') + 2:].strip()
                    elif line.startswith("Last Mount Point:") or line.startswith("Last mounted on:"):
                        self.info['lastmountpoint'] = line[line.index(':') + 2:].strip().replace("//", "/")
                    elif line.startswith("Volume Name:") and not self.info.get('label'):
                        self.info['label'] = line[line.index(':') + 2:].strip()
                    elif line.startswith("Version:"):
                        self.info['version'] = line[line.index(':') + 2:].strip()
                    elif line.startswith("Source OS:"):
                        self.info['version'] = line[line.index(':') + 2:].strip()
                    elif 'CYLINDER GROUP INFORMATION' in line or 'BLOCK GROUP INFORMATION' in line:
                        # noinspection PyBroadException
                        try:
                            stats_thread.process.terminate()
                            logger.debug("Terminated fsstat at cylinder/block group information.")
                        except Exception:
                            pass
                        break

                if self.info.get('lastmountpoint') and self.info.get('label'):
                    self.info['label'] = "{0} ({1})".format(self.info['lastmountpoint'], self.info['label'])
                elif self.info.get('lastmountpoint') and not self.info.get('label'):
                    self.info['label'] = self.info['lastmountpoint']
                elif not self.info.get('lastmountpoint') and self.info.get('label') and \
                        self.info['label'].startswith("/"):  # e.g. /boot1
                    if self.info['label'].endswith("1"):
                        self.info['lastmountpoint'] = self.info['label'][:-1]
                    else:
                        self.info['lastmountpoint'] = self.info['label']

            except Exception:  # ignore any exceptions here.
                logger.exception("Error while obtaining stats.")

        stats_thread.process = None

        thread = threading.Thread(target=stats_thread)
        thread.start()
        thread.join(timeout)
        if thread.is_alive():
            # noinspection PyBroadException
            try:
                stats_thread.process.terminate()
            except Exception:
                pass
            thread.join()
            logger.debug("Killed fsstat after {0}s".format(timeout))

    def detect_mountpoint(self):
        """Attempts to detect the previous mountpoint if this was not done through :func:`load_fsstat_data`. This
        detection does some heuristic method on the mounted volume.
        """

        if self.info.get('lastmountpoint'):
            return self.info.get('lastmountpoint')
        if not self.mountpoint:
            return None

        result = None
        paths = os.listdir(self.mountpoint)
        if 'grub' in paths:
            result = '/boot'
        elif 'usr' in paths and 'var' in paths and 'root' in paths:
            result = '/'
        elif 'bin' in paths and 'lib' in paths and 'local' in paths and 'src' in paths and 'usr' not in paths:
            result = '/usr'
        elif 'bin' in paths and 'lib' in paths and 'local' not in paths and 'src' in paths and 'usr' not in paths:
            result = '/usr/local'
        elif 'lib' in paths and 'local' in paths and 'tmp' in paths and 'var' not in paths:
            result = '/var'
        # elif sum(['bin' in paths, 'boot' in paths, 'cdrom' in paths, 'dev' in paths, 'etc' in paths, 'home' in paths,
        #          'lib' in paths, 'lib64' in paths, 'media' in paths, 'mnt' in paths, 'opt' in paths,
        #          'proc' in paths, 'root' in paths, 'sbin' in paths, 'srv' in paths, 'sys' in paths, 'tmp' in paths,
        #          'usr' in paths, 'var' in paths]) > 11:
        #    result = '/'

        if result:
            self.info['lastmountpoint'] = result
            if not self.info.get('label'):
                self.info['label'] = self.info['lastmountpoint']
            logger.info("Detected mountpoint as {0} based on files in volume".format(self.info['lastmountpoint']))

        return result

    # noinspection PyBroadException
    def unmount(self, allow_lazy=False):
        """Unounts the volume from the filesystem.

        :raises SubsystemError: if one of the underlying processes fails
        :raises CleanupError: if the cleanup fails
        """

        for volume in self.volumes:
            try:
                volume.unmount(allow_lazy=allow_lazy)
            except ImageMounterError:
                pass

        if self.is_mounted:
            logger.info("Unmounting volume %s", self)

        if self.loopback and self.info.get('volume_group'):
            _util.check_call_(["lvm", 'vgchange', '-a', 'n', self.info['volume_group']],
                              wrap_error=True, stdout=subprocess.PIPE)
            self.info['volume_group'] = ""

        if self.loopback and self._paths.get('luks'):
            _util.check_call_(['cryptsetup', 'luksClose', self._paths['luks']], wrap_error=True, stdout=subprocess.PIPE)
            del self._paths['luks']

        if self._paths.get('bde'):
            try:
                _util.clean_unmount(['fusermount', '-u'], self._paths['bde'])
            except SubsystemError:
                if not allow_lazy:
                    raise
                _util.clean_unmount(['fusermount', '-uz'], self._paths['bde'])
            del self._paths['bde']

        if self._paths.get('md'):
            md_path = self._paths['md']
            del self._paths['md']  # removing it here to ensure we do not enter an infinite loop, will add it back later

            # MD arrays are a bit complicated, we also check all other volumes that are part of this array and
            # unmount them as well.
            logger.debug("All other volumes that use %s as well will also be unmounted", md_path)
            for v in self.disk.get_volumes():
                if v != self and v._paths.get('md') == md_path:
                    v.unmount(allow_lazy=allow_lazy)

            try:
                _util.check_output_(["mdadm", '--stop', md_path], stderr=subprocess.STDOUT)
            except Exception as e:
                self._paths['md'] = md_path
                raise SubsystemError(e)

        if self._paths.get('vss'):
            try:
                _util.clean_unmount(['fusermount', '-u'], self._paths['vss'])
            except SubsystemError:
                if not allow_lazy:
                    raise
                _util.clean_unmount(['fusermount', '-uz'], self._paths['vss'])
            del self._paths['vss']

        if self.loopback:
            _util.check_call_(['losetup', '-d', self.loopback], wrap_error=True)
            self.loopback = ""

        if self._paths.get('bindmounts'):
            for mp in self._paths['bindmounts']:
                _util.clean_unmount(['umount'], mp, rmdir=False)
            del self._paths['bindmounts']

        if self.mountpoint:
            _util.clean_unmount(['umount'], self.mountpoint)
            self.mountpoint = ""

        if self._paths.get('carve'):
            try:
                shutil.rmtree(self._paths['carve'])
            except OSError as e:
                raise SubsystemError(e)
            else:
                del self._paths['carve']

        self.is_mounted = False