diff --git a/bmaptools/BmapHelpers.py b/bmaptools/BmapHelpers.py index 790e713..878ed08 100644 --- a/bmaptools/BmapHelpers.py +++ b/bmaptools/BmapHelpers.py @@ -20,7 +20,16 @@ import os import struct +import subprocess from fcntl import ioctl +from subprocess import PIPE + +# Path to check for zfs compatibility. +ZFS_COMPAT_PARAM_PATH = '/sys/module/zfs/parameters/zfs_dmu_offset_next_sync' + +class Error(Exception): + """A class for all the other exceptions raised by this module.""" + pass def human_size(size): """Transform size in bytes into a human-readable form.""" @@ -83,3 +92,52 @@ def program_is_available(name): return True return False + +def get_file_system_type(path): + """Return the file system type for 'path'.""" + + abspath = os.path.realpath(path) + proc = subprocess.Popen(["df", "-T", "--", abspath], stdout=PIPE, stderr=PIPE) + stdout, stderr = proc.communicate() + + # Parse the output of subprocess, for example: + # Filesystem Type 1K-blocks Used Available Use% Mounted on + # rpool/USERDATA/foo_5ucog2 zfs 456499712 86956288 369543424 20% /home/foo + ftype = None + if stdout: + lines = stdout.splitlines() + if len(lines) >= 2: + fields = lines[1].split(None, 2) + if len(fields) >= 2: + ftype = fields[1].lower() + + if not ftype: + raise Error("failed to find file system type for path at '%s'\n" + "Here is the 'df -T' output\nstdout:\n%s\nstderr:\n%s" + % (path, stdout, stderr)) + return ftype + +def is_zfs_configuration_compatible(): + """Return if hosts zfs configuration is compatible.""" + + path = ZFS_COMPAT_PARAM_PATH + if not os.path.isfile(path): + return False + + try: + with open(path, "r") as fobj: + return int(fobj.readline()) == 1 + except IOError as err: + raise Error("cannot open zfs param path '%s': %s" + % (path, err)) + except ValueError as err: + raise Error("invalid value read from param path '%s': %s" + % (path, err)) + +def is_compatible_file_system(path): + """Return if paths file system is compatible.""" + + fstype = get_file_system_type(path) + if fstype == "zfs": + return is_zfs_configuration_compatible() + return True diff --git a/bmaptools/Filemap.py b/bmaptools/Filemap.py index e06e654..8831b2d 100644 --- a/bmaptools/Filemap.py +++ b/bmaptools/Filemap.py @@ -100,6 +100,11 @@ def __init__(self, image): raise Error("cannot synchronize image file '%s': %s " % (self._image_path, err.strerror)) + if not BmapHelpers.is_compatible_file_system(self._image_path): + fstype = BmapHelpers.get_file_system_type(self._image_path) + raise Error("image file on incompatible file system '%s': '%s': see docs for fix" + % (self._image_path, fstype)) + _log.debug("opened image \"%s\"" % self._image_path) _log.debug("block size %d, blocks count %d, image size %d" % (self.block_size, self.blocks_cnt, self.image_size)) diff --git a/docs/README b/docs/README index de2b551..a8a2c59 100644 --- a/docs/README +++ b/docs/README @@ -285,41 +285,42 @@ expand it. Later on, you may reconstruct it using the "bmaptool copy" command. Project structure ~~~~~~~~~~~~~~~~~ --------------------------------------------------------------------------------- -| - bmaptool | A tools to create bmap and copy with bmap. Based | -| | on the 'BmapCreate.py' and 'BmapCopy.py' modules. | -| - setup.py | A script to turn the entire bmap-tools project | -| | into a python egg. | -| - setup.cfg | contains a piece of nose tests configuration | -| - .coveragerc | lists files to include into test coverage report | -| - TODO | Just a list of things to be done for the project. | -| - make_a_release.sh | Most people may ignore this script. It is used by | -| | maintainer when creating a new release. | -| - tests/ | Contains the project unit-tests. | -| | - test_api_base.py | Tests the base API modules: 'BmapCreate.py' and | -| | | 'BmapCopy.py'. | -| | - test_filemap.py | Tests the 'Filemap.py' module. | -| | - test_compat.py | Tests that new BmapCopy implementations support old | -| | | bmap formats, and old BmapCopy implementations | -| | | support new compatible bmap fomrats. | -| | - helpers.py | Helper functions shared between the unit-tests. | -| | - test-data/ | Data files for the unit-tests | -| | - oldcodebase/ | Copies of old BmapCopy implementations for bmap | -| | | format forward-compatibility verification. | -| - bmaptools/ | The API modules which implement all the bmap | -| | | functionality. | -| | - BmapCreate.py | Creates a bmap for a given file. | -| | - BmapCopy.py | Implements copying of an image using its bmap. | -| | - Filemap.py | Allows for reading files' block map. | -| | - BmapHelpers.py | Just helper functions used all over the project. | -| | - TransRead.py | Provides a transparent way to read various kind of | -| | | files (compressed, etc) | -| - debian/* | Debian packaging for the project. | -| - doc/* | Project documentation. | -| - packaging/* | RPM packaging (Fedora & OpenSuse) for the project. | -| - contrib/* | Various contributions that may be useful, but | -| | project maintainers do not really test or maintain. | --------------------------------------------------------------------------------- +------------------------------------------------------------------------------------ +| - bmaptool | A tools to create bmap and copy with bmap. Based | +| | on the 'BmapCreate.py' and 'BmapCopy.py' modules. | +| - setup.py | A script to turn the entire bmap-tools project | +| | into a python egg. | +| - setup.cfg | contains a piece of nose tests configuration | +| - .coveragerc | lists files to include into test coverage report | +| - TODO | Just a list of things to be done for the project. | +| - make_a_release.sh | Most people may ignore this script. It is used by | +| | maintainer when creating a new release. | +| - tests/ | Contains the project unit-tests. | +| | - test_api_base.py | Tests the base API modules: 'BmapCreate.py' and | +| | | 'BmapCopy.py'. | +| | - test_filemap.py | Tests the 'Filemap.py' module. | +| | - test_compat.py | Tests that new BmapCopy implementations support old | +| | | bmap formats, and old BmapCopy implementations | +| | | support new compatible bmap fomrats. | +| | - test_bmap_helpers.py | Tests the 'BmapHelpers.py' module. | +| | - helpers.py | Helper functions shared between the unit-tests. | +| | - test-data/ | Data files for the unit-tests | +| | - oldcodebase/ | Copies of old BmapCopy implementations for bmap | +| | | format forward-compatibility verification. | +| - bmaptools/ | The API modules which implement all the bmap | +| | | functionality. | +| | - BmapCreate.py | Creates a bmap for a given file. | +| | - BmapCopy.py | Implements copying of an image using its bmap. | +| | - Filemap.py | Allows for reading files' block map. | +| | - BmapHelpers.py | Just helper functions used all over the project. | +| | - TransRead.py | Provides a transparent way to read various kind of | +| | | files (compressed, etc) | +| - debian/* | Debian packaging for the project. | +| - doc/* | Project documentation. | +| - packaging/* | RPM packaging (Fedora & OpenSuse) for the project. | +| - contrib/* | Various contributions that may be useful, but | +| | project maintainers do not really test or maintain. | +------------------------------------------------------------------------------------ How to run unit tests ~~~~~~~~~~~~~~~~~~~~~ diff --git a/requirements-test.txt b/requirements-test.txt index 5c26af4..1cc6bbb 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,2 +1,4 @@ six -nose \ No newline at end of file +nose +backports.tempfile +mock \ No newline at end of file diff --git a/tests/test_bmap_helpers.py b/tests/test_bmap_helpers.py new file mode 100644 index 0000000..8516164 --- /dev/null +++ b/tests/test_bmap_helpers.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +# vim: ts=4 sw=4 et ai si +# +# Copyright (c) 2012-2014 Intel, Inc. +# License: GPLv2 +# Author: Artem Bityutskiy +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License, version 2, +# as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. + +""" +This test verifies 'BmapHelpers' module functionality. +""" + +import os +import sys +import tempfile +from mock import patch, mock +from backports import tempfile as btempfile +from bmaptools import BmapHelpers + + +# This is a work-around for Centos 6 +try: + import unittest2 as unittest # pylint: disable=F0401 +except ImportError: + import unittest + + +class TestBmapHelpers(unittest.TestCase): + """The test class for these unit tests.""" + + def test_get_file_system_type(self): + """Check a file system type is returned when used with a file""" + + with tempfile.NamedTemporaryFile("r", prefix="testfile_", + delete=True, dir=".", suffix=".img") as fobj: + fstype = BmapHelpers.get_file_system_type(fobj.name) + self.assertTrue(fstype) + + def test_get_file_system_type_no_fstype_found(self): + """Check error raised when supplied file doesnt exist""" + + directory = os.path.dirname(__file__) + fobj = os.path.join(directory, "BmapHelpers/file/does/not/exist") + with self.assertRaises(BmapHelpers.Error): + BmapHelpers.get_file_system_type(fobj) + + def test_get_file_system_type_symlink(self): + """Check a file system type is returned when used with a symlink""" + + with btempfile.TemporaryDirectory(prefix="testdir_", dir=".") as directory: + fobj = tempfile.NamedTemporaryFile("r", prefix="testfile_", delete=False, + dir=directory, suffix=".img") + lnk = os.path.join(directory, "test_symlink") + os.symlink(fobj.name, lnk) + fstype = BmapHelpers.get_file_system_type(lnk) + self.assertTrue(fstype) + + def test_is_zfs_configuration_compatible_enabled(self): + """Check compatiblilty check is true when zfs param is set correctly""" + + with tempfile.NamedTemporaryFile("w+", prefix="testfile_", + delete=True, dir=".", suffix=".txt") as fobj: + fobj.write("1") + fobj.flush() + mockobj = mock.patch.object(BmapHelpers, "ZFS_COMPAT_PARAM_PATH", fobj.name) + with mockobj: + self.assertTrue(BmapHelpers.is_zfs_configuration_compatible()) + + + def test_is_zfs_configuration_compatible_disabled(self): + """Check compatiblilty check is false when zfs param is set incorrectly""" + + with tempfile.NamedTemporaryFile("w+", prefix="testfile_", + delete=True, dir=".", suffix=".txt") as fobj: + fobj.write("0") + fobj.flush() + mockobj = mock.patch.object(BmapHelpers, "ZFS_COMPAT_PARAM_PATH", fobj.name) + with mockobj: + self.assertFalse(BmapHelpers.is_zfs_configuration_compatible()) + + def test_is_zfs_configuration_compatible_invalid_read_value(self): + """Check error raised if any content of zfs config file invalid""" + + with tempfile.NamedTemporaryFile("a", prefix="testfile_", + delete=True, dir=".", suffix=".txt") as fobj: + mockobj = mock.patch.object(BmapHelpers, "ZFS_COMPAT_PARAM_PATH", fobj.name) + with self.assertRaises(BmapHelpers.Error): + with mockobj: + BmapHelpers.is_zfs_configuration_compatible() + + @patch("builtins.open" if sys.version_info[0] >= 3 else "__builtin__.open") + def test_is_zfs_configuration_compatible_unreadable_file(self, mock_open): + """Check error raised if any IO errors when checking zfs config file""" + + mock_open.side_effect = IOError + with self.assertRaises(BmapHelpers.Error): + BmapHelpers.is_zfs_configuration_compatible() + + def test_is_zfs_configuration_compatible_notinstalled(self): + """Check compatiblilty check passes when zfs not installed""" + + directory = os.path.dirname(__file__) + filepath = os.path.join(directory, "BmapHelpers/file/does/not/exist") + mockobj = mock.patch.object(BmapHelpers, "ZFS_COMPAT_PARAM_PATH", filepath) + with mockobj: + self.assertFalse(BmapHelpers.is_zfs_configuration_compatible()) + + @patch.object(BmapHelpers, "get_file_system_type", return_value="zfs") + def test_is_compatible_file_system_zfs_valid(self, mock_get_fs_type): #pylint: disable=unused-argument + """Check compatiblilty check passes when zfs param is set correctly""" + + with tempfile.NamedTemporaryFile("w+", prefix="testfile_", + delete=True, dir=".", suffix=".img") as fobj: + fobj.write("1") + fobj.flush() + mockobj = mock.patch.object(BmapHelpers, "ZFS_COMPAT_PARAM_PATH", fobj.name) + with mockobj: + self.assertTrue(BmapHelpers.is_compatible_file_system(fobj.name)) + + @patch.object(BmapHelpers, "get_file_system_type", return_value="zfs") + def test_is_compatible_file_system_zfs_invalid(self, mock_get_fs_type): #pylint: disable=unused-argument + """Check compatiblilty check fails when zfs param is set incorrectly""" + + with tempfile.NamedTemporaryFile("w+", prefix="testfile_", + delete=True, dir=".", suffix=".img") as fobj: + fobj.write("0") + fobj.flush() + mockobj = mock.patch.object(BmapHelpers, "ZFS_COMPAT_PARAM_PATH", fobj.name) + with mockobj: + self.assertFalse(BmapHelpers.is_compatible_file_system(fobj.name)) + + @patch.object(BmapHelpers, "get_file_system_type", return_value="ext4") + def test_is_compatible_file_system_ext4(self, mock_get_fs_type): #pylint: disable=unused-argument + """Check non-zfs file systems pass compatiblilty checks""" + + with tempfile.NamedTemporaryFile("w+", prefix="testfile_", + delete=True, dir=".", suffix=".img") as fobj: + self.assertTrue(BmapHelpers.is_compatible_file_system(fobj.name))