Changes are: * quick way to make (un)available resources in availability.py * all external resources are now constants (more uniform, makes testing easier) * have available and availabilityErrors in ExternalResource and Task, define available in terms of availabilityErrors, makes use in fs a bit less wordy * caching is done at the external resource level and at the PackageMethod level * make whether or not lvm actions in devicelibs are actually done dependent on whether lvm is available * track availability information in remaining bits of devicelibs files * add check to get_supported_raid_levels to take into account availability information * add a device type supported check in devicefactory * guard some test against device support being missing
From: mulhern amulhern@redhat.com
Related: #12
See commit 3b50bf259ab8e1ce98969cf15e672459bbef5e18for a further explanation.
Signed-off-by: mulhern amulhern@redhat.com --- blivet/formats/fslabeling.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-)
diff --git a/blivet/formats/fslabeling.py b/blivet/formats/fslabeling.py index ef5b4ae..0abeaf7 100644 --- a/blivet/formats/fslabeling.py +++ b/blivet/formats/fslabeling.py @@ -61,8 +61,8 @@ def labelingArgs(self, label):
class Ext2FSLabeling(FSLabeling):
- default_label = property(lambda s: "") - label_app = property(lambda s: fslabel.E2Label) + default_label = "" + label_app = fslabel.E2Label
def labelFormatOK(self, label): return len(label) < 17 @@ -72,8 +72,8 @@ def labelingArgs(self, label):
class FATFSLabeling(FSLabeling):
- default_label = property(lambda s: "NO NAME") - label_app = property(lambda s: fslabel.DosFsLabel) + default_label = "NO NAME" + label_app = fslabel.DosFsLabel
def labelFormatOK(self, label): return len(label) < 12 @@ -83,8 +83,8 @@ def labelingArgs(self, label):
class JFSLabeling(FSLabeling):
- default_label = property(lambda s: "") - label_app = property(lambda s: fslabel.JFSTune) + default_label = "" + label_app = fslabel.JFSTune
def labelFormatOK(self, label): return len(label) < 17 @@ -94,8 +94,8 @@ def labelingArgs(self, label):
class ReiserFSLabeling(FSLabeling):
- default_label = property(lambda s: "") - label_app = property(lambda s: fslabel.ReiserFSTune) + default_label = "" + label_app = fslabel.ReiserFSTune
def labelFormatOK(self, label): return len(label) < 17 @@ -105,8 +105,8 @@ def labelingArgs(self, label):
class XFSLabeling(FSLabeling):
- default_label = property(lambda s: "") - label_app = property(lambda s: fslabel.XFSAdmin) + default_label = "" + label_app = fslabel.XFSAdmin
def labelFormatOK(self, label): return ' ' not in label and len(label) < 13 @@ -116,8 +116,8 @@ def labelingArgs(self, label):
class HFSLabeling(FSLabeling):
- default_label = property(lambda s: "Untitled") - label_app = property(lambda s: None) + default_label = "Untitled" + label_app = None
def labelFormatOK(self, label): return ':' not in label and len(label) < 28 and len(label) > 0 @@ -127,8 +127,8 @@ def labelingArgs(self, label):
class HFSPlusLabeling(FSLabeling):
- default_label = property(lambda s: "Untitled") - label_app = property(lambda s: None) + default_label = "Untitled" + label_app = None
def labelFormatOK(self, label): return ':' not in label and 0 < len(label) < 129 @@ -138,8 +138,8 @@ def labelingArgs(self, label):
class NTFSLabeling(FSLabeling):
- default_label = property(lambda s: "") - label_app = property(lambda s: fslabel.NTFSLabel) + default_label = "" + label_app = fslabel.NTFSLabel
def labelFormatOK(self, label): return len(label) < 129
The commit referenced in the commit message no longer exists, the hash should be updated in the final PR before it gets merged.
No, it is there. There is a missing space between the 8 and the f, now fixed.
Great, thanks!
From: mulhern amulhern@redhat.com
Related: #12
After a few more patches they will be required to access instance variables.
Does not affect downstream, which always uses instance when invoking, anyway.
Signed-off-by: mulhern amulhern@redhat.com --- blivet/formats/__init__.py | 8 +++---- blivet/formats/fs.py | 15 +++++------- blivet/formats/swap.py | 6 ++--- tests/formats_test/labeling_test.py | 46 ++++++++++++++++++------------------- 4 files changed, 34 insertions(+), 41 deletions(-)
diff --git a/blivet/formats/__init__.py b/blivet/formats/__init__.py index d68f8ba..2acca22 100644 --- a/blivet/formats/__init__.py +++ b/blivet/formats/__init__.py @@ -221,13 +221,11 @@ def dict(self): "resizable": self.resizable} return d
- @classmethod - def labeling(cls): + def labeling(self): """Returns False by default since most formats are non-labeling.""" return False
- @classmethod - def labelFormatOK(cls, label): + def labelFormatOK(self, label): """Checks whether the format of the label is OK for whatever application is used by blivet to write a label for this format. If there is no application that blivet uses to write a label, @@ -239,7 +237,7 @@ def labelFormatOK(cls, label): :return: True if the format of the label is OK, otherwise False """ # pylint: disable=unused-argument - return cls.labeling() + return self.labeling()
def _setLabel(self, label): """Sets the label for this format. diff --git a/blivet/formats/fs.py b/blivet/formats/fs.py index dd282eb..a8b9558 100644 --- a/blivet/formats/fs.py +++ b/blivet/formats/fs.py @@ -151,25 +151,22 @@ def dict(self): "mountable": self.mountable}) return d
- @classmethod - def labeling(cls): + def labeling(self): """Returns True if this filesystem uses labels, otherwise False.
:rtype: bool """ - return cls._labelfs is not None + return self._labelfs is not None
- @classmethod - def relabels(cls): + def relabels(self): """Returns True if it is possible to relabel this filesystem after creation, otherwise False.
:rtype: bool """ - return cls._labelfs is not None and cls._labelfs.label_app is not None + return self._labelfs is not None and self._labelfs.label_app is not None
- @classmethod - def labelFormatOK(cls, label): + def labelFormatOK(self, label): """Return True if the label has an acceptable format for this filesystem. None, which represents accepting the default for this device, is always acceptable. @@ -177,7 +174,7 @@ def labelFormatOK(cls, label): :param label: A possible label :type label: str or None """ - return label is None or (cls._labelfs is not None and cls._labelfs.labelFormatOK(label)) + return label is None or (self._labelfs is not None and self._labelfs.labelFormatOK(label))
label = property(lambda s: s._getLabel(), lambda s,l: s._setLabel(l), doc="this filesystem's label") diff --git a/blivet/formats/swap.py b/blivet/formats/swap.py index 264aa2f..26ebd4f 100644 --- a/blivet/formats/swap.py +++ b/blivet/formats/swap.py @@ -80,13 +80,11 @@ def dict(self): d.update({"priority": self.priority, "label": self.label}) return d
- @classmethod - def labeling(cls): + def labeling(self): """Returns True as mkswap can write a label to the swap space.""" return True
- @classmethod - def labelFormatOK(cls, label): + def labelFormatOK(self, label): """Returns True since no known restrictions on the label.""" return True
diff --git a/tests/formats_test/labeling_test.py b/tests/formats_test/labeling_test.py index 82a61b7..dfa6e79 100755 --- a/tests/formats_test/labeling_test.py +++ b/tests/formats_test/labeling_test.py @@ -15,41 +15,41 @@ def testLabels(self): """Initialize some filesystems with valid and invalid labels."""
# Ext2FS has a maximum length of 16 - self.assertFalse(fs.Ext2FS.labelFormatOK("root___filesystem")) - self.assertTrue(fs.Ext2FS.labelFormatOK("root__filesystem")) + self.assertFalse(fs.Ext2FS().labelFormatOK("root___filesystem")) + self.assertTrue(fs.Ext2FS().labelFormatOK("root__filesystem"))
# FATFS has a maximum length of 11 - self.assertFalse(fs.FATFS.labelFormatOK("rtfilesystem")) - self.assertTrue(fs.FATFS.labelFormatOK("rfilesystem")) + self.assertFalse(fs.FATFS().labelFormatOK("rtfilesystem")) + self.assertTrue(fs.FATFS().labelFormatOK("rfilesystem"))
# JFS has a maximum length of 16 - self.assertFalse(fs.JFS.labelFormatOK("root___filesystem")) - self.assertTrue(fs.JFS.labelFormatOK("root__filesystem")) + self.assertFalse(fs.JFS().labelFormatOK("root___filesystem")) + self.assertTrue(fs.JFS().labelFormatOK("root__filesystem"))
# ReiserFS has a maximum length of 16 - self.assertFalse(fs.ReiserFS.labelFormatOK("root___filesystem")) - self.assertTrue(fs.ReiserFS.labelFormatOK("root__filesystem")) + self.assertFalse(fs.ReiserFS().labelFormatOK("root___filesystem")) + self.assertTrue(fs.ReiserFS().labelFormatOK("root__filesystem"))
#XFS has a maximum length 12 and does not allow spaces - self.assertFalse(fs.XFS.labelFormatOK("root_filesyst")) - self.assertFalse(fs.XFS.labelFormatOK("root file")) - self.assertTrue(fs.XFS.labelFormatOK("root_filesys")) + self.assertFalse(fs.XFS().labelFormatOK("root_filesyst")) + self.assertFalse(fs.XFS().labelFormatOK("root file")) + self.assertTrue(fs.XFS().labelFormatOK("root_filesys"))
#HFS has a maximum length of 27, minimum length of 1, and does not allow colons - self.assertFalse(fs.HFS.labelFormatOK("n" * 28)) - self.assertFalse(fs.HFS.labelFormatOK("root:file")) - self.assertFalse(fs.HFS.labelFormatOK("")) - self.assertTrue(fs.HFS.labelFormatOK("n" * 27)) + self.assertFalse(fs.HFS().labelFormatOK("n" * 28)) + self.assertFalse(fs.HFS().labelFormatOK("root:file")) + self.assertFalse(fs.HFS().labelFormatOK("")) + self.assertTrue(fs.HFS().labelFormatOK("n" * 27))
#HFSPlus has a maximum length of 128, minimum length of 1, and does not allow colons - self.assertFalse(fs.HFSPlus.labelFormatOK("n" * 129)) - self.assertFalse(fs.HFSPlus.labelFormatOK("root:file")) - self.assertFalse(fs.HFSPlus.labelFormatOK("")) - self.assertTrue(fs.HFSPlus.labelFormatOK("n" * 128)) + self.assertFalse(fs.HFSPlus().labelFormatOK("n" * 129)) + self.assertFalse(fs.HFSPlus().labelFormatOK("root:file")) + self.assertFalse(fs.HFSPlus().labelFormatOK("")) + self.assertTrue(fs.HFSPlus().labelFormatOK("n" * 128))
# NTFS has a maximum length of 128 - self.assertFalse(fs.NTFS.labelFormatOK("n" * 129)) - self.assertTrue(fs.NTFS.labelFormatOK("n" * 128)) + self.assertFalse(fs.NTFS().labelFormatOK("n" * 129)) + self.assertTrue(fs.NTFS().labelFormatOK("n" * 128))
# all devices are permitted to be passed a label argument of None # some will ignore it completely @@ -62,7 +62,7 @@ class MethodsTestCase(unittest.TestCase): def setUp(self): self.fs = {} for k, v in device_formats.items(): - if issubclass(v, fs.FS) and v.labeling(): + if issubclass(v, fs.FS) and v().labeling(): self.fs[k] = v(device="/dev", label="myfs")
@@ -87,7 +87,7 @@ def testGetLabelArgs(self): # all of the remaining are non-labeling so will accept any label label = "Houston, we have a problem!" for name, klass in device_formats.items(): - if issubclass(klass, fs.FS) and not klass.labeling() and not issubclass(klass, fs.NFS): + if issubclass(klass, fs.FS) and not klass().labeling() and not issubclass(klass, fs.NFS): self.assertEqual(klass(device="/dev", label=label).label, label, msg=name)
class XFSTestCase(fslabeling.CompleteLabelingAsRoot):
From: mulhern amulhern@redhat.com
Related: #12
Because labeling errors are sometimes not as severe as other errors, so we do want to distinguish.
Signed-off-by: mulhern amulhern@redhat.com --- blivet/errors.py | 6 ++++++ 1 file changed, 6 insertions(+)
diff --git a/blivet/errors.py b/blivet/errors.py index 4a31987..179c823 100644 --- a/blivet/errors.py +++ b/blivet/errors.py @@ -77,6 +77,12 @@ class MultipathMemberError(DeviceFormatError): class FSError(DeviceFormatError): pass
+class FSWriteLabelError(FSError): + pass + +class FSReadLabelError(FSError): + pass + class FSResizeError(FSError): def __init__(self, message, details): FSError.__init__(self, message)
No ``FSLabelError`` class added by the commit.
Commit fixed.
From: mulhern amulhern@redhat.com
Related: #12
This way, tasks can know what the kernel filesystems are if they need to. Avoids circular imports of filesystems, task, then filesystem again.
Signed-off-by: mulhern amulhern@redhat.com --- blivet/devicetree.py | 2 +- blivet/formats/fs.py | 14 ++------------ blivet/formats/fslib.py | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 13 deletions(-) create mode 100644 blivet/formats/fslib.py
diff --git a/blivet/devicetree.py b/blivet/devicetree.py index 8a58402..27df6b8 100644 --- a/blivet/devicetree.py +++ b/blivet/devicetree.py @@ -1015,7 +1015,7 @@ def handleNodevFilesystems(self): except ValueError: log.error("failed to parse /proc/mounts line: %s", line) continue - if fstype in formats.fs.nodev_filesystems: + if fstype in formats.fslib.nodev_filesystems: if not flags.include_nodev: continue
diff --git a/blivet/formats/fs.py b/blivet/formats/fs.py index a8b9558..6372a2b 100644 --- a/blivet/formats/fs.py +++ b/blivet/formats/fs.py @@ -41,21 +41,11 @@ from .. import udev from ..mounts import mountsCache
+from .fslib import kernel_filesystems, update_kernel_filesystems + import logging log = logging.getLogger("blivet")
-kernel_filesystems = [] -nodev_filesystems = [] - -def update_kernel_filesystems(): - for line in open("/proc/filesystems").readlines(): - fields = line.split() - kernel_filesystems.append(fields[-1]) - if fields[0] == "nodev": - nodev_filesystems.append(fields[-1]) - -update_kernel_filesystems() - class FS(DeviceFormat): """ Filesystem base class. """ _type = "Abstract Filesystem Class" # fs type name diff --git a/blivet/formats/fslib.py b/blivet/formats/fslib.py new file mode 100644 index 0000000..092439d --- /dev/null +++ b/blivet/formats/fslib.py @@ -0,0 +1,34 @@ +# fslib.py +# Library to support filesystem classes. +# +# Copyright (C) 2015 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Dave Lehman dlehman@redhat.com +# David Cantrell dcantrell@redhat.com +# Anne Mulhern amulhern@redhat.com + +kernel_filesystems = [] +nodev_filesystems = [] + +def update_kernel_filesystems(): + for line in open("/proc/filesystems").readlines(): + fields = line.split() + kernel_filesystems.append(fields[-1]) + if fields[0] == "nodev": + nodev_filesystems.append(fields[-1]) + +update_kernel_filesystems()
This should use ``with`` and iterate over the file instead of reading everything into memory first, but that's out of the scope of this commit (and the patch set).
Might as well while I'm here. Should probably encapsulate a bit better, too, but that is definitely outside the scope of the patch.
From: mulhern amulhern@redhat.com
Related: #12
Signed-off-by: mulhern amulhern@redhat.com --- blivet/tasks/__init__.py | 0 blivet/tasks/availability.py | 276 +++++++++++++++++++++++++++++++++++++++++++ blivet/tasks/task.py | 95 +++++++++++++++ python-blivet.spec | 2 + setup.py | 2 +- tests/pylint/runpylint.py | 2 + 6 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 blivet/tasks/__init__.py create mode 100644 blivet/tasks/availability.py create mode 100644 blivet/tasks/task.py
diff --git a/blivet/tasks/__init__.py b/blivet/tasks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blivet/tasks/availability.py b/blivet/tasks/availability.py new file mode 100644 index 0000000..4a35ef0 --- /dev/null +++ b/blivet/tasks/availability.py @@ -0,0 +1,276 @@ +# availability.py +# Class for tracking availability of an application. +# +# Copyright (C) 2014-2015 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Anne Mulhern amulhern@redhat.com + +import abc +from distutils.version import LooseVersion +import rpm + +from six import add_metaclass + +from gi.repository import BlockDev as blockdev + +from .. import util +from ..errors import AvailabilityError + +import logging +log = logging.getLogger("blivet") + +CACHE_AVAILABILITY = True + +class ExternalResource(object): + """ An external resource. """ + + def __init__(self, method, name): + """ Initializes an instance of an application. + + :param method: A method object + :type method: :class:`Method` + :param str name: the name of the external resource + """ + self._method = method + self.name = name + self._availabilityErrors = None + + def __str__(self): + return self.name + + @property + def availabilityErrors(self): + """ Whether the resource has any availability errors. + + :returns: [] if the resource is available + :rtype: list of str + """ + if self._availabilityErrors is None or not CACHE_AVAILABILITY: + self._availabilityErrors = self._method.availabilityErrors(self) + return self._availabilityErrors[:] + + @property + def available(self): + """ Whether the resource is available. + + :returns: True if the resource is available + :rtype: bool + """ + return self.availabilityErrors == [] + +@add_metaclass(abc.ABCMeta) +class Method(object): + """ Method for determining if external resource is available.""" + + @abc.abstractmethod + def availabilityErrors(self, resource): + """ Returns [] if the resource is available. + + :param resource: any external resource + :type resource: :class:`ExternalResource` + + :returns: [] if the external resource is available + :rtype: list of str + """ + raise NotImplementedError() + +class Path(Method): + """ Methods for when application is found in PATH. """ + + def availabilityErrors(self, resource): + """ Returns [] if the name of the application is in the path. + + :param resource: any application + :type resource: :class:`ExternalResource` + + :returns: [] if the name of the application is in the path + :rtype: list of str + """ + if not util.find_program_in_path(resource.name): + return ["application %s is not in $PATH" % resource.name] + else: + return [] + +Path = Path() + +class PackageInfo(object): + + def __init__(self, package_name, required_version=None): + """ Initializer. + + :param str package_name: the name of the package + :param required_version: the required version for this package + :type required_version: :class:`distutils.LooseVersion` or NoneType + """ + self.package_name = package_name + self.required_version = required_version + + def __str__(self): + return "%s-%s" % (self.package_name, self.required_version) + +class PackageMethod(Method): + """ Methods for checking the package version of the external resource. """ + + def __init__(self, package=None): + """ Initializer. + + :param :class:`PackageInfo` package: + """ + self.package = package + self._availabilityErrors = None + + @property + def packageVersion(self): + """ Returns the version of the package. + + :returns: the package version + :rtype: LooseVersion + """ + ts = rpm.TransactionSet() + info = ts.dbMatch("provides", self.package.package_name) + if info.count() == 0: + raise AvailabilityError("Could not determine package version for %s" % self.package.package_name) + return LooseVersion(info.next()['version']) + + def availabilityErrors(self, resource): + if self._availabilityErrors is not None and CACHE_AVAILABILITY: + return self._availabilityErrors[:] + + self._availabilityErrors = Path.availabilityErrors(resource) + + if self.package.required_version is None: + return self._availabilityErrors[:] + + try: + if self.packageVersion < self.package.required_version: + self._availabilityErrors.append("installed version %s for package %s is less than required version %s" % (self.packageVersion, self.package.package_name, self.package.required_version)) + except AvailabilityError as e: + # In contexts like the installer, a package may not be available, + # but the version of the tools is likely to be correct. + log.warning(str(e)) + + return self._availabilityErrors[:] + +class BlockDevMethod(Method): + """ Methods for when application is actually a libblockdev plugin. """ + + def availabilityErrors(self, resource): + """ Returns [] if the plugin is loaded. + + :param resource: a libblockdev plugin + :type resource: :class:`ExternalResource` + + :returns: [] if the name of the plugin is loaded + :rtype: list of str + """ + if resource.name in blockdev.get_available_plugin_names(): + return [] + else: + return ["libblockdev plugin %s not loaded" % resource.name] + +BlockDevMethod = BlockDevMethod() + +class UnavailableMethod(Method): + """ Method that indicates a resource is unavailable. """ + + def availabilityErrors(self, resource): + return ["always unavailable"] + +UnavailableMethod = UnavailableMethod() + +class AvailableMethod(Method): + """ Method that indicates a resource is available. """ + + def availabilityErrors(self, resource): + return [] + +AvailableMethod = AvailableMethod() + +def application(name): + """ Construct an external resource that is an application. + + This application will be available if its name can be found in $PATH. + """ + return ExternalResource(Path, name) + +def application_by_package(name, package_method): + """ Construct an external resource that is an application. + + This application will be available if its name can be found in $PATH + AND its package version is at least the required version. + + :param :class:`PackageMethod` package_method: the package method + """ + return ExternalResource(package_method, name) + +def blockdev_plugin(name): + """ Construct an external resource that is a libblockdev plugin. """ + return ExternalResource(BlockDevMethod, name) + +def unavailable_resource(name): + """ Construct an external resource that is always unavailable. """ + return ExternalResource(UnavailableMethod, name) + +def available_resource(name): + """ Construct an external resource that is always available. """ + return ExternalResource(AvailableMethod, name) + +# blockdev plugins +BLOCKDEV_BTRFS_PLUGIN = blockdev_plugin("btrfs") +BLOCKDEV_CRYPTO_PLUGIN = blockdev_plugin("crypto") +BLOCKDEV_DM_PLUGIN = blockdev_plugin("dm") +BLOCKDEV_LOOP_PLUGIN = blockdev_plugin("loop") +BLOCKDEV_LVM_PLUGIN = blockdev_plugin("lvm") +BLOCKDEV_MDRAID_PLUGIN = blockdev_plugin("mdraid") +BLOCKDEV_MPATH_PLUGIN = blockdev_plugin("mpath") +BLOCKDEV_SWAP_PLUGIN = blockdev_plugin("swap") + +# packages +E2FSPROGS_PACKAGE = PackageMethod(PackageInfo("e2fsprogs", LooseVersion("1.41.0"))) + +# applications +DEBUGREISERFS_APP = application("debugreiserfs") +DF_APP = application("df") +DOSFSCK_APP = application("dosfsck") +DOSFSLABEL_APP = application("dosfslabel") +DUMPE2FS_APP = application_by_package("dumpe2fs", E2FSPROGS_PACKAGE) +E2FSCK_APP = application_by_package("e2fsck", E2FSPROGS_PACKAGE) +E2LABEL_APP = application_by_package("e2label", E2FSPROGS_PACKAGE) +FSCK_HFSPLUS_APP = application("fsck.hfsplus") +HFORMAT_APP = application("hformat") +JFSTUNE_APP = application("jfs_tune") +KPARTX_APP = application("kpartx") +MKDOSFS_APP = application("mkdosfs") +MKE2FS_APP = application_by_package("mke2fs", E2FSPROGS_PACKAGE) +MKFS_BTRFS_APP = application("mkfs.btrfs") +MKFS_GFS2_APP = application("mkfs.gfs2") +MKFS_HFSPLUS_APP = application("mkfs.hfsplus") +MKFS_JFS_APP = application("mkfs.jfs") +MKFS_XFS_APP = application("mkfs.xfs") +MKNTFS_APP = application("mkntfs") +MKREISERFS_APP = application("mkreiserfs") +MULTIPATH_APP = application("multipath") +NTFSINFO_APP = application("ntfsinfo") +NTFSLABEL_APP = application("ntfslabel") +NTFSRESIZE_APP = application("ntfsresize") +REISERFSTUNE_APP = application("reiserfstune") +RESIZE2FS_APP = application_by_package("resize2fs", E2FSPROGS_PACKAGE) +XFSADMIN_APP = application("xfs_admin") +XFSDB_APP = application("xfs_db") +XFSFREEZE_APP = application("xfs_freeze") + +MOUNT_APP = application("mount") diff --git a/blivet/tasks/task.py b/blivet/tasks/task.py new file mode 100644 index 0000000..31e8dd4 --- /dev/null +++ b/blivet/tasks/task.py @@ -0,0 +1,95 @@ +# task.py +# Abstract class for tasks. +# +# Copyright (C) 2015 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Anne Mulhern amulhern@redhat.com + +import abc + +from six import add_metaclass + +@add_metaclass(abc.ABCMeta) +class Task(object): + """ An abstract class that represents some task. """ + + # Whether or not the functionality is implemented in the task class. + # It is True by default. Only NotImplementedClass and its descendants + # should have a False value. + implemented = True + + description = abc.abstractproperty(doc="Brief description for this task.") + + @property + def availabilityErrors(self): + """ Reasons if this task or the tasks it depends on are unavailable. """ + return self._availabilityErrors + \ + [e for t in self.dependsOn for e in t.availabilityErrors] + + @property + def available(self): + """ True if the task is available, otherwise False. + + :returns: True if the task is available + :rtype: bool + """ + return self.availabilityErrors == [] + + _availabilityErrors = abc.abstractproperty( + doc="Reasons if the necessary external tools are unavailable.") + + dependsOn = abc.abstractproperty(doc="tasks that this task depends on") + + @abc.abstractmethod + def doTask(self, *args, **kwargs): + """ Do the task for this class. """ + raise NotImplementedError() + +class UnimplementedTask(Task): + """ A null Task, which returns a negative or empty for all properties.""" + + description = "an unimplemented task" + implemented = False + + @property + def _availabilityErrors(self): + return ["Not implemented task can not succeed."] + + dependsOn = [] + + def doTask(self, *args, **kwargs): + raise NotImplementedError() + +@add_metaclass(abc.ABCMeta) +class BasicApplication(Task): + """ A task representing an application. """ + + ext = abc.abstractproperty(doc="The object representing the external resource.") + + # TASK methods + + @property + def _availabilityErrors(self): + errors = self.ext.availabilityErrors + if errors: + return ["application %s is not available: %s" % (self.ext, " and ".join(errors))] + else: + return [] + + @property + def dependsOn(self): + return [] diff --git a/python-blivet.spec b/python-blivet.spec index ef4bacb..9d499be 100644 --- a/python-blivet.spec +++ b/python-blivet.spec @@ -43,6 +43,7 @@ Requires: libselinux-python Requires: libblockdev >= %{libblockdevver} Requires: libblockdev-plugins-all >= %{libblockdevver} Requires: libselinux-python +Requires: rpm-python
%description The python-blivet package is a python module for examining and modifying @@ -64,6 +65,7 @@ Requires: util-linux >= %{utillinuxver} Requires: dosfstools Requires: e2fsprogs >= %{e2fsver} Requires: lsof +Requires: rpm-python3
%description -n python3-%{realname} The python3-%{realname} is a python3 package for examining and modifying storage diff --git a/setup.py b/setup.py index 9bb5831..f5f575d 100644 --- a/setup.py +++ b/setup.py @@ -36,4 +36,4 @@ def add_member_order_option(files): author='David Lehman', author_email='dlehman@redhat.com', url='http://fedoraproject.org/wiki/blivet', data_files=data_files, - packages=['blivet', 'blivet.devices', 'blivet.devicelibs', 'blivet.formats']) + packages=['blivet', 'blivet.devices', 'blivet.devicelibs', 'blivet.formats', 'blivet.tasks']) diff --git a/tests/pylint/runpylint.py b/tests/pylint/runpylint.py index 3fb7540..4abc9a2 100755 --- a/tests/pylint/runpylint.py +++ b/tests/pylint/runpylint.py @@ -14,6 +14,8 @@ def __init__(self): FalsePositive(r"(PartitioningTestCase|PartitionDeviceTestCase).*: Instance of 'DeviceFormat' has no .* member"), FalsePositive(r"Instance of 'int' has no .* member"), FalsePositive(r"Instance of 'LUKSDevice' has no .* member"), + FalsePositive(r"Method 'doTask' is abstract in class 'Task' but is not overridden"), + FalsePositive(r"Method 'doTask' is abstract in class 'UnimplementedTask' but is not overridden"), FalsePositive(r"No value for argument 'member_count' in unbound method call$"), FalsePositive(r"No value for argument 'smallest_member_size' in unbound method call$"),
In reply to line 42 of blivet/tasks/availability.py:
Still referring to **an application**
In reply to line 147 of blivet/tasks/availability.py:
Please check that this works with both Python 2 and Python 3 (I remember some issues with python-rpm and Python 3)
In reply to line 42 of blivet/tasks/availability.py:
fixed.
In reply to line 147 of blivet/tasks/availability.py:
ok, needed some modification. Now looks like:
``` ts = rpm.TransactionSet() info = ts.dbMatch("provides", self.package.package_name) if len(info) == 0: raise AvailabilityError("Could not determine package version for %s" % self.package.package_name) version = next(info)['version'] if six.PY3: version = version.decode() return LooseVersion(version) ```
In reply to line 147 of blivet/tasks/availability.py:
Looks good to me.
From: mulhern amulhern@redhat.com
Related: #12
Signed-off-by: mulhern amulhern@redhat.com --- blivet/errors.py | 4 ++++ 1 file changed, 4 insertions(+)
diff --git a/blivet/errors.py b/blivet/errors.py index 179c823..6677fce 100644 --- a/blivet/errors.py +++ b/blivet/errors.py @@ -194,3 +194,7 @@ class UnknownSourceDeviceError(StorageError): # factories class DeviceFactoryError(StorageError): pass + +class AvailabilityError(StorageError): + """ Raised if problem determining availability of external resource. """ + pass
From: mulhern amulhern@redhat.com
Related: #12
Signed-off-by: mulhern amulhern@redhat.com --- blivet/tasks/fsck.py | 157 +++++++++++++++++++++++++++++ blivet/tasks/fsinfo.py | 106 ++++++++++++++++++++ blivet/tasks/fslabeling.py | 98 ++++++++++++++++++ blivet/tasks/fsminsize.py | 187 +++++++++++++++++++++++++++++++++++ blivet/tasks/fsmkfs.py | 231 +++++++++++++++++++++++++++++++++++++++++++ blivet/tasks/fsmount.py | 198 +++++++++++++++++++++++++++++++++++++ blivet/tasks/fsreadlabel.py | 133 +++++++++++++++++++++++++ blivet/tasks/fsresize.py | 151 ++++++++++++++++++++++++++++ blivet/tasks/fssize.py | 166 +++++++++++++++++++++++++++++++ blivet/tasks/fssync.py | 89 +++++++++++++++++ blivet/tasks/fswritelabel.py | 116 ++++++++++++++++++++++ 11 files changed, 1632 insertions(+) create mode 100644 blivet/tasks/fsck.py create mode 100644 blivet/tasks/fsinfo.py create mode 100644 blivet/tasks/fslabeling.py create mode 100644 blivet/tasks/fsminsize.py create mode 100644 blivet/tasks/fsmkfs.py create mode 100644 blivet/tasks/fsmount.py create mode 100644 blivet/tasks/fsreadlabel.py create mode 100644 blivet/tasks/fsresize.py create mode 100644 blivet/tasks/fssize.py create mode 100644 blivet/tasks/fssync.py create mode 100644 blivet/tasks/fswritelabel.py
diff --git a/blivet/tasks/fsck.py b/blivet/tasks/fsck.py new file mode 100644 index 0000000..80d8661 --- /dev/null +++ b/blivet/tasks/fsck.py @@ -0,0 +1,157 @@ +# fsck.py +# Filesystem check functionality. +# +# Copyright (C) 2015 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Anne Mulhern amulhern@redhat.com + +import abc + +from six import add_metaclass + +from ..errors import FSError +from .. import util + +from . import availability +from . import task + +_UNKNOWN_RC_MSG = "Unknown return code: %d" + +@add_metaclass(abc.ABCMeta) +class FSCK(task.BasicApplication): + """An abstract class that represents actions associated with + checking consistency of a filesystem. + """ + description = "fsck" + + options = abc.abstractproperty( + doc="Options for invoking the application.") + + def __init__(self, an_fs): + """ Initializer. + + :param FS an_fs: a filesystem object + """ + self.fs = an_fs + + # IMPLEMENTATION methods + + @abc.abstractmethod + def _errorMessage(self, rc): + """ Error message corresponding to rc. + + :param int rc: the fsck program return code + :returns: an error message corresponding to the code, or None + :rtype: str or NoneType + + A return value of None indicates no error. + """ + raise NotImplementedError() + + @property + def _fsckCommand(self): + """The command to check the filesystem. + + :return: the command + :rtype: list of str + """ + return [str(self.ext)] + self.options + [self.fs.device] + + def doTask(self): + """ Check the filesystem. + + :raises FSError: on failure + """ + error_msgs = self.availabilityErrors + if error_msgs: + raise FSError("\n".join(error_msgs)) + + try: + rc = util.run_program(self._fsckCommand) + except OSError as e: + raise FSError("filesystem check failed: %s" % e) + + error_msg = self._errorMessage(rc) + if error_msg is not None: + hdr = "%(type)s filesystem check failure on %(device)s: " % \ + {"type": self.fs.type, "device": self.fs.device} + + raise FSError(hdr + error_msg) + + +class DosFSCK(FSCK): + _fsckErrors = {1: "Recoverable errors have been detected or dosfsck has " + "discovered an internal inconsistency.", + 2: "Usage error."} + + ext = availability.DOSFSCK_APP + options = ["-n"] + + def _errorMessage(self, rc): + if rc < 1: + return None + try: + return self._fsckErrors[rc] + except KeyError: + return _UNKNOWN_RC_MSG % rc + + +class Ext2FSCK(FSCK): + _fsckErrors = {4: "File system errors left uncorrected.", + 8: "Operational error.", + 16: "Usage or syntax error.", + 32: "e2fsck cancelled by user request.", + 128: "Shared library error."} + + ext = availability.E2FSCK_APP + options = ["-f", "-p", "-C", "0"] + + def _errorMessage(self, rc): + msgs = (self._fsckErrors[c] for c in self._fsckErrors.keys() if rc & c) + return "\n".join(msgs) or None + +class HFSPlusFSCK(FSCK): + _fsckErrors = {3: "Quick check found a dirty filesystem; no repairs done.", + 4: "Root filesystem was dirty. System should be rebooted.", + 8: "Corrupt filesystem, repairs did not succeed.", + 47: "Major error found; no repairs attempted."} + ext = availability.FSCK_HFSPLUS_APP + options = [] + + def _errorMessage(self, rc): + if rc < 1: + return None + try: + return self._fsckErrors[rc] + except KeyError: + return _UNKNOWN_RC_MSG % rc + +class NTFSFSCK(FSCK): + ext = availability.NTFSRESIZE_APP + options = ["-c"] + + def _errorMessage(self, rc): + return _UNKNOWN_RC_MSG % (rc,) if rc != 0 else None + +class UnimplementedFSCK(task.UnimplementedTask): + + def __init__(self, an_fs): + """ Initializer. + + :param FS an_fs: a filesystem object + """ + self.fs = an_fs diff --git a/blivet/tasks/fsinfo.py b/blivet/tasks/fsinfo.py new file mode 100644 index 0000000..13ed555 --- /dev/null +++ b/blivet/tasks/fsinfo.py @@ -0,0 +1,106 @@ +# fsinfo.py +# Filesystem information gathering classes. +# +# Copyright (C) 2015 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Anne Mulhern amulhern@redhat.com + +import abc + +from six import add_metaclass + +from ..errors import FSError +from .. import util + +from . import availability +from . import task + +@add_metaclass(abc.ABCMeta) +class FSInfo(task.BasicApplication): + """ An abstract class that represents an information gathering app. """ + + description = "filesystem info" + + options = abc.abstractproperty( + doc="Options for invoking the application.") + + def __init__(self, an_fs): + """ Initializer. + + :param FS an_fs: a filesystem object + """ + self.fs = an_fs + + @property + def _infoCommand(self): + """ Returns the command for reading filesystem information. + + :returns: a list of appropriate options + :rtype: list of str + """ + return [str(self.ext)] + self.options + [self.fs.device] + + def doTask(self): + """ Returns information from the command. + + :returns: a string representing the output of the command + :rtype: str + :raises FSError: if info cannot be obtained + """ + error_msgs = self.availabilityErrors + if error_msgs: + raise FSError("\n".join(error_msgs)) + + error_msg = None + try: + (rc, out) = util.run_program_and_capture_output(self._infoCommand) + if rc: + error_msg = "failed to gather fs info: %s" % rc + except OSError as e: + error_msg = "failed to gather fs info: %s" % e + if error_msg: + raise FSError(error_msg) + return out + +class Ext2FSInfo(FSInfo): + ext = availability.DUMPE2FS_APP + options = ["-h"] + +class JFSInfo(FSInfo): + ext = availability.JFSTUNE_APP + options = ["-l"] + +class NTFSInfo(FSInfo): + ext = availability.NTFSINFO_APP + options = ["-m"] + +class ReiserFSInfo(FSInfo): + ext = availability.DEBUGREISERFS_APP + options = [] + +class XFSInfo(FSInfo): + ext = availability.XFSDB_APP + options = ["-c", "sb 0", "-c", "p dblocks", "-c", "p blocksize"] + +class UnimplementedFSInfo(task.UnimplementedTask): + + def __init__(self, an_fs): + """ Initializer. + + :param FS an_fs: a filesystem object + """ + self.fs = an_fs diff --git a/blivet/tasks/fslabeling.py b/blivet/tasks/fslabeling.py new file mode 100644 index 0000000..b0a4c7e --- /dev/null +++ b/blivet/tasks/fslabeling.py @@ -0,0 +1,98 @@ +# fslabeling.py +# Filesystem labeling classes for anaconda's storage configuration module. +# +# Copyright (C) 2014 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Anne Mulhern amulhern@redhat.com + +import abc + +from six import add_metaclass + +@add_metaclass(abc.ABCMeta) +class FSLabeling(object): + """An abstract class that represents filesystem labeling actions. + """ + + default_label = abc.abstractproperty( + doc="Default label set on this filesystem at creation.") + + @abc.abstractmethod + def labelFormatOK(self, label): + """Returns True if this label is correctly formatted for this + filesystem, otherwise False. + + :param str label: the label for this filesystem + :rtype: bool + """ + raise NotImplementedError + +class Ext2FSLabeling(FSLabeling): + + default_label = "" + + def labelFormatOK(self, label): + return len(label) < 17 + +class FATFSLabeling(FSLabeling): + + default_label = "NO NAME" + + def labelFormatOK(self, label): + return len(label) < 12 + +class JFSLabeling(FSLabeling): + + default_label = "" + + def labelFormatOK(self, label): + return len(label) < 17 + +class ReiserFSLabeling(FSLabeling): + + default_label = "" + + def labelFormatOK(self, label): + return len(label) < 17 + +class XFSLabeling(FSLabeling): + + default_label = "" + + def labelFormatOK(self, label): + return ' ' not in label and len(label) < 13 + +class HFSLabeling(FSLabeling): + + default_label = "Untitled" + + def labelFormatOK(self, label): + return ':' not in label and len(label) < 28 and len(label) > 0 + +class HFSPlusLabeling(FSLabeling): + + default_label = "Untitled" + + def labelFormatOK(self, label): + return ':' not in label and 0 < len(label) < 129 + +class NTFSLabeling(FSLabeling): + + default_label = "" + + def labelFormatOK(self, label): + return len(label) < 129 diff --git a/blivet/tasks/fsminsize.py b/blivet/tasks/fsminsize.py new file mode 100644 index 0000000..5044190 --- /dev/null +++ b/blivet/tasks/fsminsize.py @@ -0,0 +1,187 @@ +# fsminsize.py +# Filesystem size gathering classes. +# +# Copyright (C) 2015 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Anne Mulhern amulhern@redhat.com + +import abc + +from six import add_metaclass + +from ..errors import FSError +from .. import util +from ..size import Size + +from . import availability +from . import task + +@add_metaclass(abc.ABCMeta) +class FSMinSize(task.BasicApplication): + """ An abstract class that represents min size information extraction. """ + + description = "minimum filesystem size" + + options = abc.abstractproperty(doc="Options for use with app.") + + def __init__(self, an_fs): + """ Initializer. + + :param FS an_fs: a filesystem object + """ + self.fs = an_fs + + def _resizeCommand(self): + return [str(self.ext)] + self.options + [self.fs.device] + + def _getResizeInfo(self): + """ Get info from fsresize program. + + :rtype: str + :returns: output returned by fsresize program + """ + error_msg = None + try: + (rc, out) = util.run_program_and_capture_output(self._resizeCommand()) + except OSError as e: + error_msg = "failed to gather info from resize program: %s" % e + if rc: + error_msg = "failed to gather info from resize program: %s" % e + + if error_msg: + raise FSError(error_msg) + return out + + @abc.abstractmethod + def doTask(self): + """ Returns the minimum size for this filesystem object. + + :rtype: :class:`~.size.Size` + :returns: the minimum size + :raises FSError: if filesystem can not be obtained + """ + raise NotImplementedError() + +class Ext2FSMinSize(FSMinSize): + + ext = availability.RESIZE2FS_APP + options = ["-P"] + + @property + def dependsOn(self): + return [self.fs._info] + + def _extractBlockSize(self): + """ Extract block size from filesystem info. + + :returns: block size of fileystem or None + :rtype: :class:`~.size.Size` or NoneType + """ + if self.fs._current_info is None: + return None + + blockSize = None + for line in (l.strip() for l in self.fs._current_info.splitlines() if l.startswith("Block size:")): + try: + blockSize = int(line.split(" ")[-1]) + break + except ValueError: + continue + + return Size(blockSize) if blockSize else None + + def _extractNumBlocks(self, info): + """ Extract the number of blocks from the resizefs info. + + :returns: the number of blocks or None + :rtype: int or NoneType + """ + numBlocks = None + for line in info.splitlines(): + (text, _sep, value) = line.partition(":") + if "minimum size of the filesystem" not in text: + continue + + try: + numBlocks = int(value.strip()) + break + except ValueError: + break + + return numBlocks + + def doTask(self): + error_msgs = self.availabilityErrors + if error_msgs: + raise FSError("\n".join(error_msgs)) + + blockSize = self._extractBlockSize() + if blockSize is None: + raise FSError("failed to get block size for %s filesystem on %s" % (self.fs.mountType, self.fs.device.name)) + + resize_info = self._getResizeInfo() + numBlocks = self._extractNumBlocks(resize_info) + if numBlocks is None: + raise FSError("failed to get minimum block number for %s filesystem on %s" % (self.fs.mountType, self.fs.device.name)) + + return blockSize * numBlocks + +class NTFSMinSize(FSMinSize): + + ext = availability.NTFSRESIZE_APP + options = ["-m"] + + def _extractMinSize(self, info): + """ Extract the minimum size from the resizefs info. + + :param str info: info obtained from resizefs prog + :rtype: :class:`~.size.Size` or NoneType + :returns: the minimum size, or None + """ + minSize = None + for line in info.splitlines(): + (text, _sep, value) = line.partition(":") + if "Minsize" not in text: + continue + + try: + minSize = Size("%d MB" % int(value.strip())) + except ValueError: + break + + return minSize + + + def doTask(self): + error_msgs = self.availabilityErrors + if error_msgs: + raise FSError("\n".join(error_msgs)) + + resize_info = self._getResizeInfo() + minSize = self._extractMinSize(resize_info) + if minSize is None: + raise FSError("Unable to discover minimum size of filesystem on %s" % self.fs.device) + return minSize + +class UnimplementedFSMinSize(task.UnimplementedTask): + + def __init__(self, an_fs): + """ Initializer. + + :param FS an_fs: a filesystem object + """ + self.fs = an_fs diff --git a/blivet/tasks/fsmkfs.py b/blivet/tasks/fsmkfs.py new file mode 100644 index 0000000..a2110a9 --- /dev/null +++ b/blivet/tasks/fsmkfs.py @@ -0,0 +1,231 @@ +# fsmkfs.py +# Filesystem formatting classes. +# +# Copyright (C) 2015 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Anne Mulhern amulhern@redhat.com + +import abc + +from six import add_metaclass + +from ..errors import FSError, FSWriteLabelError +from .. import util + +from . import availability +from . import task + +@add_metaclass(abc.ABCMeta) +class FSMkfsTask(task.Task): + + canLabel = abc.abstractproperty(doc="whether this task labels") + +@add_metaclass(abc.ABCMeta) +class FSMkfs(task.BasicApplication, FSMkfsTask): + """An abstract class that represents filesystem creation actions. """ + description = "mkfs" + + label_option = abc.abstractproperty( + doc="Option for setting a filesystem label.") + + args = abc.abstractproperty(doc="options for creating filesystem") + + def __init__(self, an_fs): + """ Initializer. + + :param FS an_fs: a filesystem object + """ + self.fs = an_fs + + # IMPLEMENTATION methods + + @property + def canLabel(self): + """ Whether this task can label the filesystem. + + :returns: True if this task can label the filesystem + :rtype: bool + """ + return self.label_option is not None + + @property + def _labelOptions(self): + """ Any labeling options that a particular filesystem may use. + + :returns: labeling options + :rtype: list of str + """ + # Do not know how to set label while formatting. + if self.label_option is None: + return [] + + # No label to set + if self.fs.label is None: + return [] + + if self.fs.labelFormatOK(self.fs.label): + return [self.label_option, self.fs.label] + else: + raise FSWriteLabelError("Choosing not to apply label (%s) during creation of filesystem %s. Label format is unacceptable for this filesystem." % (self.fs.label, self.fs.type)) + + def _formatOptions(self, options=None, label=False): + """Get a list of format options to be used when creating the + filesystem. + + :param options: any special options + :type options: list of str or None + :param bool label: if True, label if possible, default is False + """ + options = options or [] + + if not isinstance(options, list): + raise FSError("options parameter must be a list.") + + label_options = self._labelOptions if label else [] + return options + self.args + label_options + [self.fs.device] + + def _mkfsCommand(self, options, label): + """Return the command to make the filesystem. + + :param options: any special options + :type options: list of str or None + :returns: the mkfs command + :rtype: list of str + """ + return [str(self.ext)] + self._formatOptions(options, label) + + def doTask(self, options=None, label=False): + """Create the format on the device and label if possible and desired. + + :param options: any special options, may be None + :type options: list of str or NoneType + :param bool label: whether to label while creating, default is False + """ + # pylint: disable=arguments-differ + error_msgs = self.availabilityErrors + if error_msgs: + raise FSError("\n".join(error_msgs)) + + options = options or [] + try: + ret = util.run_program(self._mkfsCommand(options, label)) + except OSError as e: + raise FSError(e) + + if ret: + raise FSError("format failed: %s" % ret) + +class BTRFSMkfs(FSMkfs): + ext = availability.MKFS_BTRFS_APP + label_option = None + + @property + def args(self): + return [] + +class Ext2FSMkfs(FSMkfs): + ext = availability.MKE2FS_APP + label_option = "-L" + + _opts = [] + + @property + def args(self): + return self._opts + (["-T", self.fs.fsprofile] if self.fs.fsprofile else []) + +class Ext3FSMkfs(Ext2FSMkfs): + _opts = ["-t", "ext3"] + +class Ext4FSMkfs(Ext3FSMkfs): + _opts = ["-t", "ext4"] + +class FATFSMkfs(FSMkfs): + ext = availability.MKDOSFS_APP + label_option = "-n" + + @property + def args(self): + return [] + +class GFS2Mkfs(FSMkfs): + ext = availability.MKFS_GFS2_APP + label_option = None + + @property + def args(self): + return ["-j", "1", "-p", "lock_nolock", "-O"] + +class HFSMkfs(FSMkfs): + ext = availability.HFORMAT_APP + label_option = "-l" + + @property + def args(self): + return [] + +class HFSPlusMkfs(FSMkfs): + ext = availability.MKFS_HFSPLUS_APP + label_option = "-v" + + @property + def args(self): + return [] + +class JFSMkfs(FSMkfs): + ext = availability.MKFS_JFS_APP + label_option = "-L" + + @property + def args(self): + return ["-q"] + +class NTFSMkfs(FSMkfs): + ext = availability.MKNTFS_APP + label_option = "-L" + + @property + def args(self): + return [] + +class ReiserFSMkfs(FSMkfs): + ext = availability.MKREISERFS_APP + label_option = "-l" + + @property + def args(self): + return ["-f", "-f"] + +class XFSMkfs(FSMkfs): + ext = availability.MKFS_XFS_APP + label_option = "-L" + + @property + def args(self): + return ["-f"] + +class UnimplementedFSMkfs(task.UnimplementedTask, FSMkfsTask): + + def __init__(self, an_fs): + """ Initializer. + + :param FS an_fs: a filesystem object + """ + self.fs = an_fs + + @property + def canLabel(self): + return False diff --git a/blivet/tasks/fsmount.py b/blivet/tasks/fsmount.py new file mode 100644 index 0000000..552faed --- /dev/null +++ b/blivet/tasks/fsmount.py @@ -0,0 +1,198 @@ +# fsmount.py +# Filesystem mounting classes. +# +# Copyright (C) 2015 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Anne Mulhern amulhern@redhat.com + +import os + +from ..errors import FSError +from ..flags import flags +from .. import util +from ..formats import fslib + +from . import availability +from . import task + +class FSMount(task.BasicApplication): + """An abstract class that represents filesystem mounting actions. """ + description = "mount a filesystem" + + options = ["defaults"] + # type argument to pass to mount, if different from filesystem type + fstype = None + + ext = availability.MOUNT_APP + + def __init__(self, an_fs): + self.fs = an_fs + + # TASK methods + + @property + def _hasDriver(self): + """ Is there a filesystem driver in the kernel modules directory. """ + modpath = os.path.realpath(os.path.join("/lib/modules", os.uname()[2])) + if os.path.isdir(modpath): + modname = "%s.ko" % self.mountType + for _root, _dirs, files in os.walk(modpath): + if any(x.startswith(modname) for x in files): + return True + return False + + @property + def _canMount(self): + return (self.mountType in fslib.kernel_filesystems) or \ + (os.access("/sbin/mount.%s" % (self.mountType,), os.X_OK)) or \ + self._hasDriver + + @property + def _availabilityErrors(self): + errors = super(FSMount, self)._availabilityErrors + + if not self._canMount: + errors.append("mounting filesystem %s is not supported" % self.mountType) + return errors + + # IMPLEMENTATION methods + + @property + def mountType(self): + """ Mount type string to pass to mount command. + + :returns: mount type string + :rtype: str + """ + return self.fstype or self.fs._type + + def _modifyOptions(self, options): + """ Any mandatory options can be added in this method. + + :param str options: an option string + :returns: a modified option string + :rtype: str + """ + return options + + def mountOptions(self, options): + """ The options used for mounting. + + :param options: mount options + :type options: str or NoneType + :returns: the options used by the task + :rtype: str + """ + if not options or not isinstance(options, str): + options = self.fs.mountopts or ",".join(self.options) + + return self._modifyOptions(options) + + def doTask(self, mountpoint, options=None): + """Create the format on the device and label if possible and desired. + + :param str mountpoint: mountpoint that overrides self.mountpoint + :param options: mount options + :type options: str or None + """ + # pylint: disable=arguments-differ + error_msgs = self.availabilityErrors + if error_msgs: + raise FSError("\n".join(error_msgs)) + + try: + rc = util.mount(self.fs.device, mountpoint, + fstype=self.mountType, + options=self.mountOptions(options)) + except OSError as e: + raise FSError("mount failed: %s" % e) + + if rc: + raise FSError("mount failed: %s" % rc) + +class AppleBootstrapFSMount(FSMount): + fstype = "hfs" + +class BindFSMount(FSMount): + + @property + def _availabilityErrors(self): + errors = [] + if not self.ext.available: + errors.append("application %s is not available" % self.ext) + + return errors + + def _modifyOptions(self, options): + return ",".join(["bind", options]) + +class DevPtsFSMount(FSMount): + options = ["gid=5", "mode=620"] + +class FATFSMount(FSMount): + options = ["umask=0077", "shortname=winnt"] + +class EFIFSMount(FATFSMount): + fstype = "vfat" + +class HFSPlusMount(FSMount): + fstype = "hfsplus" + +class Iso9660FSMount(FSMount): + options = ["ro"] + +class NoDevFSMount(FSMount): + + @property + def mountType(self): + return self.fs.device + +class NFSMount(FSMount): + + def _availabilityErrors(self): + return ["nfs filesystem can't be mounted"] + +class NTFSMount(FSMount): + options = ["default", "ro"] + +class SELinuxFSMount(NoDevFSMount): + + @property + def _availabilityErrors(self): + errors = super(SELinuxFSMount, self)._availabilityErrors + if not flags.selinux: + errors.append("selinux not enabled") + return errors + +class TmpFSMount(NoDevFSMount): + + def _modifyOptions(self, options): + # This duplicates some code in fs.TmpFS._getOptions. + # There seems to be no way around that. + if self.fs._accept_default_size: + size_opt = None + else: + size_opt = self.fs._sizeOption(self.fs._size) + return ",".join(o for o in (options, size_opt) if o) + + @property + def _availabilityErrors(self): + errors = [] + if not self.ext.available: + errors.append("application %s is not available" % self.ext) + + return errors diff --git a/blivet/tasks/fsreadlabel.py b/blivet/tasks/fsreadlabel.py new file mode 100644 index 0000000..59e3e12 --- /dev/null +++ b/blivet/tasks/fsreadlabel.py @@ -0,0 +1,133 @@ +# fsreadlabel.py +# Filesystem label reading classes. +# +# Copyright (C) 2015 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Anne Mulhern amulhern@redhat.com + +import abc +import re + +from six import add_metaclass + +from ..errors import FSReadLabelError +from .. import util + +from . import availability +from . import task + +@add_metaclass(abc.ABCMeta) +class FSReadLabel(task.BasicApplication): + """ An abstract class that represents reading a filesystem's label. """ + description = "read filesystem label" + + label_regex = abc.abstractproperty( + doc="Matches the string output by the reading application.") + + args = abc.abstractproperty(doc="arguments for reading a label.") + + def __init__(self, an_fs): + """ Initializer. + + :param FS an_fs: a filesystem object + """ + self.fs = an_fs + + # IMPLEMENTATION methods + + @property + def _readCommand(self): + """Get the command to read the filesystem label. + + :return: the command + :rtype: list of str + """ + return [str(self.ext)] + self.args + + def _extractLabel(self, labelstr): + """Extract the label from an output string. + + :param str labelstr: the string containing the label information + + :return: the label + :rtype: str + + Raises an FSReadLabelError if the label can not be extracted. + """ + match = re.match(self.label_regex, labelstr) + if match is None: + raise FSReadLabelError("Unknown format for application %s" % self.ext) + return match.group('label') + + def doTask(self): + """ Get the label. + + :returns: the filesystem label + :rtype: str + """ + error_msgs = self.availabilityErrors + if error_msgs: + raise FSReadLabelError("\n".join(error_msgs)) + + (rc, out) = util.run_program_and_capture_output(self._readCommand) + if rc != 0: + raise FSReadLabelError("read label failed") + + label = out.strip() + + return label if label == "" else self._extractLabel(label) + +class DosFSReadLabel(FSReadLabel): + ext = availability.DOSFSLABEL_APP + label_regex = r'(?P<label>.*)' + + @property + def args(self): + return [self.fs.device] + +class Ext2FSReadLabel(FSReadLabel): + ext = availability.E2LABEL_APP + label_regex = r'(?P<label>.*)' + + @property + def args(self): + return [self.fs.device] + +class NTFSReadLabel(FSReadLabel): + ext = availability.NTFSLABEL_APP + label_regex = r'(?P<label>.*)' + + @property + def args(self): + return [self.fs.device] + +class XFSReadLabel(FSReadLabel): + ext = availability.XFSADMIN_APP + label_regex = r'label = "(?P<label>.*)"' + + @property + def args(self): + return ["-l", self.fs.device] + +class UnimplementedFSReadLabel(task.UnimplementedTask): + + def __init__(self, an_fs): + """ Initializer. + + :param FS an_fs: a filesystem object + """ + self.fs = an_fs diff --git a/blivet/tasks/fsresize.py b/blivet/tasks/fsresize.py new file mode 100644 index 0000000..ef12686 --- /dev/null +++ b/blivet/tasks/fsresize.py @@ -0,0 +1,151 @@ +# fsresize.py +# Filesystem resizing classes. +# +# Copyright (C) 2015 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Anne Mulhern amulhern@redhat.com + +import abc + +from six import add_metaclass + +from ..errors import FSError +from ..size import B, KiB, MiB, GiB, KB, MB, GB +from ..import util + +from . import availability +from . import task + +@add_metaclass(abc.ABCMeta) +class FSResizeTask(task.Task): + """ The abstract properties that any resize task must have. """ + + unit = abc.abstractproperty(doc="Resize unit.") + size_fmt = abc.abstractproperty(doc="Size format string.") + +@add_metaclass(abc.ABCMeta) +class FSResize(task.BasicApplication, FSResizeTask): + """ An abstract class for resizing a filesystem. """ + + description = "resize filesystem" + + args = abc.abstractproperty(doc="Resize arguments.") + + def __init__(self, an_fs): + """ Initializer. + + :param FS an_fs: a filesystem object + """ + self.fs = an_fs + + # IMPLEMENTATION methods + + @abc.abstractmethod + def sizeSpec(self): + """ Returns a string specification for the target size of the command. + :returns: size specification + :rtype: str + """ + raise NotImplementedError() + + def _resizeCommand(self): + return [str(self.ext)] + self.args + + def doTask(self): + """ Resize the device. + + :raises FSError: on failure + """ + error_msgs = self.availabilityErrors + if error_msgs: + raise FSError("\n".join(error_msgs)) + + try: + ret = util.run_program(self._resizeCommand()) + except OSError as e: + raise FSError(e) + + if ret: + raise FSError("resize failed: %s" % ret) + +class Ext2FSResize(FSResize): + ext = availability.RESIZE2FS_APP + unit = MiB + + # No bytes specification is described in the man pages. A number without + # any suffix is interpreted as indicating the number of filesystem blocks. + # A suffix of "s" specifies a 512 byte sector. It is omitted here because + # the lookup is only by standard binary units. + size_fmt = {KiB: "%dK", MiB: "%dM", GiB: "%dG"}[unit] + + def sizeSpec(self): + return self.size_fmt % self.fs.targetSize.convertTo(self.unit) + + @property + def args(self): + return ["-p", self.fs.device, self.sizeSpec()] + +class NTFSResize(FSResize): + ext = availability.NTFSRESIZE_APP + unit = B + size_fmt = {B: "%d", KB: "%dK", MB: "%dM", GB: "%dG"}[unit] + + def sizeSpec(self): + return self.size_fmt % self.fs.targetSize.convertTo(self.unit) + + @property + def args(self): + return [ + "-ff", # need at least two 'f's to fully suppress interaction + "-s", self.sizeSpec(), + self.fs.device + ] + +class TmpFSResize(FSResize): + + ext = availability.MOUNT_APP + unit = MiB + size_fmt = {KiB: "%dk", MiB: "%dm", GiB: "%dg"}[unit] + + def sizeSpec(self): + return "size=%s" % (self.size_fmt % self.fs.targetSize.convertTo(self.unit)) + + @property + def args(self): + # This is too closely mixed in w/ TmpFS object, due to the + # fact that resizing is done by mounting and that the options are + # therefore mount options. The situation is hard to avoid, though. + opts = self.fs.mountopts or ",".join(self.fs._mount.options) + options = ("remount", opts, self.sizeSpec()) + return ['-o', ",".join(options), self.fs._type, self.fs.systemMountpoint] + +class UnimplementedFSResize(task.UnimplementedTask, FSResizeTask): + + def __init__(self, an_fs): + """ Initializer. + + :param FS an_fs: a filesystem object + """ + self.fs = an_fs + + @property + def unit(self): + raise NotImplementedError() + + @property + def size_fmt(self): + raise NotImplementedError() diff --git a/blivet/tasks/fssize.py b/blivet/tasks/fssize.py new file mode 100644 index 0000000..b28083e --- /dev/null +++ b/blivet/tasks/fssize.py @@ -0,0 +1,166 @@ +# fssize.py +# Filesystem size gathering classes. +# +# Copyright (C) 2015 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Anne Mulhern amulhern@redhat.com + +import abc +from collections import namedtuple + +from six import add_metaclass + +from ..errors import FSError +from ..size import Size +from .. import util + +from . import availability +from . import task + +_tags = ("count", "size") +_Tags = namedtuple("_Tags", _tags) + +@add_metaclass(abc.ABCMeta) +class FSSize(task.Task): + """ An abstract class that represents size information extraction. """ + description = "current filesystem size" + + tags = abc.abstractproperty( + doc="Strings used for extracting components of size.") + + def __init__(self, an_fs): + """ Initializer. + + :param FS an_fs: a filesystem object + """ + self.fs = an_fs + + # TASK methods + + @property + def _availabilityErrors(self): + return [] + + @property + def dependsOn(self): + return [self.fs._info] + + # IMPLEMENTATION methods + + def doTask(self): + """ Returns the size of the filesystem. + + :returns: the size of the filesystem + :rtype: :class:`~.size.Size` + :raises FSError: on failure + """ + error_msgs = self.availabilityErrors + if error_msgs: + raise FSError("\n".join(error_msgs)) + + if self.fs._current_info is None: + raise FSError("No info available for size computation.") + + # Setup initial values + values = {} + for k in _tags: + values[k] = None + + # Attempt to set values from info + for line in (l.strip() for l in self.fs._current_info.splitlines()): + key = next((k for k in _tags if line.startswith(getattr(self.tags, k))), None) + if not key: + continue + + if values[key] is not None: + raise FSError("found two matches for key %s" % key) + + # Look for last numeric value in matching line + fields = line.split() + fields.reverse() + for field in fields: + try: + values[key] = int(field) + break + except ValueError: + continue + + # Raise an error if a value is missing + missing = next((k for k in _tags if values[k] is None), None) + if missing is not None: + raise FSError("Failed to parse info for %s." % missing) + + return values["count"] * Size(values["size"]) + +class Ext2FSSize(FSSize): + tags = _Tags(size="Block size:", count="Block count:") + +class JFSSize(FSSize): + tags = _Tags(size="Physical block size:", count="Aggregate size:") + +class NTFSSize(FSSize): + tags = _Tags(size="Cluster Size:", count="Volume Size in Clusters:") + +class ReiserFSSize(FSSize): + tags = _Tags(size="Blocksize:", count="Count of blocks on the device:") + +class XFSSize(FSSize): + tags = _Tags(size="blocksize =", count="dblocks =") + +class TmpFSSize(task.BasicApplication): + description = "current filesystem size" + + ext = availability.DF_APP + + def __init__(self, an_fs): + """ Initializer. + + :param FS an_fs: a filesystem object + """ + self.fs = an_fs + + @property + def _sizeCommand(self): + return [str(self.ext), self.fs.systemMountpoint, "--output=size"] + + def doTask(self): + error_msgs = self.availabilityErrors + if error_msgs: + raise FSError("\n".join(error_msgs)) + + try: + (ret, out) = util.run_program_and_capture_output(self._sizeCommand) + if ret: + raise FSError("Failed to execute command %s." % self._sizeCommand) + except OSError: + raise FSError("Failed to execute command %s." % self._sizeCommand) + + lines = out.splitlines() + if len(lines) != 2 or lines[0].strip() != "1K-blocks": + raise FSError("Failed to parse output of command %s." % self._sizeCommand) + + return Size("%s KiB" % lines[1]) + + +class UnimplementedFSSize(task.UnimplementedTask): + + def __init__(self, an_fs): + """ Initializer. + + :param FS an_fs: a filesystem object + """ + self.fs = an_fs diff --git a/blivet/tasks/fssync.py b/blivet/tasks/fssync.py new file mode 100644 index 0000000..e83aa12 --- /dev/null +++ b/blivet/tasks/fssync.py @@ -0,0 +1,89 @@ +# fssync.py +# Filesystem syncing classes. +# +# Copyright (C) 2014 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Anne Mulhern amulhern@redhat.com + +import abc + +from six import add_metaclass + +from ..errors import FSError +from .. import util + +from . import availability +from . import task + +@add_metaclass(abc.ABCMeta) +class FSSync(task.BasicApplication): + """ An abstract class that represents syncing a filesystem. """ + + description = "filesystem syncing" + + def __init__(self, an_fs): + """ Initializer. + + :param FS an_fs: a filesystem object + """ + self.fs = an_fs + + @abc.abstractmethod + def doTask(self): + raise NotImplementedError() + +class XFSSync(FSSync): + """ Sync application for XFS. """ + + ext = availability.XFSFREEZE_APP + + def _freezeCommand(self): + return [str(self.ext), "-f", self.fs.systemMountpoint] + + def _unfreezeCommand(self): + return [str(self.ext), "-u", self.fs.systemMountpoint] + + def doTask(self, root="/"): + # pylint: disable=arguments-differ + error_msgs = self.availabilityErrors + if error_msgs: + raise FSError("\n".join(error_msgs)) + + error_msg = None + try: + rc = util.run_program(self._freezeCommand(), root=root) + except OSError as e: + error_msg = "failed to sync filesytem: %s" % e + error_msg = error_msg or rc + + try: + rc = util.run_program(self._unfreezeCommand(), root=root) + except OSError as e: + error_msg = error_msg or "failed to sync filesystem: %s" % e + error_msg = error_msg or rc + + if error_msg: + raise FSError(error_msg) + +class UnimplementedFSSync(task.UnimplementedTask): + + def __init__(self, an_fs): + """ Initializer. + + :param FS an_fs: a filesystem object + """ + self.fs = an_fs diff --git a/blivet/tasks/fswritelabel.py b/blivet/tasks/fswritelabel.py new file mode 100644 index 0000000..af64d92 --- /dev/null +++ b/blivet/tasks/fswritelabel.py @@ -0,0 +1,116 @@ +# fswritelabel.py +# Filesystem label writing classes. +# +# Copyright (C) 2015 Red Hat, Inc. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions of +# the GNU General Public License v.2, or (at your option) any later version. +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY expressed or implied, including the implied warranties of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. You should have received a copy of the +# GNU General Public License along with this program; if not, write to the +# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the +# source code or documentation are not subject to the GNU General Public +# License and may only be used or replicated with the express permission of +# Red Hat, Inc. +# +# Red Hat Author(s): Anne Mulhern amulhern@redhat.com + +import abc + +from six import add_metaclass + +from .. import util +from ..errors import FSWriteLabelError + +from . import availability +from . import task + +@add_metaclass(abc.ABCMeta) +class FSWriteLabel(task.BasicApplication): + """ An abstract class that represents writing a label for a filesystem. """ + + description = "write filesystem label" + + args = abc.abstractproperty(doc="arguments for writing a label") + + def __init__(self, an_fs): + """ Initializer. + + :param FS an_fs: a filesystem object + """ + self.fs = an_fs + + # IMPLEMENTATION methods + + @property + def _setCommand(self): + """Get the command to label the filesystem. + + :return: the command + :rtype: list of str + """ + return [str(self.ext)] + self.args + + def doTask(self): + error_msgs = self.availabilityErrors + if error_msgs: + raise FSWriteLabelError("\n".join(error_msgs)) + + rc = util.run_program(self._setCommand) + if rc: + raise FSWriteLabelError("label failed") + +class DosFSWriteLabel(FSWriteLabel): + ext = availability.DOSFSLABEL_APP + + @property + def args(self): + return [self.fs.device, self.fs.label] + +class Ext2FSWriteLabel(FSWriteLabel): + ext = availability.E2LABEL_APP + + @property + def args(self): + return [self.fs.device, self.fs.label] + +class JFSWriteLabel(FSWriteLabel): + ext = availability.JFSTUNE_APP + + @property + def args(self): + return ["-L", self.fs.label, self.fs.device] + +class NTFSWriteLabel(FSWriteLabel): + ext = availability.NTFSLABEL_APP + + @property + def args(self): + return [self.fs.device, self.fs.label] + +class ReiserFSWriteLabel(FSWriteLabel): + ext = availability.REISERFSTUNE_APP + + @property + def args(self): + return ["-l", self.fs.label, self.fs.device] + +class XFSWriteLabel(FSWriteLabel): + ext = availability.XFSADMIN_APP + + @property + def args(self): + return ["-L", self.fs.label if self.fs.label != "" else "--", self.fs.device] + +class UnimplementedFSWriteLabel(task.UnimplementedTask): + + def __init__(self, an_fs): + """ Initializer. + + :param FS an_fs: a filesystem object + """ + self.fs = an_fs
From: mulhern amulhern@redhat.com
Related: #12
Signed-off-by: mulhern amulhern@redhat.com --- blivet/formats/fs.py | 845 ++++++++++-------------------------- blivet/formats/fslabel.py | 205 --------- blivet/formats/fslabeling.py | 148 ------- tests/formats_test/fs_test.py | 2 +- tests/formats_test/fslabeling.py | 8 +- tests/formats_test/fstesting.py | 14 +- tests/formats_test/labeling_test.py | 10 +- 7 files changed, 239 insertions(+), 993 deletions(-) delete mode 100644 blivet/formats/fslabel.py delete mode 100644 blivet/formats/fslabeling.py
diff --git a/blivet/formats/fs.py b/blivet/formats/fs.py index 6372a2b..89ad2c7 100644 --- a/blivet/formats/fs.py +++ b/blivet/formats/fs.py @@ -26,8 +26,19 @@ import os import tempfile
-from . import fslabeling -from ..errors import FormatCreateError, FSError, FSResizeError +from ..tasks import fsck +from ..tasks import fsinfo +from ..tasks import fslabeling +from ..tasks import fsminsize +from ..tasks import fsmkfs +from ..tasks import fsmount +from ..tasks import fsreadlabel +from ..tasks import fsresize +from ..tasks import fssize +from ..tasks import fssync +from ..tasks import fswritelabel +from ..errors import FormatCreateError, FSError, FSReadLabelError +from ..errors import FSWriteLabelError, FSResizeError from . import DeviceFormat, register_device_format from .. import util from .. import platform @@ -36,8 +47,7 @@ from ..storage_log import log_exception_info, log_method_call from .. import arch from ..size import Size, ROUND_UP, ROUND_DOWN, unitStr -from ..size import B, KiB, MiB, GiB, KB, MB, GB -from ..i18n import _, N_ +from ..i18n import N_ from .. import udev from ..mounts import mountsCache
@@ -49,22 +59,19 @@ class FS(DeviceFormat): """ Filesystem base class. """ _type = "Abstract Filesystem Class" # fs type name - _mountType = None # like _type but for passing to mount _name = None - _mkfs = "" # mkfs utility _modules = [] # kernel modules required for support - _resizefs = "" # resize utility _labelfs = None # labeling functionality - _fsck = "" # fs check utility - _fsckErrors = {} # fs check command error codes & msgs - _infofs = "" # fs info utility - _defaultFormatOptions = [] # default options passed to mkfs - _defaultMountOptions = ["defaults"] # default options passed to mount - _defaultCheckOptions = [] - _defaultInfoOptions = [] - _existingSizeFields = [] - _resizefsUnit = None - _fsProfileSpecifier = None # mkfs option specifying fsprofile + _fsckClass = fsck.UnimplementedFSCK + _infoClass = fsinfo.UnimplementedFSInfo + _minsizeClass = fsminsize.UnimplementedFSMinSize + _mkfsClass = fsmkfs.UnimplementedFSMkfs + _mountClass = fsmount.FSMount + _readlabelClass = fsreadlabel.UnimplementedFSReadLabel + _resizeClass = fsresize.UnimplementedFSResize + _sizeinfoClass = fssize.UnimplementedFSSize + _syncClass = fssync.UnimplementedFSSync + _writelabelClass = fswritelabel.UnimplementedFSWriteLabel
def __init__(self, **kwargs): """ @@ -87,10 +94,28 @@ def __init__(self, **kwargs): that you can specify the device at the last moment by specifying it via the 'device' kwarg to the :meth:`create` method. """ + if self.__class__ is FS: raise TypeError("FS is an abstract class.")
DeviceFormat.__init__(self, **kwargs) + + # Create task objects + self._info = self._infoClass(self) + self._fsck = self._fsckClass(self) + self._mkfs = self._mkfsClass(self) + self._mount = self._mountClass(self) + self._readlabel = self._readlabelClass(self) + self._resize = self._resizeClass(self) + self._sync = self._syncClass(self) + self._writelabel = self._writelabelClass(self) + + # These two may depend on info class, so create them after + self._minsize = self._minsizeClass(self) + self._sizeinfo = self._sizeinfoClass(self) + + self._current_info = None # info obtained by _info task + self.mountpoint = kwargs.get("mountpoint") self.mountopts = kwargs.get("mountopts") self.label = kwargs.get("label") @@ -103,7 +128,7 @@ def __init__(self, **kwargs): # Resize operations are limited to error-free filesystems whose current # size is known. self._resizable = False - if flags.installer_mode and self.resizefsProg: + if flags.installer_mode and self._resize.available: # if you want current/min size you have to call updateSizeInfo try: self.updateSizeInfo() @@ -146,7 +171,7 @@ def labeling(self):
:rtype: bool """ - return self._labelfs is not None + return (self._mkfs.canLabel and self._mkfs.available) or self._writelabel.available
def relabels(self): """Returns True if it is possible to relabel this filesystem @@ -154,7 +179,7 @@ def relabels(self):
:rtype: bool """ - return self._labelfs is not None and self._labelfs.label_app is not None + return self._writelabel.available
def labelFormatOK(self, label): """Return True if the label has an acceptable format for this @@ -206,11 +231,26 @@ def _getSize(self):
def updateSizeInfo(self): """ Update this filesystem's current and minimum size (for resize). """ + + # This method ensures: + # * If there are fsck errors, self._resizable is False. + # Note that if there is no fsck program, no errors are possible. + # * If it is not possible to obtain the current size of the + # filesystem by interrogating the filesystem, self._resizable + # is False (and self._size is 0). + # * _minInstanceSize is obtained or it is set to _size. Effectively + # this means that it is the actual minimum size, or if that + # cannot be obtained the actual current size of the device. + # If it was not possible to obtain the current size of the device + # then _minInstanceSize is 0, but since _resizable is False + # that information can not be used to shrink the filesystem below + # its unknown actual minimum size. + # * self._getMinSize() is only run if fsck succeeds and a current + # existing size can be obtained. if not self.exists: return
- self._size = Size(0) - self._minSize = self.__class__._minSize + self._current_info = None self._minInstanceSize = Size(0) self._resizable = self.__class__._resizable
@@ -218,123 +258,69 @@ def updateSizeInfo(self): try: self.doCheck() except FSError: - errors = True + self._resizable = False raise - else: - errors = False finally: # try to gather current size info anyway - info = self._getFSInfo() - self._size = self._getExistingSize(info=info) - self._minSize = self._size # default to current size + self._size = Size(0) + try: + if self._info.available: + self._current_info = self._info.doTask() + except FSError as e: + log.info("Failed to obtain info for device %s: %s", self.device, e) + try: + self._size = self._sizeinfo.doTask() + self._minInstanceSize = self._size + except (FSError, NotImplementedError) as e: + log.warning("Failed to obtain current size for device %s: %s", self.device, e) + # We absolutely need a current size to enable resize. To shrink the # filesystem we need a real minimum size provided by the resize # tool. Failing that, we can default to the current size, # effectively disabling shrink. - if errors or self._size == Size(0): + if self._size == Size(0): self._resizable = False
- self._getMinSize(info=info) # force calculation of minimum size - - def _getMinSize(self, info=None): - pass + try: + result = self._minsize.doTask() + size = self._padSize(result) + if result < size: + log.debug("padding min size from %s up to %s", result, size) + else: + log.debug("using current size %s as min size", size) + self._minInstanceSize = size + except (FSError, NotImplementedError) as e: + log.warning("Failed to obtain minimum size for device %s: %s", self.device, e)
- def _getFSInfo(self): - buf = "" - if self.infofsProg and self.exists and \ - util.find_program_in_path(self.infofsProg): - argv = self._defaultInfoOptions + [ self.device ] - try: - buf = util.capture_output([self.infofsProg] + argv) - except OSError as e: - log.error("failed to gather fs info: %s", e) - - return buf - - def _getExistingSize(self, info=None): - """ Determine the size of this filesystem. - - Filesystem must exist. Each filesystem varies, but the general - procedure is to run the filesystem dump or info utility and read - the block size and number of blocks for the filesystem - and compute megabytes from that. - - The loop that reads the output from the infofsProg is meant - to be simple, but take in to account variations in output. - The general procedure: - 1) Capture output from infofsProg. - 2) Iterate over each line of the output: - a) Trim leading and trailing whitespace. - b) Break line into fields split on ' ' - c) If line begins with any of the strings in - _existingSizeFields, start at the end of - fields and take the first one that converts - to an int. Store this in the values list. - d) Repeat until the values list length equals - the _existingSizeFields length. - 3) If the length of the values list equals the length - of _existingSizeFields, compute the size of this - filesystem by multiplying all of the values together - to get bytes, then convert to megabytes. Return - this value. - 4) If we were unable to capture all fields, return 0. - - The caller should catch exceptions from this method. Any - exception raised indicates a need to change the fields we - are looking for, the command to run and arguments, or - something else. If you catch an exception from this method, - assume the filesystem cannot be resized. - - :keyword info: filesystem info buffer - :type info: str (output of :attr:`infofsProg`) - :returns: size of existing fs in MiB. - :rtype: float. + @property + def minSize(self): + # If self._minInstanceSize is not 0, then it should be no less than + # self._minSize, by definition, and since a non-zero value indicates + # that it was obtained, it is the preferred value. + # If self._minInstanceSize is less than self._minSize, + # but not 0, then there must be some mistake, so better to use + # self._minSize. + return max(self._minInstanceSize, self._minSize) + + def _padSize(self, size): + """ Return a size padded according to some inflating rules. + + This method was originally designed solely for minimum sizes, + and may only apply to them. + + :param size: any size + :type size: :class:`~.size.Size` + :returns: a padded size + :rtype: :class:`~.size.Size` """ - if not self._existingSizeFields: - return Size(0) - - size = Size(0) - if self.exists: - if info is None: - info = self._getFSInfo() - - try: - values = [] - for line in info.splitlines(): - found = False - - line = line.strip() - tmp = line.split() - tmp.reverse() + # add some padding to the min size + padded = min(size * Decimal('1.1'), size + Size("500 MiB"))
- for field in self._existingSizeFields: - if line.startswith(field): - for subfield in tmp: - try: - values.append(int(subfield)) - found = True - break - except ValueError: - continue + # make sure the padded and rounded min size is not larger than + # the current size + padded = min(padded.roundToNearest(self._resize.unit, rounding=ROUND_UP), self.currentSize)
- if found: - break - - if len(values) == len(self._existingSizeFields): - break - - if len(values) != len(self._existingSizeFields): - return Size(0) - - size = 1 - for value in values: - size *= value - - size = Size(size) - except Exception: # pylint: disable=broad-except - log_exception_info(log.error, "failed to obtain size of filesystem on %s", [self.device]) - - return size + return padded
@property def currentSize(self): @@ -348,34 +334,10 @@ def free(self): """ return max(Size(0), self.currentSize - self.minSize)
- def _getFormatOptions(self, options=None, do_labeling=False): - """Get a list of format options to be used when creating the - filesystem. - - :param options: any special options - :type options: list of str or None - :param bool do_labeling: True if labeling during filesystem creation, - otherwise False - """ - argv = [] - if options and isinstance(options, list): - argv.extend(options) - argv.extend(self.defaultFormatOptions) - if self._fsProfileSpecifier and self.fsprofile: - argv.extend([self._fsProfileSpecifier, self.fsprofile]) - - if do_labeling and self.label is not None: - if self.labelFormatOK(self.label): - argv.extend(self._labelfs.labelingArgs(self.label)) - else: - log.warning("Choosing not to apply label (%s) during creation of filesystem %s. Label format is unacceptable for this filesystem.", self.label, self.type) - - argv.append(self.device) - return argv
def _preCreate(self, **kwargs): super(FS, self)._preCreate(**kwargs) - if not self.mkfsProg: + if not self._mkfs.available: return
def _create(self, **kwargs): @@ -390,19 +352,14 @@ def _create(self, **kwargs): if not self.formattable: return
- argv = self._getFormatOptions(options=kwargs.get("options"), - do_labeling=not self.relabels()) - super(FS, self)._create() - ret = 0 try: - ret = util.run_program([self.mkfsProg] + argv) - except OSError as e: + self._mkfs.doTask(options=kwargs.get("options"), label=not self.relabels()) + except FSWriteLabelError as e: + log.warning("Choosing not to apply label (%s) during creation of filesystem %s. Label format is unacceptable for this filesystem.", self.label, self.type) + except FSError as e: raise FormatCreateError(e, self.device)
- if ret: - raise FormatCreateError("format failed: %s" % ret, self.device) - def _postCreate(self, **kwargs): super(FS, self)._postCreate(**kwargs) if self.label is not None and self.relabels(): @@ -411,17 +368,6 @@ def _postCreate(self, **kwargs): except FSError as e: log.warning("Failed to write label (%s) for filesystem %s: %s", self.label, self.type, e)
- @property - def resizeArgs(self): - """ Returns the arguments for resizing the filesystem. - - Must be overridden by every class that has non-None _resizefs. - - :returns: arguments for resizing a filesystem. - :rtype: list of str - """ - return [] - def doResize(self): """ Resize this filesystem based on this instance's targetSize attr.
@@ -436,7 +382,7 @@ def doResize(self): if self.targetSize == self.currentSize: return
- if not self.resizefsProg: + if not self._resize.available: return
# tmpfs mounts don't need an existing device node @@ -462,7 +408,7 @@ def doResize(self): # We always round down because the fs has to fit on whatever device # contains it. To round up would risk quietly setting a target size too # large for the device to hold. - rounded = self.targetSize.roundToNearest(self._resizefsUnit, + rounded = self.targetSize.roundToNearest(self._resize.unit, rounding=ROUND_DOWN)
# 1. target size was between the min size and max size values prior to @@ -483,38 +429,22 @@ def doResize(self): # early. if self.targetSize > self.currentSize and rounded < self.currentSize: log.info("rounding target size down to next %s obviated resize of " - "filesystem on %s", unitStr(self._resizefsUnit), self.device) + "filesystem on %s", unitStr(self._resize.unit), self.device) return else: self.targetSize = rounded
try: - ret = util.run_program([self.resizefsProg] + self.resizeArgs) - except OSError as e: + self._resize.doTask() + except FSError as e: raise FSResizeError(e, self.device)
- if ret: - raise FSResizeError("resize failed: %s" % ret, self.device) - self.doCheck()
# XXX must be a smarter way to do this self._size = self.targetSize self.notifyKernel()
- def _getCheckArgs(self): - argv = [] - argv.extend(self.defaultCheckOptions) - argv.append(self.device) - return argv - - def _fsckFailed(self, rc): - # pylint: disable=unused-argument - return False - - def _fsckErrorMessage(self, rc): - return _("Unknown return code: %d.") % (rc,) - def doCheck(self): """ Run a filesystem check.
@@ -523,23 +453,13 @@ def doCheck(self): if not self.exists: raise FSError("filesystem has not been created")
- if not self.fsckProg: + if not self._fsck.available: return
if not os.path.exists(self.device): raise FSError("device does not exist")
- try: - ret = util.run_program([self.fsckProg] + self._getCheckArgs()) - except OSError as e: - raise FSError("filesystem check failed: %s" % e) - - if self._fsckFailed(ret): - hdr = _("%(type)s filesystem check failure on %(device)s: ") % \ - {"type": self.type, "device": self.device} - - msg = self._fsckErrorMessage(ret) - raise FSError(hdr + msg) + self._fsck.doTask()
def loadModule(self): """Load whatever kernel module is required to support this filesystem.""" @@ -631,24 +551,6 @@ def _preSetup(self, **kwargs): chrootedMountpoint = os.path.normpath("%s/%s" % (chroot, mountpoint)) return self.systemMountpoint != chrootedMountpoint
- def _setupMountOptions(self, options): - """ The options actually used to mount the filesystem. - - These are not necessarily the same as either the default options - or the options argument. - - :param str options: the mount options passed to setup() method. - :returns: the mount options used by setup - :rtype: str - """ - if not options or not isinstance(options, str): - options = self.options - - if isinstance(self, BindFS): - options = "bind," + options - - return options - def _setup(self, **kwargs): """ Mount this filesystem.
@@ -658,7 +560,7 @@ def _setup(self, **kwargs): :keyword mountpoint: mountpoint (overrides self.mountpoint) :raises: FSError """ - options = self._setupMountOptions(kwargs.get("options", "")) + options = kwargs.get("options", "") chroot = kwargs.get("chroot", "/") mountpoint = kwargs.get("mountpoint") or self.mountpoint
@@ -668,23 +570,14 @@ def _setup(self, **kwargs): # #mountpoint = os.path.join(chroot, mountpoint) chrootedMountpoint = os.path.normpath("%s/%s" % (chroot, mountpoint)) - - try: - rc = util.mount(self.device, chrootedMountpoint, - fstype=self.mountType, - options=options) - except Exception as e: - raise FSError("mount failed: %s" % e) - - if rc: - raise FSError("mount failed: %s" % rc) + self._mount.doTask(chrootedMountpoint, options=options)
def _postSetup(self, **kwargs): - options = self._setupMountOptions(kwargs.get("options", "")) + options = kwargs.get("options", "") chroot = kwargs.get("chroot", "/") mountpoint = kwargs.get("mountpoint") or self.mountpoint
- if flags.selinux and "ro" not in options.split(",") and flags.installer_mode: + if flags.selinux and "ro" not in self._mount.mountOptions(options).split(",") and flags.installer_mode: ret = util.reset_file_context(mountpoint, chroot) if not ret: log.warning("Failed to reset SElinux context for newly mounted filesystem root directory to default.") @@ -734,27 +627,17 @@ def readLabel(self): :return: the filesystem's label :rtype: str
- Raises a FSError if the label can not be read. + Raises a FSReadLabelError if the label can not be read. """ if not self.exists: - raise FSError("filesystem has not been created") + raise FSReadLabelError("filesystem has not been created")
if not os.path.exists(self.device): - raise FSError("device does not exist") + raise FSReadLabelError("device does not exist")
- if not self.relabels() or not self._labelfs.label_app.reads: - raise FSError("no application to read label for filesystem %s" % self.type) - - (rc, out) = util.run_program_and_capture_output(self._labelfs.label_app.readLabelCommand(self)) - if rc: - raise FSError("read label failed") - - label = out.strip() - - if label == "": - return "" - else: - return self._labelfs.label_app.extractLabel(label) + if not self._readlabel.available: + raise FSReadLabelError("can not read label for filesystem %s" % self.type) + return self._readlabel.doTask()
def writeLabel(self): """ Create a label for this filesystem. @@ -772,119 +655,55 @@ def writeLabel(self): if not self.exists: raise FSError("filesystem has not been created")
- if not self.relabels(): + if not self._writelabel.available: raise FSError("no application to set label for filesystem %s" % self.type)
if not self.labelFormatOK(self.label): - raise FSError("bad label format for labelling application %s" % self._labelfs.label_app.name) + raise FSError("bad label format for labelling application %s" % self._writelabel)
if not os.path.exists(self.device): raise FSError("device does not exist")
- rc = util.run_program(self._labelfs.label_app.setLabelCommand(self)) - if rc: - raise FSError("label failed") - + self._writelabel.doTask() self.notifyKernel()
@property - def mkfsProg(self): - """ Program used to create filesystems of this type. """ - return self._mkfs - - @property - def fsckProg(self): - """ Program used to check filesystems of this type. """ - return self._fsck - - @property - def resizefsProg(self): - """ Program used to resize filesystems of this type. """ - return self._resizefs - - @property - def labelfsProg(self): - """ Program used to manage labels for this filesystem type. - - May be None if no such program exists. - """ - if self._labelfs and self._labelfs.label_app: - return self._labelfs.label_app.name - else: - return None - - @property - def infofsProg(self): - """ Program used to get information for this filesystem type. """ - return self._infofs - - @property def utilsAvailable(self): # we aren't checking for fsck because we shouldn't need it - for prog in [self.mkfsProg, self.resizefsProg, self.labelfsProg, - self.infofsProg]: - if not prog: - continue - - if not util.find_program_in_path(prog): - return False - - return True + tasks = (self._mkfs, self._resize, self._writelabel, self._info) + return all(not t.implemented or t.available for t in tasks)
@property def supported(self): log_method_call(self, supported=self._supported) - return self._supported and self.utilsAvailable - - @property - def mountable(self): - canmount = (self.mountType in kernel_filesystems) or \ - (os.access("/sbin/mount.%s" % (self.mountType,), os.X_OK)) - - # Still consider the filesystem type mountable if there exists - # an appropriate filesystem driver in the kernel modules directory. - if not canmount: - modpath = os.path.realpath(os.path.join("/lib/modules", os.uname()[2])) - if os.path.isdir(modpath): - modname = "%s.ko" % self.mountType - for _root, _dirs, files in os.walk(modpath): - if any(x.startswith(modname) for x in files): - return True - - return canmount + return super(FS, self).supported and self.utilsAvailable
@property - def resizable(self): - """ Can formats of this filesystem type be resized? """ - return super(FS, self).resizable and self.utilsAvailable + def controllable(self): + return super(FS, self).controllable and self.mountable
@property - def defaultFormatOptions(self): - """ Default options passed to mkfs for this filesystem type. """ - # return a copy to prevent modification - return self._defaultFormatOptions[:] + def mountable(self): + return self._mount.available
@property - def defaultMountOptions(self): - """ Default options passed to mount for this filesystem type. """ - # return a copy to prevent modification - return self._defaultMountOptions[:] + def formattable(self): + return super(FS, self).formattable and self._mkfs.available
@property - def defaultCheckOptions(self): - """ Default options passed to checker for this filesystem type. """ - # return a copy to prevent modification - return self._defaultCheckOptions[:] + def resizable(self): + """ Can formats of this filesystem type be resized? """ + return super(FS, self).resizable and self._resize.available
def _getOptions(self): - return self.mountopts or ",".join(self.defaultMountOptions) + return self.mountopts or ",".join(self._mount.options)
def _setOptions(self, options): self.mountopts = options
@property def mountType(self): - return self._mountType or self._type + return self._mount.mountType
# These methods just wrap filesystem-specific methods in more # generically named methods so filesystems and formatted devices @@ -923,8 +742,21 @@ def status(self): return False return self.systemMountpoint is not None
- def sync(self, root="/"): - pass + def sync(self, root='/'): + """ Ensure that data we've written is at least in the journal. + + This is a little odd because xfs_freeze will only be + available under the install root. + """ + if not self.status or not self.systemMountpoint or \ + not self.systemMountpoint.startswith(root) or \ + not self._sync.available: + return + + try: + self._sync.doTask(root) + except FSError as e: + log.error(e)
def populateKSData(self, data): super(FS, self).populateKSData(data) @@ -940,128 +772,35 @@ def populateKSData(self, data): class Ext2FS(FS): """ ext2 filesystem. """ _type = "ext2" - _mkfs = "mke2fs" _modules = ["ext2"] - _resizefs = "resize2fs" _labelfs = fslabeling.Ext2FSLabeling() - _fsck = "e2fsck" - _fsckErrors = {4: N_("File system errors left uncorrected."), - 8: N_("Operational error."), - 16: N_("Usage or syntax error."), - 32: N_("e2fsck cancelled by user request."), - 128: N_("Shared library error.")} _packages = ["e2fsprogs"] _formattable = True _supported = True _resizable = True _linuxNative = True _maxSize = Size("8 TiB") - _defaultCheckOptions = ["-f", "-p", "-C", "0"] _dump = True _check = True - _infofs = "dumpe2fs" - _defaultInfoOptions = ["-h"] - _existingSizeFields = ["Block count:", "Block size:"] - _resizefsUnit = MiB - _fsProfileSpecifier = "-T" + _fsckClass = fsck.Ext2FSCK + _infoClass = fsinfo.Ext2FSInfo + _minsizeClass = fsminsize.Ext2FSMinSize + _mkfsClass = fsmkfs.Ext2FSMkfs + _readlabelClass = fsreadlabel.Ext2FSReadLabel + _resizeClass = fsresize.Ext2FSResize + _sizeinfoClass = fssize.Ext2FSSize + _writelabelClass = fswritelabel.Ext2FSWriteLabel partedSystem = fileSystemType["ext2"]
- def _fsckFailed(self, rc): - for errorCode in self._fsckErrors.keys(): - if rc & errorCode: - return True - return False - - def _fsckErrorMessage(self, rc): - msg = '' - - for errorCode in self._fsckErrors.keys(): - if rc & errorCode: - msg += "\n" + _(self._fsckErrors[errorCode]) - - return msg.strip() - - def _getMinSize(self, info=None): - """ Set the minimum size for this filesystem in MiB. - - :keyword info: filesystem info buffer - :type info: str (output of :attr:`infofsProg`) - :rtype: None. - """ - size = self._minSize - blockSize = None - - if self.exists and os.path.exists(self.device): - if info is None: - # get block size - info = self._getFSInfo() - - for line in info.splitlines(): - if line.startswith("Block size:"): - blockSize = int(line.split(" ")[-1]) - - if blockSize is None: - raise FSError("failed to get block size for %s filesystem " - "on %s" % (self.mountType, self.device)) - - # get minimum size according to resize2fs - buf = util.capture_output([self.resizefsProg, - "-P", self.device]) - _size = None - for line in buf.splitlines(): - # line will look like: - # Estimated minimum size of the filesystem: 1148649 - (text, _sep, minSize) = line.partition(":") - if "minimum size of the filesystem" not in text: - continue - minSize = minSize.strip() - if not minSize: - break - _size = Size(int(minSize) * blockSize) - break - - if not _size: - log.warning("failed to get minimum size for %s filesystem " - "on %s", self.mountType, self.device) - else: - size = _size - orig_size = size - log.debug("size=%s, current=%s", size, self.currentSize) - # add some padding - size = min(size * Decimal('1.1'), - size + Size("500 MiB")) - # make sure that the padded and rounded min size is not larger - # than the current size - size = min(size.roundToNearest(self._resizefsUnit, - rounding=ROUND_UP), - self.currentSize) - if orig_size < size: - log.debug("padding min size from %s up to %s", orig_size, size) - else: - log.debug("using current size %s as min size", size) - - self._minInstanceSize = size - - @property - def minSize(self): - return self._minInstanceSize - - @property - def resizeArgs(self): - # No unit specifier is interpreted not as bytes, but block size. - FMT = {KiB: "%dK", MiB: "%dM", GiB: "%dG"}[self._resizefsUnit] - size_spec = FMT % self.targetSize.convertTo(self._resizefsUnit) - return ["-p", self.device, size_spec] - register_device_format(Ext2FS)
class Ext3FS(Ext2FS): """ ext3 filesystem. """ _type = "ext3" - _defaultFormatOptions = ["-t", "ext3"] _modules = ["ext3"] partedSystem = fileSystemType["ext3"] + _mkfsClass = fsmkfs.Ext3FSMkfs
# It is possible for a user to specify an fsprofile that defines a blocksize # smaller than the default of 4096 bytes and therefore to make liars of us @@ -1075,8 +814,8 @@ class Ext3FS(Ext2FS): class Ext4FS(Ext3FS): """ ext4 filesystem. """ _type = "ext4" - _defaultFormatOptions = ["-t", "ext4"] _modules = ["ext4"] + _mkfsClass = fsmkfs.Ext4FSMkfs partedSystem = fileSystemType["ext4"]
register_device_format(Ext4FS) @@ -1085,39 +824,29 @@ class Ext4FS(Ext3FS): class FATFS(FS): """ FAT filesystem. """ _type = "vfat" - _mkfs = "mkdosfs" _modules = ["vfat"] _labelfs = fslabeling.FATFSLabeling() - _fsck = "dosfsck" - _fsckErrors = {1: N_("Recoverable errors have been detected or dosfsck has " - "discovered an internal inconsistency."), - 2: N_("Usage error.")} _supported = True _formattable = True _maxSize = Size("1 TiB") _packages = [ "dosfstools" ] - _defaultMountOptions = ["umask=0077", "shortname=winnt"] - _defaultCheckOptions = ["-n"] + _fsckClass = fsck.DosFSCK + _mkfsClass = fsmkfs.FATFSMkfs + _mountClass = fsmount.FATFSMount + _readlabelClass = fsreadlabel.DosFSReadLabel + _writelabelClass = fswritelabel.DosFSWriteLabel # FIXME this should be fat32 in some cases partedSystem = fileSystemType["fat16"]
- def _fsckFailed(self, rc): - if rc >= 1: - return True - return False - - def _fsckErrorMessage(self, rc): - return _(self._fsckErrors[rc]) - register_device_format(FATFS)
class EFIFS(FATFS): _type = "efi" - _mountType = "vfat" _name = N_("EFI System Partition") _minSize = Size("50 MiB") _check = True + _mountClass = fsmount.EFIFSMount
@property def supported(self): @@ -1129,7 +858,6 @@ def supported(self): class BTRFS(FS): """ btrfs filesystem """ _type = "btrfs" - _mkfs = "mkfs.btrfs" _modules = ["btrfs"] _formattable = True _linuxNative = True @@ -1137,6 +865,7 @@ class BTRFS(FS): _packages = ["btrfs-progs"] _minSize = Size("256 MiB") _maxSize = Size("16 EiB") + _mkfsClass = fsmkfs.BTRFSMkfs # FIXME parted needs to be taught about btrfs so that we can set the # partition table type correctly for btrfs partitions # partedSystem = fileSystemType["btrfs"] @@ -1167,14 +896,13 @@ def _preSetup(self, **kwargs): class GFS2(FS): """ gfs2 filesystem. """ _type = "gfs2" - _mkfs = "mkfs.gfs2" _modules = ["dlm", "gfs2"] _formattable = True - _defaultFormatOptions = ["-j", "1", "-p", "lock_nolock", "-O"] _linuxNative = True _dump = True _check = True _packages = ["gfs2-utils"] + _mkfsClass = fsmkfs.GFS2Mkfs # FIXME parted needs to be thaught about btrfs so that we can set the # partition table type correctly for btrfs partitions # partedSystem = fileSystemType["gfs2"] @@ -1190,18 +918,17 @@ def supported(self): class JFS(FS): """ JFS filesystem """ _type = "jfs" - _mkfs = "mkfs.jfs" _modules = ["jfs"] _labelfs = fslabeling.JFSLabeling() - _defaultFormatOptions = ["-q"] _maxSize = Size("8 TiB") _formattable = True _linuxNative = True _dump = True _check = True - _infofs = "jfs_tune" - _defaultInfoOptions = ["-l"] - _existingSizeFields = ["Physical block size:", "Aggregate size:"] + _infoClass = fsinfo.JFSInfo + _mkfsClass = fsmkfs.JFSMkfs + _sizeinfoClass = fssize.JFSSize + _writelabelClass = fswritelabel.JFSWriteLabel partedSystem = fileSystemType["jfs"]
@property @@ -1215,18 +942,18 @@ def supported(self): class ReiserFS(FS): """ reiserfs filesystem """ _type = "reiserfs" - _mkfs = "mkreiserfs" _labelfs = fslabeling.ReiserFSLabeling() _modules = ["reiserfs"] - _defaultFormatOptions = ["-f", "-f"] _maxSize = Size("16 TiB") _formattable = True _linuxNative = True _dump = True _check = True _packages = ["reiserfs-utils"] - _infofs = "debugreiserfs" - _existingSizeFields = ["Count of blocks on the device:", "Blocksize:"] + _infoClass = fsinfo.ReiserFSInfo + _mkfsClass = fsmkfs.ReiserFSMkfs + _sizeinfoClass = fssize.ReiserFSSize + _writelabelClass = fswritelabel.ReiserFSWriteLabel partedSystem = fileSystemType["reiserfs"]
@property @@ -1240,50 +967,30 @@ def supported(self): class XFS(FS): """ XFS filesystem """ _type = "xfs" - _mkfs = "mkfs.xfs" _modules = ["xfs"] _labelfs = fslabeling.XFSLabeling() - _defaultFormatOptions = ["-f"] _maxSize = Size("16 EiB") _formattable = True _linuxNative = True _supported = True _packages = ["xfsprogs"] - _infofs = "xfs_db" - _defaultInfoOptions = ["-c", "sb 0", "-c", "p dblocks", - "-c", "p blocksize"] - _existingSizeFields = ["dblocks =", "blocksize ="] + _infoClass = fsinfo.XFSInfo + _mkfsClass = fsmkfs.XFSMkfs + _readlabelClass = fsreadlabel.XFSReadLabel + _sizeinfoClass = fssize.XFSSize + _syncClass = fssync.XFSSync + _writelabelClass = fswritelabel.XFSWriteLabel partedSystem = fileSystemType["xfs"]
- def sync(self, root='/'): - """ Ensure that data we've written is at least in the journal. - - This is a little odd because xfs_freeze will only be - available under the install root. - """ - if not self.status or not self.systemMountpoint or \ - not self.systemMountpoint.startswith(root): - return - - try: - util.run_program(["xfs_freeze", "-f", self.systemMountpoint], root=root) - except OSError as e: - log.error("failed to run xfs_freeze: %s", e) - - try: - util.run_program(["xfs_freeze", "-u", self.systemMountpoint], root=root) - except OSError as e: - log.error("failed to run xfs_freeze: %s", e)
register_device_format(XFS)
- class HFS(FS): _type = "hfs" - _mkfs = "hformat" _modules = ["hfs"] _labelfs = fslabeling.HFSLabeling() _formattable = True + _mkfsClass = fsmkfs.HFSMkfs partedSystem = fileSystemType["hfs"]
register_device_format(HFS) @@ -1291,11 +998,11 @@ class HFS(FS):
class AppleBootstrapFS(HFS): _type = "appleboot" - _mountType = "hfs" _name = N_("Apple Bootstrap") _minSize = Size("768 KiB") _maxSize = Size("1 MiB") _supported = True + _mountClass = fsmount.AppleBootstrapFSMount
@property def supported(self): @@ -1308,16 +1015,16 @@ class HFSPlus(FS): _type = "hfs+" _modules = ["hfsplus"] _udevTypes = ["hfsplus"] - _mkfs = "mkfs.hfsplus" - _fsck = "fsck.hfsplus" _packages = ["hfsplus-tools"] _labelfs = fslabeling.HFSPlusLabeling() _formattable = True - _mountType = "hfsplus" _minSize = Size("1 MiB") _maxSize = Size("2 TiB") _check = True partedSystem = fileSystemType["hfs+"] + _fsckClass = fsck.HFSPlusFSCK + _mkfsClass = fsmkfs.HFSPlusMkfs + _mountClass = fsmount.HFSPlusMount
register_device_format(HFSPlus)
@@ -1344,82 +1051,22 @@ def __init__(self, **kwargs): class NTFS(FS): """ ntfs filesystem. """ _type = "ntfs" - _mkfs = "mkntfs" - _resizefs = "ntfsresize" _labelfs = fslabeling.NTFSLabeling() - _fsck = "ntfsresize" _resizable = True _minSize = Size("1 MiB") _maxSize = Size("16 TiB") - _defaultMountOptions = ["defaults", "ro"] - _defaultCheckOptions = ["-c"] _packages = ["ntfsprogs"] - _infofs = "ntfsinfo" - _defaultInfoOptions = ["-m"] - _existingSizeFields = ["Cluster Size:", "Volume Size in Clusters:"] - _resizefsUnit = B + _fsckClass = fsck.NTFSFSCK + _infoClass = fsinfo.NTFSInfo + _minsizeClass = fsminsize.NTFSMinSize + _mkfsClass = fsmkfs.NTFSMkfs + _mountClass = fsmount.NTFSMount + _readlabelClass = fsreadlabel.NTFSReadLabel + _resizeClass = fsresize.NTFSResize + _sizeinfoClass = fssize.NTFSSize + _writelabelClass = fswritelabel.NTFSWriteLabel partedSystem = fileSystemType["ntfs"]
- def _fsckFailed(self, rc): - if rc != 0: - return True - return False - - def _getMinSize(self, info=None): - """ Set the minimum size for this filesystem. - - :keyword info: filesystem info buffer - :type info: str (output of :attr:`infofsProg`) - :rtype: None - """ - size = self._minSize - if self.exists and os.path.exists(self.device) and \ - util.find_program_in_path(self.resizefsProg): - minSize = None - buf = util.capture_output([self.resizefsProg, "-m", self.device]) - for l in buf.split("\n"): - if not l.startswith("Minsize"): - continue - try: - # ntfsresize uses SI unit prefixes - minSize = Size("%d mb" % int(l.split(":")[1].strip())) - except (IndexError, ValueError) as e: - minSize = None - log.warning("Unable to parse output for minimum size on %s: %s", self.device, e) - - if minSize is None: - log.warning("Unable to discover minimum size of filesystem " - "on %s", self.device) - else: - # add some padding to the min size - size = min(minSize * Decimal('1.1'), - minSize + Size("500 MiB")) - # make sure the padded and rounded min size is not larger than - # the current size - size = min(size.roundToNearest(self._resizefsUnit, - rounding=ROUND_UP), - self.currentSize) - if minSize < size: - log.debug("padding min size from %s up to %s", minSize, size) - else: - log.debug("using current size %s as min size", size) - - self._minInstanceSize = size - - @property - def minSize(self): - return self._minInstanceSize - - @property - def resizeArgs(self): - FMT = {B: "%d", KB: "%dK", MB: "%dM", GB: "%dG"}[self._resizefsUnit] - size_spec = FMT % self.targetSize.convertTo(self._resizefsUnit) - - # You must supply at least two '-f' options to ntfsresize or - # the proceed question will be presented to you. - return ["-ff", "-s", size_spec, self.device] - - register_device_format(NTFS)
@@ -1428,16 +1075,13 @@ class NFS(FS): """ NFS filesystem. """ _type = "nfs" _modules = ["nfs"] + _mountClass = fsmount.NFSMount
def _deviceCheck(self, devspec): if devspec is not None and ":" not in devspec: return "device must be of the form <host>:<path>" return None
- @property - def mountable(self): - return False - register_device_format(NFS)
@@ -1453,7 +1097,7 @@ class Iso9660FS(FS): """ ISO9660 filesystem. """ _type = "iso9660" _supported = True - _defaultMountOptions = ["ro"] + _mountClass = fsmount.Iso9660FSMount
register_device_format(Iso9660FS)
@@ -1461,6 +1105,7 @@ class Iso9660FS(FS): class NoDevFS(FS): """ nodev filesystem base class """ _type = "nodev" + _mountClass = fsmount.NoDevFSMount
def __init__(self, **kwargs): FS.__init__(self, **kwargs) @@ -1474,10 +1119,6 @@ def _deviceCheck(self, devspec): def type(self): return self.device
- @property - def mountType(self): - return self.device # this is probably set to the real/specific fstype - def notifyKernel(self): # NoDevFS should not need to tell the kernel anything. pass @@ -1488,7 +1129,7 @@ def notifyKernel(self): class DevPtsFS(NoDevFS): """ devpts filesystem. """ _type = "devpts" - _defaultMountOptions = ["gid=5", "mode=620"] + _mountClass = fsmount.DevPtsFSMount
register_device_format(DevPtsFS)
@@ -1511,8 +1152,6 @@ class TmpFS(NoDevFS): _supported = True # remounting can be used to change # the size of a live tmpfs mount - _resizefs = "mount" - _resizefsUnit = MiB # as tmpfs is part of the Linux kernel, # it is Linux-native _linuxNative = True @@ -1520,6 +1159,9 @@ class TmpFS(NoDevFS): # in the regard that the format is automatically created # once mounted _formattable = True + _sizeinfoClass = fssize.TmpFSSize + _mountClass = fsmount.TmpFSMount + _resizeClass = fsresize.TmpFSResize
def __init__(self, **kwargs): NoDevFS.__init__(self, **kwargs) @@ -1552,35 +1194,6 @@ def destroy(self, *args, **kwargs): """ pass
- def _getExistingSize(self, info=None): - """ Get current size of tmpfs filesystem using df. - - :param NoneType info: a dummy parameter - :rtype: Size - :returns: the current size of the filesystem, 0 if not found. - """ - if not self.status: - return Size(0) - - df = ["df", self.systemMountpoint, "--output=size"] - try: - (ret, out) = util.run_program_and_capture_output(df) - except OSError: - return Size(0) - - if ret: - return Size(0) - - lines = out.split() - if len(lines) != 2 or lines[0] != "1K-blocks": - return Size(0) - - return Size("%s KiB" % lines[1]) - - @property - def mountable(self): - return True - def _sizeOption(self, size): """ Returns a size option string appropriate for mounting tmpfs.
@@ -1593,8 +1206,7 @@ def _sizeOption(self, size): This is not impossible, since a special option for mounting is size=<percentage>%. """ - FMT = {KiB: "%dk", MiB: "%dm", GiB: "%dg"}[self._resizefsUnit] - return "size=%s" % (FMT % size.convertTo(self._resizefsUnit)) + return "size=%s" % (self._resize.size_fmt % size.convertTo(self._resize.unit))
def _getOptions(self): # Returns the regular mount options with the special size option, @@ -1640,12 +1252,6 @@ def _setDevice(self, value): # same, nothing actually needs to be set pass
- @property - def resizeArgs(self): - opts = super(TmpFS, self)._getOptions() - options = ("remount", opts, self._sizeOption(self.targetSize)) - return ['-o', ",".join(options), self._type, self.systemMountpoint] - def doResize(self): # Override superclass method to record whether mount options # should include an explicit size specification. @@ -1658,20 +1264,14 @@ def doResize(self):
class BindFS(FS): _type = "bind" - - @property - def mountable(self): - return True + _mountClass = fsmount.BindFSMount
register_device_format(BindFS)
class SELinuxFS(NoDevFS): _type = "selinuxfs" - - @property - def mountable(self): - return flags.selinux and super(SELinuxFS, self).mountable + _mountClass = fsmount.SELinuxFSMount
register_device_format(SELinuxFS)
@@ -1680,4 +1280,3 @@ class USBFS(NoDevFS): _type = "usbfs"
register_device_format(USBFS) - diff --git a/blivet/formats/fslabel.py b/blivet/formats/fslabel.py deleted file mode 100644 index 81c18e6..0000000 --- a/blivet/formats/fslabel.py +++ /dev/null @@ -1,205 +0,0 @@ -# fslabel.py -# Filesystem labeling classes for anaconda's storage configuration module. -# -# Copyright (C) 2013 Red Hat, Inc. -# -# This copyrighted material is made available to anyone wishing to use, -# modify, copy, or redistribute it subject to the terms and conditions of -# the GNU General Public License v.2, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY expressed or implied, including the implied warranties of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General -# Public License for more details. You should have received a copy of the -# GNU General Public License along with this program; if not, write to the -# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the -# source code or documentation are not subject to the GNU General Public -# License and may only be used or replicated with the express permission of -# Red Hat, Inc. -# -# Red Hat Author(s): Anne Mulhern amulhern@redhat.com - -import abc -import re - -from six import add_metaclass - -from .. import errors - -@add_metaclass(abc.ABCMeta) -class FSLabelApp(object): - """An abstract class that represents actions associated with a - filesystem's labeling application. - """ - - name = abc.abstractproperty( - doc="The name of the filesystem labeling application.") - - reads = abc.abstractproperty( - doc="Whether this application can read a label as well as write one.") - - _label_regex = abc.abstractproperty( - doc="Matches the string output by the label reading application.") - - @abc.abstractmethod - def _writeLabelArgs(self, fs): - """Returns a list of the arguments for writing a label. - - :param FS fs: a filesystem object - - :return: the arguments - :rtype: list of str - - It can be assumed in this function that fs.label is a str. - """ - raise NotImplementedError - - def setLabelCommand(self, fs): - """Get the command to label the filesystem. - - :param FS fs: a filesystem object - :return: the command - :rtype: list of str - - Raises an exception if fs.label is None. - """ - if fs.label is None: - raise errors.FSError("makes no sense to write a label when accepting default label") - return [self.name] + self._writeLabelArgs(fs) - - @abc.abstractmethod - def _readLabelArgs(self, fs): - """Returns a list of arguments for reading a label. - - :param FS fs: a filesystem object - :return: the arguments - :rtype: list of str - """ - raise NotImplementedError - - def readLabelCommand(self, fs): - """Get the command to read the filesystem label. - - :param FS fs: a filesystem object - :return: the command - :rtype: list of str - - Raises an FSError if this application can not read the label. - """ - if not self.reads: - raise errors.FSError("Application %s can not read the filesystem label." % self.name) - return [self.name] + self._readLabelArgs(fs) - - def extractLabel(self, labelstr): - """Extract the label from an output string. - - :param str labelstr: the string containing the label information - - :return: the label - :rtype: str - - Raises an FSError if the label can not be extracted. - """ - if not self.reads or self._label_regex is None: - raise errors.FSError("Unknown format for application %s" % self.name) - match = re.match(self._label_regex, labelstr) - if match is None: - raise errors.FSError("Unknown format for application %s" % self.name) - return match.group('label') - - -class E2Label(FSLabelApp): - """Application used by ext2 and its descendants.""" - - name = "e2label" - reads = True - - _label_regex = r'(?P<label>.*)' - - def _writeLabelArgs(self, fs): - return [fs.device, fs.label] - - def _readLabelArgs(self, fs): - return [fs.device] - -E2Label = E2Label() - -class DosFsLabel(FSLabelApp): - """Application used by FATFS.""" - - name = "dosfslabel" - reads = True - - _label_regex = r'(?P<label>.*)' - - def _writeLabelArgs(self, fs): - return [fs.device, fs.label] - - def _readLabelArgs(self, fs): - return [fs.device] - -DosFsLabel = DosFsLabel() - -class JFSTune(FSLabelApp): - """Application used by JFS.""" - - name = "jfs_tune" - reads = False - - _label_regex = property(lambda s: None) - - def _writeLabelArgs(self, fs): - return ["-L", fs.label, fs.device] - - def _readLabelArgs(self, fs): - raise NotImplementedError - -JFSTune = JFSTune() - -class ReiserFSTune(FSLabelApp): - """Application used by ReiserFS.""" - - name = "reiserfstune" - reads = False - - _label_regex = None - - def _writeLabelArgs(self, fs): - return ["-l", fs.label, fs.device] - - def _readLabelArgs(self, fs): - raise NotImplementedError - -ReiserFSTune = ReiserFSTune() - -class XFSAdmin(FSLabelApp): - """Application used by XFS.""" - - name = "xfs_admin" - reads = True - - _label_regex = r'label = "(?P<label>.*)"' - - def _writeLabelArgs(self, fs): - return ["-L", fs.label if fs.label != "" else "--", fs.device] - - def _readLabelArgs(self, fs): - return ["-l", fs.device] - -XFSAdmin = XFSAdmin() - -class NTFSLabel(FSLabelApp): - """Application used by NTFS.""" - - name = "ntfslabel" - reads = True - - _label_regex = r'(?P<label>.*)' - - def _writeLabelArgs(self, fs): - return [fs.device, fs.label] - - def _readLabelArgs(self, fs): - return [fs.device] - -NTFSLabel = NTFSLabel() diff --git a/blivet/formats/fslabeling.py b/blivet/formats/fslabeling.py deleted file mode 100644 index 0abeaf7..0000000 --- a/blivet/formats/fslabeling.py +++ /dev/null @@ -1,148 +0,0 @@ -# fslabeling.py -# Filesystem labeling classes for anaconda's storage configuration module. -# -# Copyright (C) 2014 Red Hat, Inc. -# -# This copyrighted material is made available to anyone wishing to use, -# modify, copy, or redistribute it subject to the terms and conditions of -# the GNU General Public License v.2, or (at your option) any later version. -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY expressed or implied, including the implied warranties of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General -# Public License for more details. You should have received a copy of the -# GNU General Public License along with this program; if not, write to the -# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the -# source code or documentation are not subject to the GNU General Public -# License and may only be used or replicated with the express permission of -# Red Hat, Inc. -# -# Red Hat Author(s): Anne Mulhern amulhern@redhat.com - -import abc - -from six import add_metaclass - -from . import fslabel - -@add_metaclass(abc.ABCMeta) -class FSLabeling(object): - """An abstract class that represents filesystem labeling actions. - """ - - default_label = abc.abstractproperty( - doc="Default label set on this filesystem at creation.") - - label_app = abc.abstractproperty( - doc="Post creation filesystem labeling application.") - - @abc.abstractmethod - def labelFormatOK(self, label): - """Returns True if this label is correctly formatted for this - filesystem, otherwise False. - - :param str label: the label for this filesystem - :rtype: bool - """ - raise NotImplementedError - - @abc.abstractmethod - def labelingArgs(self, label): - """Returns the arguments for writing the label during filesystem - creation. These arguments are intended to be passed to the - appropriate mkfs application. - - :param str label: the label to use - :return: the arguments - :rtype: list of str - """ - raise NotImplementedError - - -class Ext2FSLabeling(FSLabeling): - - default_label = "" - label_app = fslabel.E2Label - - def labelFormatOK(self, label): - return len(label) < 17 - - def labelingArgs(self, label): - return ["-L", label] - -class FATFSLabeling(FSLabeling): - - default_label = "NO NAME" - label_app = fslabel.DosFsLabel - - def labelFormatOK(self, label): - return len(label) < 12 - - def labelingArgs(self, label): - return ["-n", label] - -class JFSLabeling(FSLabeling): - - default_label = "" - label_app = fslabel.JFSTune - - def labelFormatOK(self, label): - return len(label) < 17 - - def labelingArgs(self, label): - return ["-L", label] - -class ReiserFSLabeling(FSLabeling): - - default_label = "" - label_app = fslabel.ReiserFSTune - - def labelFormatOK(self, label): - return len(label) < 17 - - def labelingArgs(self, label): - return ["-l", label] - -class XFSLabeling(FSLabeling): - - default_label = "" - label_app = fslabel.XFSAdmin - - def labelFormatOK(self, label): - return ' ' not in label and len(label) < 13 - - def labelingArgs(self, label): - return ["-L", label] - -class HFSLabeling(FSLabeling): - - default_label = "Untitled" - label_app = None - - def labelFormatOK(self, label): - return ':' not in label and len(label) < 28 and len(label) > 0 - - def labelingArgs(self, label): - return ["-l", label] - -class HFSPlusLabeling(FSLabeling): - - default_label = "Untitled" - label_app = None - - def labelFormatOK(self, label): - return ':' not in label and 0 < len(label) < 129 - - def labelingArgs(self, label): - return ["-v", label] - -class NTFSLabeling(FSLabeling): - - default_label = "" - label_app = fslabel.NTFSLabel - - def labelFormatOK(self, label): - return len(label) < 129 - - def labelingArgs(self, label): - return ["-L", label] diff --git a/tests/formats_test/fs_test.py b/tests/formats_test/fs_test.py index 834c759..2b22de3 100755 --- a/tests/formats_test/fs_test.py +++ b/tests/formats_test/fs_test.py @@ -137,7 +137,7 @@ def testResize(self): newsize = self.an_fs.currentSize * 2 self.an_fs.targetSize = newsize self.assertIsNone(self.an_fs.doResize()) - self.assertEqual(self.an_fs.size, newsize.roundToNearest(self.an_fs._resizefsUnit, rounding=ROUND_DOWN)) + self.assertEqual(self.an_fs.size, newsize.roundToNearest(self.an_fs._resize.unit, rounding=ROUND_DOWN))
def testShrink(self): # Can not shrink tmpfs, because its minimum size is its current size diff --git a/tests/formats_test/fslabeling.py b/tests/formats_test/fslabeling.py index 9c6dabb..4c76265 100644 --- a/tests/formats_test/fslabeling.py +++ b/tests/formats_test/fslabeling.py @@ -4,7 +4,7 @@ from six import add_metaclass
from tests import loopbackedtestcase -from blivet.errors import FSError +from blivet.errors import FSError, FSReadLabelError from blivet.size import Size
@add_metaclass(abc.ABCMeta) @@ -40,11 +40,11 @@ def testLabeling(self): an_fs = self._fs_class(device=self.loopDevices[0], label=self._invalid_label) self.assertIsNone(an_fs.create())
- with self.assertRaisesRegexp(FSError, "no application to read label"): + with self.assertRaises(FSReadLabelError): an_fs.readLabel()
an_fs.label = "an fs" - with self.assertRaisesRegexp(FSError, "no application to set label for filesystem"): + with self.assertRaises(FSError): an_fs.writeLabel()
def testCreating(self): @@ -81,7 +81,7 @@ def testLabeling(self): an_fs = self._fs_class(device=self.loopDevices[0], label=self._invalid_label) self.assertIsNone(an_fs.create())
- with self.assertRaisesRegexp(FSError, "no application to read label"): + with self.assertRaises(FSReadLabelError): an_fs.readLabel()
an_fs.label = "an fs" diff --git a/tests/formats_test/fstesting.py b/tests/formats_test/fstesting.py index 9a5d92e..59f8100 100644 --- a/tests/formats_test/fstesting.py +++ b/tests/formats_test/fstesting.py @@ -47,7 +47,7 @@ def _test_sizes(self, an_fs): else: expected_size = _size # If the size can be obtained it will not be 0 - if an_fs._infofs: + if an_fs._info.implemented: self.assertNotEqual(expected_size, Size(0)) self.assertTrue(expected_size <= self._DEVICE_SIZE) # Otherwise it will be 0, assuming the device was not initialized @@ -57,7 +57,7 @@ def _test_sizes(self, an_fs): self.assertEqual(an_fs.size, expected_size)
# Only the resizable filesystems can figure out their current min size - if an_fs._resizefs: + if an_fs._resize: expected_min_size = min_size else: expected_min_size = an_fs._minSize @@ -82,7 +82,7 @@ def testInstantiation(self): self.assertFalse(an_fs.exists) self.assertIsNone(an_fs.device) self.assertIsNone(an_fs.uuid) - self.assertEqual(an_fs.options, ",".join(an_fs.defaultMountOptions)) + self.assertEqual(an_fs.options, ",".join(an_fs._mount.options)) self.assertEqual(an_fs.resizable, False)
# sizes @@ -185,7 +185,7 @@ def testMountpoint(self):
def testResize(self): an_fs = self._fs_class() - if not an_fs.formattable: + if not an_fs.formattable or not an_fs.resizable: return an_fs.device = self.loopDevices[0] self.assertIsNone(an_fs.create()) @@ -210,7 +210,7 @@ def testResize(self): self.assertEqual(an_fs.targetSize, TARGET_SIZE) self.assertNotEqual(an_fs._size, TARGET_SIZE) self.assertIsNone(an_fs.doResize()) - ACTUAL_SIZE = TARGET_SIZE.roundToNearest(an_fs._resizefsUnit, rounding=ROUND_DOWN) + ACTUAL_SIZE = TARGET_SIZE.roundToNearest(an_fs._resize.unit, rounding=ROUND_DOWN) self.assertEqual(an_fs.size, ACTUAL_SIZE) self.assertEqual(an_fs._size, ACTUAL_SIZE) self._test_sizes(an_fs) @@ -288,7 +288,7 @@ def testShrink(self): if isinstance(an_fs, fs.NTFS): return self.assertIsNone(an_fs.doResize()) - ACTUAL_SIZE = TARGET_SIZE.roundToNearest(an_fs._resizefsUnit, rounding=ROUND_DOWN) + ACTUAL_SIZE = TARGET_SIZE.roundToNearest(an_fs._resize.unit, rounding=ROUND_DOWN) self.assertEqual(an_fs._size, ACTUAL_SIZE) self._test_sizes(an_fs)
@@ -345,7 +345,7 @@ def testTooBig2(self):
# CHECKME: size and target size will be adjusted attempted values # while currentSize will be actual value - TARGET_SIZE = BIG_SIZE.roundToNearest(an_fs._resizefsUnit, rounding=ROUND_DOWN) + TARGET_SIZE = BIG_SIZE.roundToNearest(an_fs._resize.unit, rounding=ROUND_DOWN) self.assertEqual(an_fs.targetSize, TARGET_SIZE) self.assertEqual(an_fs.size, an_fs.targetSize) self.assertEqual(an_fs.currentSize, old_size) diff --git a/tests/formats_test/labeling_test.py b/tests/formats_test/labeling_test.py index dfa6e79..4f69e81 100755 --- a/tests/formats_test/labeling_test.py +++ b/tests/formats_test/labeling_test.py @@ -71,18 +71,18 @@ def testGetLabelArgs(self):
# ReiserFS uses a -l flag reiserfs = self.fs["reiserfs"] - self.assertEqual(reiserfs._labelfs.label_app.setLabelCommand(reiserfs), + self.assertEqual(reiserfs._writelabel._setCommand, ["reiserfstune", "-l", "myfs", "/dev"], msg="reiserfs")
# JFS, XFS use a -L flag lflag_classes = [fs.JFS, fs.XFS] - for name, klass in ((k, v) for k, v in self.fs.items() if any(isinstance(v, c) for c in lflag_classes)): - self.assertEqual(klass._labelfs.label_app.setLabelCommand(klass), [klass._labelfs.label_app.name, "-L", "myfs", "/dev"], msg=name) + for name, klass in [(k, v) for k, v in self.fs.items() if any(isinstance(v, c) for c in lflag_classes)]: + self.assertEqual(klass._writelabel._setCommand, [str(klass._writelabel.ext), "-L", "myfs", "/dev"], msg=name)
# Ext2FS and descendants and FATFS do not use a flag noflag_classes = [fs.Ext2FS, fs.FATFS] - for name, klass in ((k, v) for k, v in self.fs.items() if any(isinstance(v, c) for c in noflag_classes)): - self.assertEqual(klass._labelfs.label_app.setLabelCommand(klass), [klass._labelfs.label_app.name, "/dev", "myfs"], msg=name) + for name, klass in [(k, v) for k, v in self.fs.items() if any(isinstance(v, c) for c in noflag_classes)]: + self.assertEqual(klass._writelabel._setCommand, [str(klass._writelabel.ext), "/dev", "myfs"], msg=name)
# all of the remaining are non-labeling so will accept any label label = "Houston, we have a problem!"
In reply to line 39 of blivet/formats/fs.py:
I think these imports could be squashed into fewer lines (2 or 3 at most)
In reply to line 749 of blivet/formats/fs.py:
This part of the docstring doesn't belong here.
In reply to line 749 of blivet/formats/fs.py:
There's nowhere better to put it, the whole thing is a little odd.
In reply to line 39 of blivet/formats/fs.py:
I don't think there's any compelling reason to, at least they're tidy and alphabetical and it won't look like it affects more things than it does if one is removed or added.
In reply to line 749 of blivet/formats/fs.py:
Fair enough, just please add the ``note::`` prefix before it so that it is marked as a note.
In reply to line 749 of blivet/formats/fs.py:
Done.
From: mulhern amulhern@redhat.com
Related: #12
Make use of the availability information in method formattable, controllable, supported, destroyable.
Signed-off-by: mulhern amulhern@redhat.com --- blivet/formats/__init__.py | 16 ++++++++++++++++ blivet/formats/luks.py | 18 ++++++++++++++++++ blivet/formats/lvmpv.py | 14 ++++++++++++++ blivet/formats/mdraid.py | 14 ++++++++++++++ blivet/formats/swap.py | 14 ++++++++++++++ 5 files changed, 76 insertions(+)
diff --git a/blivet/formats/__init__.py b/blivet/formats/__init__.py index 2acca22..b3e58fa 100644 --- a/blivet/formats/__init__.py +++ b/blivet/formats/__init__.py @@ -437,6 +437,12 @@ def _postDestroy(self, **kwargs): self.exists = False self.notifyKernel()
+ @property + def destroyable(self): + """ Do we have the facilities to destroy a format of this type. """ + # assumes wipefs is always available + return True + def setup(self, **kwargs): """ Activate the formatting.
@@ -458,6 +464,16 @@ def setup(self, **kwargs): self._setup(**kwargs) self._postSetup(**kwargs)
+ @property + def controllable(self): + """ Are external utilities available to allow this format to be both + setup and teared down. + + :returns: True if this format can be set up, otherwise False + :rtype: bool + """ + return True + def _preSetup(self, **kwargs): """ Return True if setup should proceed. """ if not self.exists: diff --git a/blivet/formats/luks.py b/blivet/formats/luks.py index c855649..f729e1e 100644 --- a/blivet/formats/luks.py +++ b/blivet/formats/luks.py @@ -29,6 +29,7 @@ from . import DeviceFormat, register_device_format from ..flags import flags from ..i18n import _, N_ +from ..tasks import availability
import logging log = logging.getLogger("blivet") @@ -44,6 +45,7 @@ class LUKS(DeviceFormat): _linuxNative = True # for clearpart _packages = ["cryptsetup"] # required packages _minSize = crypto.LUKS_METADATA_SIZE + _plugin = availability.BLOCKDEV_CRYPTO_PLUGIN
def __init__(self, **kwargs): """ @@ -150,6 +152,18 @@ def hasKey(self): (self._key_file and os.access(self._key_file, os.R_OK)))
@property + def formattable(self): + return super(LUKS, self).formattable and self._plugin.available + + @property + def supported(self): + return super(LUKS, self).supported and self._plugin.available + + @property + def controllable(self): + return super(LUKS, self).controllable and self._plugin.available + + @property def configured(self): """ To be ready we need a key or passphrase and a map name. """ return self.hasKey and self.mapName @@ -203,6 +217,10 @@ def _postCreate(self, **kwargs): self.mapName = "luks-%s" % self.uuid
@property + def destroyable(self): + return self._plugin.available + + @property def keyFile(self): """ Path to key file to be used in /etc/crypttab """ return self._key_file diff --git a/blivet/formats/lvmpv.py b/blivet/formats/lvmpv.py index 2a2e556..c1655d6 100644 --- a/blivet/formats/lvmpv.py +++ b/blivet/formats/lvmpv.py @@ -26,6 +26,7 @@ from ..storage_log import log_method_call from parted import PARTITION_LVM from ..devicelibs import lvm +from ..tasks import availability from ..i18n import N_ from ..size import Size from . import DeviceFormat, register_device_format @@ -46,6 +47,7 @@ class LVMPhysicalVolume(DeviceFormat): _minSize = lvm.LVM_PE_SIZE * 2 # one for metadata and one for data _packages = ["lvm2"] # required packages _ksMountpoint = "pv." + _plugin = availability.BLOCKDEV_LVM_PLUGIN
def __init__(self, **kwargs): """ @@ -94,6 +96,14 @@ def dict(self): "peStart": self.peStart, "dataAlignment": self.dataAlignment}) return d
+ @property + def formattable(self): + return super(LVMPhysicalVolume, self).formattable and self._plugin.available + + @property + def supported(self): + return super(LVMPhysicalVolume, self).supported and self._plugin.available + def _create(self, **kwargs): log_method_call(self, device=self.device, type=self.type, status=self.status) @@ -121,6 +131,10 @@ def _destroy(self, **kwargs): blockdev.lvm.pvscan(self.device)
@property + def destroyable(self): + return self._plugin.available + + @property def status(self): # XXX hack return (self.exists and self.vgName and diff --git a/blivet/formats/mdraid.py b/blivet/formats/mdraid.py index be4a915..5d8b22b 100644 --- a/blivet/formats/mdraid.py +++ b/blivet/formats/mdraid.py @@ -27,6 +27,7 @@ from . import DeviceFormat, register_device_format from ..flags import flags from ..i18n import N_ +from ..tasks import availability
import logging log = logging.getLogger("blivet") @@ -43,6 +44,7 @@ class MDRaidMember(DeviceFormat): _linuxNative = True # for clearpart _packages = ["mdadm"] # required packages _ksMountpoint = "raid." + _plugin = availability.BLOCKDEV_MDRAID_PLUGIN
def __init__(self, **kwargs): """ @@ -74,10 +76,22 @@ def dict(self): d.update({"mdUUID": self.mdUuid, "biosraid": self.biosraid}) return d
+ @property + def formattable(self): + return super(MDRaidMember, self).formattable and self._plugin.available + + @property + def supported(self): + return super(MDRaidMember, self).supported and self._plugin.available + def _destroy(self, **kwargs): blockdev.md.destroy(self.device)
@property + def destroyable(self): + return self._plugin.available + + @property def status(self): # XXX hack -- we don't have a nice way to see if the array is active return False diff --git a/blivet/formats/swap.py b/blivet/formats/swap.py index 26ebd4f..a93a70f 100644 --- a/blivet/formats/swap.py +++ b/blivet/formats/swap.py @@ -22,6 +22,7 @@
from parted import PARTITION_SWAP, fileSystemType from ..storage_log import log_method_call +from ..tasks import availability from . import DeviceFormat, register_device_format from ..size import Size from gi.repository import BlockDev as blockdev @@ -40,6 +41,7 @@ class SwapSpace(DeviceFormat): _formattable = True # can be formatted _supported = True # is supported _linuxNative = True # for clearpart + _plugin = availability.BLOCKDEV_SWAP_PLUGIN
#see rhbz#744129 for details _maxSize = Size("128 GiB") @@ -80,6 +82,18 @@ def dict(self): d.update({"priority": self.priority, "label": self.label}) return d
+ @property + def formattable(self): + return super(SwapSpace, self).formattable and self._plugin.available + + @property + def supported(self): + return super(SwapSpace, self).supported and self._plugin.available + + @property + def controllable(self): + return super(SwapSpace, self).controllable and self._plugin.available + def labeling(self): """Returns True as mkswap can write a label to the swap space.""" return True
From: mulhern amulhern@redhat.com
Related: #12
Signed-off-by: mulhern amulhern@redhat.com --- blivet/devices/btrfs.py | 3 +++ blivet/devices/disk.py | 3 +++ blivet/devices/dm.py | 5 +++++ blivet/devices/loop.py | 2 ++ blivet/devices/lvm.py | 2 ++ blivet/devices/md.py | 2 ++ blivet/devices/storage.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 60 insertions(+)
diff --git a/blivet/devices/btrfs.py b/blivet/devices/btrfs.py index ced4275..867e271 100644 --- a/blivet/devices/btrfs.py +++ b/blivet/devices/btrfs.py @@ -41,10 +41,13 @@ from .container import ContainerDevice from .raid import RaidDevice
+from ..tasks import availability + class BTRFSDevice(StorageDevice): """ Base class for BTRFS volume and sub-volume devices. """ _type = "btrfs" _packages = ["btrfs-progs"] + _external_dependencies = [availability.BLOCKDEV_BTRFS_PLUGIN]
def __init__(self, *args, **kwargs): """ Passing None or no name means auto-generate one like btrfs.%d """ diff --git a/blivet/devices/disk.py b/blivet/devices/disk.py index 39acea2..0d06c30 100644 --- a/blivet/devices/disk.py +++ b/blivet/devices/disk.py @@ -29,6 +29,7 @@ from ..storage_log import log_method_call from .. import udev from ..size import Size +from ..tasks import availability
from ..fcoe import fcoe
@@ -163,6 +164,7 @@ class DMRaidArrayDevice(DMDevice, ContainerDevice): _isDisk = True _formatClassName = property(lambda s: "dmraidmember") _formatUUIDAttr = property(lambda s: None) + _external_dependencies = [availability.BLOCKDEV_DM_PLUGIN]
def __init__(self, name, fmt=None, size=None, parents=None, sysfsPath=''): @@ -242,6 +244,7 @@ class MultipathDevice(DMDevice): _packages = ["device-mapper-multipath"] _partitionable = True _isDisk = True + _external_dependencies = [availability.MULTIPATH_APP]
def __init__(self, name, fmt=None, size=None, serial=None, parents=None, sysfsPath=''): diff --git a/blivet/devices/dm.py b/blivet/devices/dm.py index f980f64..10eca77 100644 --- a/blivet/devices/dm.py +++ b/blivet/devices/dm.py @@ -27,6 +27,7 @@ from .. import util from ..storage_log import log_method_call from .. import udev +from ..tasks import availability
import logging log = logging.getLogger("blivet") @@ -38,6 +39,10 @@ class DMDevice(StorageDevice): """ A device-mapper device """ _type = "dm" _devDir = "/dev/mapper" + _external_dependencies = [ + availability.KPARTX_APP, + availability.BLOCKDEV_DM_PLUGIN + ]
def __init__(self, name, fmt=None, size=None, dmUuid=None, uuid=None, target=None, exists=False, parents=None, sysfsPath=''): diff --git a/blivet/devices/loop.py b/blivet/devices/loop.py index aca8d44..301fdf4 100644 --- a/blivet/devices/loop.py +++ b/blivet/devices/loop.py @@ -24,6 +24,7 @@
from .. import errors from ..storage_log import log_method_call +from ..tasks import availability
import logging log = logging.getLogger("blivet") @@ -33,6 +34,7 @@ class LoopDevice(StorageDevice): """ A loop device. """ _type = "loop" + _external_dependencies = [availability.BLOCKDEV_LOOP_PLUGIN]
def __init__(self, name=None, fmt=None, size=None, sysfsPath=None, exists=False, parents=None): diff --git a/blivet/devices/lvm.py b/blivet/devices/lvm.py index d7526e4..a4bc712 100644 --- a/blivet/devices/lvm.py +++ b/blivet/devices/lvm.py @@ -35,6 +35,7 @@ from ..storage_log import log_method_call from .. import udev from ..size import Size, KiB, MiB, ROUND_UP, ROUND_DOWN +from ..tasks import availability
import logging log = logging.getLogger("blivet") @@ -445,6 +446,7 @@ class LVMLogicalVolumeDevice(DMDevice): _resizable = True _packages = ["lvm2"] _containerClass = LVMVolumeGroupDevice + _external_dependencies = default=[availability.BLOCKDEV_LVM_PLUGIN]
def __init__(self, name, parents=None, size=None, uuid=None, copies=1, logSize=None, segType=None, diff --git a/blivet/devices/md.py b/blivet/devices/md.py index 82b5ec8..d81f058 100644 --- a/blivet/devices/md.py +++ b/blivet/devices/md.py @@ -32,6 +32,7 @@ from ..storage_log import log_method_call from .. import udev from ..size import Size +from ..tasks import availability
import logging log = logging.getLogger("blivet") @@ -47,6 +48,7 @@ class MDRaidArrayDevice(ContainerDevice, RaidDevice): _devDir = "/dev/md" _formatClassName = property(lambda s: "mdmember") _formatUUIDAttr = property(lambda s: "mdUuid") + _external_dependencies = [availability.BLOCKDEV_MDRAID_PLUGIN]
def __init__(self, name, level=None, major=None, minor=None, size=None, memberDevices=None, totalDevices=None, diff --git a/blivet/devices/storage.py b/blivet/devices/storage.py index 052b397..aae22af 100644 --- a/blivet/devices/storage.py +++ b/blivet/devices/storage.py @@ -56,6 +56,7 @@ class StorageDevice(Device): _partitionable = False _isDisk = False _encrypted = False + _external_dependencies = []
def __init__(self, name, fmt=None, uuid=None, size=None, major=None, minor=None, @@ -748,3 +749,45 @@ def isNameValid(cls, name):
badchars = any(c in ('\x00', '/') for c in name) return not(badchars or name == '.' or name == '..') + + @classmethod + def typeExternalDependencies(cls): + """ A list of external dependencies of this device type. + + :returns: a set of external dependencies + :rtype: set of availability.ExternalResource + + The external dependencies include the dependencies of this + device type and of all superclass device types. + """ + return set( + d for p in cls.__mro__ if issubclass(p, StorageDevice) for d in p._external_dependencies + ) + + @classmethod + def unavailableTypeDependencies(cls): + """ A set of unavailable dependencies for this type. + + :return: the unavailable external dependencies for this type + :rtype: set of availability.ExternalResource + """ + return set(e for e in cls.typeExternalDependencies() if not e.available) + + @property + def externalDependencies(self): + """ A list of external dependencies of this device and its parents. + + :returns: the external dependencies of this device and all parents. + :rtype: set of availability.ExternalResource + """ + return set(d for p in self.ancestors for d in p.typeExternalDependencies()) + + @property + def unavailableDependencies(self): + """ Any unavailable external dependencies of this device or its + parents. + + :returns: A list of unavailable external dependencies. + :rtype: set of availability.externalResource + """ + return set(e for e in self.externalDependencies if not e.available)
From: mulhern amulhern@redhat.com
Related: #12
Signed-off-by: mulhern amulhern@redhat.com --- blivet/deviceaction.py | 12 +++++ tests/devices_test/dependencies_test.py | 90 +++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 tests/devices_test/dependencies_test.py
diff --git a/blivet/deviceaction.py b/blivet/deviceaction.py index 4d10765..c706d5c 100644 --- a/blivet/deviceaction.py +++ b/blivet/deviceaction.py @@ -148,6 +148,12 @@ def __init__(self, device): util.ObjectID.__init__(self) if not isinstance(device, StorageDevice): raise ValueError("arg 1 must be a StorageDevice instance") + + unavailable_dependencies = device.unavailableDependencies + if unavailable_dependencies: + dependencies_str = ", ".join(str(d) for d in unavailable_dependencies) + raise ValueError("device type %s requires unavailable_dependencies: %s" % (device.type, dependencies_str)) + self.device = device self.container = getattr(self.device, "container", None) self._applied = False @@ -516,6 +522,9 @@ def __init__(self, device, fmt=None): if self._format.exists: raise ValueError("specified format already exists")
+ if not self._format.formattable: + raise ValueError("resource to create this format %s is unavailable" % fmt) + def apply(self): """ apply changes related to the action to the device(s) """ if self._applied: @@ -638,6 +647,9 @@ def __init__(self, device): DeviceAction.__init__(self, device) self.origFormat = self.device.format
+ if not device.format.destroyable: + raise ValueError("resource to destroy this format type %s is unavailable" % device.format.type) + def apply(self): if self._applied: return diff --git a/tests/devices_test/dependencies_test.py b/tests/devices_test/dependencies_test.py new file mode 100644 index 0000000..9299855 --- /dev/null +++ b/tests/devices_test/dependencies_test.py @@ -0,0 +1,90 @@ +#!/usr/bin/python +# vim:set fileencoding=utf-8 + +import unittest + +from blivet.deviceaction import ActionCreateDevice +from blivet.deviceaction import ActionDestroyDevice + +from blivet.deviceaction import ActionCreateFormat +from blivet.deviceaction import ActionDestroyFormat + +from blivet.devices import DiskDevice +from blivet.devices import LUKSDevice +from blivet.devices import MDRaidArrayDevice +from blivet.devices import PartitionDevice + +from blivet.formats import getFormat + +from blivet.tasks import availability + +class DeviceDependenciesTestCase(unittest.TestCase): + """Test external device dependencies. """ + + def testDependencies(self): + dev1 = DiskDevice("name", fmt=getFormat("mdmember")) + dev2 = DiskDevice("other", fmt=getFormat("mdmember")) + dev = MDRaidArrayDevice("dev", level="raid1", parents=[dev1,dev2]) + luks = LUKSDevice("luks", parents=[dev]) + + # a parent's dependencies are a subset of its child's. + for d in dev.externalDependencies: + self.assertIn(d, luks.externalDependencies) + + # make sure that there's at least something in these dependencies + self.assertTrue(len(luks.externalDependencies) > 0) + +class MockingDeviceDependenciesTestCase(unittest.TestCase): + """Test availability of external device dependencies. """ + + def setUp(self): + dev1 = DiskDevice("name", fmt=getFormat("mdmember")) + dev2 = DiskDevice("other") + self.part = PartitionDevice("part", fmt=getFormat("mdmember"), parents=[dev2]) + self.dev = MDRaidArrayDevice("dev", level="raid1", parents=[dev1, self.part], fmt=getFormat("luks")) + self.luks = LUKSDevice("luks", parents=[self.dev], fmt=getFormat("ext4")) + + self.mdraid_method = availability.BLOCKDEV_MDRAID_PLUGIN._method + self.dm_method = availability.BLOCKDEV_DM_PLUGIN._method + self.cache_availability = availability.CACHE_AVAILABILITY + + def testAvailabilityMDRAIDplugin(self): + + availability.CACHE_AVAILABILITY = False + availability.BLOCKDEV_DM_PLUGIN._method = availability.AvailableMethod + + # if the plugin is not in, there's nothing to test + self.assertIn(availability.BLOCKDEV_MDRAID_PLUGIN, self.luks.externalDependencies) + + # dev is not among its unavailable dependencies + availability.BLOCKDEV_MDRAID_PLUGIN._method = availability.AvailableMethod + self.assertNotIn(availability.BLOCKDEV_MDRAID_PLUGIN, self.luks.unavailableDependencies) + self.assertIsNotNone(ActionCreateDevice(self.luks)) + self.assertIsNotNone(ActionDestroyDevice(self.luks)) + self.assertIsNotNone(ActionCreateFormat(self.luks, fmt=getFormat("macefi"))) + self.assertIsNotNone(ActionDestroyFormat(self.luks)) + + # dev is among the unavailable dependencies + availability.BLOCKDEV_MDRAID_PLUGIN._method = availability.UnavailableMethod + self.assertIn(availability.BLOCKDEV_MDRAID_PLUGIN, self.luks.unavailableDependencies) + with self.assertRaises(ValueError): + ActionCreateDevice(self.luks) + with self.assertRaises(ValueError): + ActionDestroyDevice(self.dev) + with self.assertRaises(ValueError): + ActionCreateFormat(self.dev) + with self.assertRaises(ValueError): + ActionDestroyFormat(self.dev) + + def tearDown(self): + availability.BLOCKDEV_MDRAID_PLUGIN._method = self.mdraid_method + availability.BLOCKDEV_DM_PLUGIN._method = self.dm_method + + availability.CACHE_AVAILABILITY = False + availability.BLOCKDEV_MDRAID_PLUGIN.available # pylint: disable=pointless-statement + availability.BLOCKDEV_DM_PLUGIN.available # pylint: disable=pointless-statement + + availability.CACHE_AVAILABILITY = self.cache_availability + +if __name__ == "__main__": + unittest.main()
In reply to line 35 of tests/devices_test/dependencies_test.py:
``self.assertGreater`` would give us better information in case of failure ("*X is not bigger than 0*" instead of "*False is not True*").
In reply to line 35 of tests/devices_test/dependencies_test.py:
better, done.
From: mulhern amulhern@redhat.com
Related: #12
But log info about what plugin was missing.
Signed-off-by: mulhern amulhern@redhat.com --- blivet/__init__.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-)
diff --git a/blivet/__init__.py b/blivet/__init__.py index 6775b65..7725c43 100644 --- a/blivet/__init__.py +++ b/blivet/__init__.py @@ -63,17 +63,27 @@ def cb(self, exn): log_bd_message = lambda level, msg: program_log.info(msg)
# initialize the libblockdev library +from gi.repository import GLib from gi.repository import BlockDev as blockdev -_REQUIRED_PLUGIN_NAMES = set(("lvm", "btrfs", "swap", "crypto", "loop", "mdraid", "mpath", "dm")) -_required_plugins = blockdev.plugin_specs_from_names(_REQUIRED_PLUGIN_NAMES) +_REQUESTED_PLUGIN_NAMES = set(("lvm", "btrfs", "swap", "crypto", "loop", "mdraid", "mpath", "dm")) +_requested_plugins = blockdev.plugin_specs_from_names(_REQUESTED_PLUGIN_NAMES) if not blockdev.is_initialized(): - if not blockdev.try_init(require_plugins=_required_plugins, log_func=log_bd_message): - raise RuntimeError("Failed to initialize the libblockdev library with all required plugins") + try: + blockdev.try_init(require_plugins=_requested_plugins, log_func=log_bd_message) + except GLib.GError: + pass else: avail_plugs = set(blockdev.get_available_plugin_names()) - if avail_plugs != _REQUIRED_PLUGIN_NAMES: - if not blockdev.reinit(require_plugins=_required_plugins, reload=False, log_func=log_bd_message): - raise RuntimeError("Failed to initialize the libblockdev library with all required plugins") + if avail_plugs != _REQUESTED_PLUGIN_NAMES: + try: + blockdev.reinit(require_plugins=_requested_plugins, reload=False, log_func=log_bd_message) + except GLib.GError: + pass + +avail_plugs = set(blockdev.get_available_plugin_names()) +missing_plugs = _REQUESTED_PLUGIN_NAMES - avail_plugs +for p in missing_plugs: + log.info("Failed to load plugin %s", p)
def enable_installer_mode(): """ Configure the module for use by anaconda (OS installer). """
From: mulhern amulhern@redhat.com
Related: #12
All plugin are no longer required, but libblockdev is, to run the plugins. dosfstools is no longer required. e2fsprogs remains because it is blivet's default filesystem type.
Signed-off-by: mulhern amulhern@redhat.com --- python-blivet.spec | 4 ---- 1 file changed, 4 deletions(-)
diff --git a/python-blivet.spec b/python-blivet.spec index 9d499be..6fbbcef 100644 --- a/python-blivet.spec +++ b/python-blivet.spec @@ -36,12 +36,10 @@ Requires: util-linux >= %{utillinuxver} Requires: python-pyudev Requires: parted >= %{partedver} Requires: pyparted >= %{pypartedver} -Requires: dosfstools Requires: e2fsprogs >= %{e2fsver} Requires: lsof Requires: libselinux-python Requires: libblockdev >= %{libblockdevver} -Requires: libblockdev-plugins-all >= %{libblockdevver} Requires: libselinux-python Requires: rpm-python
@@ -60,9 +58,7 @@ Requires: parted >= %{partedver} Requires: python3-pyparted >= %{pypartedver} Requires: libselinux-python3 Requires: libblockdev >= %{libblockdevver} -Requires: libblockdev-plugins-all >= %{libblockdevver} Requires: util-linux >= %{utillinuxver} -Requires: dosfstools Requires: e2fsprogs >= %{e2fsver} Requires: lsof Requires: rpm-python3
From: mulhern amulhern@redhat.com
Related: #12
Signed-off-by: mulhern amulhern@redhat.com --- blivet/devicelibs/lvm.py | 4 ++++ 1 file changed, 4 insertions(+)
diff --git a/blivet/devicelibs/lvm.py b/blivet/devicelibs/lvm.py index 6366703..e781d1d 100644 --- a/blivet/devicelibs/lvm.py +++ b/blivet/devicelibs/lvm.py @@ -30,6 +30,7 @@ from ..size import Size from ..i18n import N_ from ..flags import flags +from ..tasks import availability
# some of lvm's defaults that we have no way to ask it for LVM_PE_START = Size("1 MiB") @@ -82,6 +83,9 @@ def _set_global_config(): blockdev.lvm.set_global_config(config_string)
def needs_config_refresh(fn): + if not availability.BLOCKDEV_LVM_PLUGIN.available: + return lambda *args, **kwargs: None + def fn_with_refresh(*args, **kwargs): ret = fn(*args, **kwargs) _set_global_config()
From: mulhern amulhern@redhat.com
Related: #12
Signed-off-by: mulhern amulhern@redhat.com --- blivet/devicelibs/btrfs.py | 3 +++ blivet/devicelibs/crypto.py | 3 +++ blivet/devicelibs/lvm.py | 2 ++ blivet/devicelibs/mdraid.py | 3 +++ 4 files changed, 11 insertions(+)
diff --git a/blivet/devicelibs/btrfs.py b/blivet/devicelibs/btrfs.py index 65a6ca6..c8d9319 100644 --- a/blivet/devicelibs/btrfs.py +++ b/blivet/devicelibs/btrfs.py @@ -22,6 +22,7 @@
from . import raid from ..size import Size +from ..tasks import availability
import logging log = logging.getLogger("blivet") @@ -35,3 +36,5 @@ RAID_levels = raid.RAIDLevels(["raid0", "raid1", "raid10", "single"])
metadata_levels = raid.RAIDLevels(["raid0", "raid1", "raid10", "single", "dup"]) + +EXTERNAL_DEPENDENCIES = [availability.BLOCKDEV_BTRFS_PLUGIN] diff --git a/blivet/devicelibs/crypto.py b/blivet/devicelibs/crypto.py index 5fc867d..474c905 100644 --- a/blivet/devicelibs/crypto.py +++ b/blivet/devicelibs/crypto.py @@ -21,6 +21,9 @@ #
from ..size import Size +from ..tasks import availability
LUKS_METADATA_SIZE = Size("2 MiB") MIN_CREATE_ENTROPY = 256 # bits + +EXTERNAL_DEPENDENCIES = [availability.BLOCKDEV_CRYPTO_PLUGIN] diff --git a/blivet/devicelibs/lvm.py b/blivet/devicelibs/lvm.py index e781d1d..0b7658c 100644 --- a/blivet/devicelibs/lvm.py +++ b/blivet/devicelibs/lvm.py @@ -48,6 +48,8 @@ KNOWN_THPOOL_PROFILES = (ThPoolProfile("thin-generic", N_("Generic")), ThPoolProfile("thin-performance", N_("Performance")))
+EXTERNAL_DEPENDENCIES = [availability.BLOCKDEV_LVM_PLUGIN] + # Start config_args handling code # # Theoretically we can handle all that can be handled with the LVM --config diff --git a/blivet/devicelibs/mdraid.py b/blivet/devicelibs/mdraid.py index bbcd02f..2f63f8e 100644 --- a/blivet/devicelibs/mdraid.py +++ b/blivet/devicelibs/mdraid.py @@ -22,6 +22,7 @@
from ..size import Size from . import raid +from ..tasks import availability
import logging log = logging.getLogger("blivet") @@ -40,3 +41,5 @@ def isRaidLevel(cls, level): hasattr(level, 'get_size')
RAID_levels = MDRaidLevels(["raid0", "raid1", "raid4", "raid5", "raid6", "raid10", "linear"]) + +EXTERNAL_DEPENDENCIES = [availability.BLOCKDEV_MDRAID_PLUGIN]
From: mulhern amulhern@redhat.com
Related: #12
Signed-off-by: mulhern amulhern@redhat.com --- blivet/devicefactory.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/blivet/devicefactory.py b/blivet/devicefactory.py index 553d011..d2c1a67 100644 --- a/blivet/devicefactory.py +++ b/blivet/devicefactory.py @@ -66,7 +66,10 @@ def get_supported_raid_levels(device_type): elif device_type == DEVICE_TYPE_MD: pkg = mdraid
- return set(pkg.RAID_levels) if pkg else set() + if pkg and all(d.available for d in pkg.EXTERNAL_DEPENDENCIES): + return set(pkg.RAID_levels) + else: + return set()
def get_device_type(device): # the only time we should ever get a thin pool here is when we're removing
I think this commit should be squashed with the previous one.
I'ld rather keep as is, they are distinct decisions, although obviously related.
From: mulhern amulhern@redhat.com
Related: #12
Signed-off-by: mulhern amulhern@redhat.com --- blivet/devicefactory.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-)
diff --git a/blivet/devicefactory.py b/blivet/devicefactory.py index d2c1a67..90f66bf 100644 --- a/blivet/devicefactory.py +++ b/blivet/devicefactory.py @@ -22,7 +22,9 @@
from .storage_log import log_method_call from .errors import DeviceFactoryError, StorageError -from .devices import LUKSDevice +from .devices import BTRFSDevice, DiskDevice +from .devices import LUKSDevice, LVMLogicalVolumeDevice, LVMThinPoolDevice +from .devices import PartitionDevice, MDRaidArrayDevice from .formats import getFormat from .devicelibs import btrfs from .devicelibs import mdraid @@ -49,6 +51,29 @@ DEVICE_TYPE_DISK = 4 DEVICE_TYPE_LVM_THINP = 5
+def is_supported_device_type(device_type): + """ Return True if blivet supports this device type. + + :param device_type: an enumeration indicating the device type + :type device_type: int + + :returns: True if this device type is supported + :rtype: bool + """ + devices = [] + if device_type == DEVICE_TYPE_BTRFS: + devices = [BTRFSDevice] + elif device_type == DEVICE_TYPE_DISK: + devices = [DiskDevice] + elif device_type in (DEVICE_TYPE_LVM, DEVICE_TYPE_LVM_THINP): + devices = [LVMLogicalVolumeDevice, LVMThinPoolDevice] + elif device_type == DEVICE_TYPE_PARTITION: + devices = [PartitionDevice] + elif device_type == DEVICE_TYPE_MD: + devices = [MDRaidArrayDevice] + + return all(c.unavailableTypeDependencies() == set() for c in devices) + def get_supported_raid_levels(device_type): """ Return the supported raid levels for this device type.
In reply to line 75 of blivet/devicefactory.py:
I'd prefer:
``` return not any(c.unavailableTypeDependencies() for c in device) ```
here if that's a possible replacement.
In reply to line 75 of blivet/devicefactory.py:
done.
From: mulhern amulhern@redhat.com
Related: #12
Use the methods that check the availability of the specific tasks required. With the additional granularity can test a bunch more things.
Note that it is not possible to use resizable to check availability of test.
Signed-off-by: mulhern amulhern@redhat.com --- tests/formats_test/fslabeling.py | 18 +++++++++++-- tests/formats_test/fstesting.py | 55 +++++++++++++++++++++++++--------------- 2 files changed, 51 insertions(+), 22 deletions(-)
diff --git a/tests/formats_test/fslabeling.py b/tests/formats_test/fslabeling.py index 4c76265..200cffe 100644 --- a/tests/formats_test/fslabeling.py +++ b/tests/formats_test/fslabeling.py @@ -26,8 +26,10 @@ def __init__(self, methodName='runTest'):
def setUp(self): an_fs = self._fs_class() - if not an_fs.utilsAvailable: - self.skipTest("utilities unavailable for filesystem %s" % an_fs.name) + if not an_fs.formattable: + self.skipTest("can not create filesystem %s" % an_fs.name) + if not an_fs.labeling(): + self.skipTest("can not label filesystem %s" % an_fs.name) super(LabelingAsRoot, self).setUp()
def testLabeling(self): @@ -38,6 +40,8 @@ def testLabeling(self): * raise an exception when relabeling the filesystem """ an_fs = self._fs_class(device=self.loopDevices[0], label=self._invalid_label) + if an_fs._readlabel.availabilityErrors or not an_fs.relabels(): + self.skipTest("can not read or write label for filesystem %s" % an_fs.name) self.assertIsNone(an_fs.create())
with self.assertRaises(FSReadLabelError): @@ -79,6 +83,8 @@ def testLabeling(self): * raise an exception when relabeling with an invalid label """ an_fs = self._fs_class(device=self.loopDevices[0], label=self._invalid_label) + if an_fs._readlabel.availabilityErrors or not an_fs.relabels(): + self.skipTest("can not read or write label for filesystem %s" % an_fs.name) self.assertIsNone(an_fs.create())
with self.assertRaises(FSReadLabelError): @@ -116,6 +122,8 @@ def testLabeling(self): * raise an exception when relabeling with an invalid label """ an_fs = self._fs_class(device=self.loopDevices[0], label=self._invalid_label) + if an_fs._readlabel.availabilityErrors or not an_fs.relabels(): + self.skipTest("can not read or write label for filesystem %s" % an_fs.name) self.assertIsNone(an_fs.create()) self.assertEqual(an_fs.readLabel(), an_fs._labelfs.default_label)
@@ -140,6 +148,8 @@ def testCreating(self): Verify that the filesystem has that label. """ an_fs = self._fs_class(device=self.loopDevices[0], label="start") + if an_fs._readlabel.availabilityErrors: + self.skipTest("can not read label for filesystem %s" % an_fs.name) self.assertIsNone(an_fs.create()) self.assertEqual(an_fs.readLabel(), "start")
@@ -148,6 +158,8 @@ def testCreatingNone(self): Verify that the filesystem has the default label. """ an_fs = self._fs_class(device=self.loopDevices[0], label=None) + if an_fs._readlabel.availabilityErrors: + self.skipTest("can not read label for filesystem %s" % an_fs.name) self.assertIsNone(an_fs.create()) self.assertEqual(an_fs.readLabel(), an_fs._labelfs.default_label)
@@ -156,5 +168,7 @@ def testCreatingEmpty(self): Verify that the filesystem has the empty label. """ an_fs = self._fs_class(device=self.loopDevices[0], label="") + if an_fs._readlabel.availabilityErrors: + self.skipTest("can not read label for filesystem %s" % an_fs.name) self.assertIsNone(an_fs.create()) self.assertEqual(an_fs.readLabel(), "") diff --git a/tests/formats_test/fstesting.py b/tests/formats_test/fstesting.py index 59f8100..b8c4944 100644 --- a/tests/formats_test/fstesting.py +++ b/tests/formats_test/fstesting.py @@ -26,9 +26,6 @@ def __init__(self, methodName='runTest'): super(FSAsRoot, self).__init__(methodName=methodName, deviceSpec=[self._DEVICE_SIZE])
def setUp(self): - an_fs = self._fs_class() - if not an_fs.utilsAvailable: - self.skipTest("utilities unavailable for filesystem %s" % an_fs.name) super(FSAsRoot, self).setUp()
def _test_sizes(self, an_fs): @@ -112,7 +109,7 @@ def testInstantiation(self): def testCreation(self): an_fs = self._fs_class() if not an_fs.formattable: - return + self.skipTest("can not create filesystem %s" % an_fs.name) an_fs.device = self.loopDevices[0] self.assertIsNone(an_fs.create()) self.assertEqual(an_fs.resizable, False) @@ -130,8 +127,8 @@ def testCreation(self):
def testLabeling(self): an_fs = self._fs_class() - if not an_fs.labeling(): - return + if not an_fs.formattable or not an_fs.labeling(): + self.skipTest("can not label filesystem %s" % an_fs.name) an_fs.device = self.loopDevices[0] an_fs.label = "label" self.assertTrue(an_fs.labelFormatOK("label")) @@ -144,8 +141,8 @@ def testLabeling(self):
def testRelabeling(self): an_fs = self._fs_class() - if not an_fs.labeling(): - return + if not an_fs.formattable or not an_fs.labeling(): + self.skipTest("can not label filesystem %s" % an_fs.name) an_fs.device = self.loopDevices[0] self.assertIsNone(an_fs.create()) an_fs.label = "label" @@ -160,9 +157,9 @@ def testMounting(self): an_fs = self._fs_class() # FIXME: BTRFS fails to mount if isinstance(an_fs, fs.BTRFS): - return + self.skipTest("no mounting filesystem %s" % an_fs.name) if not an_fs.formattable: - return + self.skipTest("can not create filesystem %s" % an_fs.name) an_fs.device = self.loopDevices[0] self.assertIsNone(an_fs.create()) self.assertTrue(an_fs.testMount()) @@ -171,9 +168,9 @@ def testMountpoint(self): an_fs = self._fs_class() # FIXME: BTRFS fails to mount if isinstance(an_fs, fs.BTRFS): - return + self.skipTest("no mounting filesystem %s" % an_fs.name) if not an_fs.formattable or not an_fs.mountable: - return + self.skipTest("can not create or mount filesystem %s" % an_fs.name) an_fs.device = self.loopDevices[0] self.assertIsNone(an_fs.create()) mountpoint = tempfile.mkdtemp() @@ -185,8 +182,8 @@ def testMountpoint(self):
def testResize(self): an_fs = self._fs_class() - if not an_fs.formattable or not an_fs.resizable: - return + if not an_fs.formattable: + self.skipTest("can not create filesystem %s" % an_fs.name) an_fs.device = self.loopDevices[0] self.assertIsNone(an_fs.create()) an_fs.updateSizeInfo() @@ -225,9 +222,12 @@ def testNoExplicitTargetSize(self): # gets value of _size in constructor, so if _size is set to not-zero # in constructor call behavior would be different. if not self._resizable: - return + self.skipTest("Not checking resize for this test category.")
an_fs = self._fs_class() + if not an_fs.formattable: + self.skipTest("can not create filesystem %s" % an_fs.name) + an_fs.device = self.loopDevices[0] self.assertIsNone(an_fs.create()) an_fs.updateSizeInfo() @@ -245,10 +245,13 @@ def testNoExplicitTargetSize2(self): resize action resizes filesystem to that size. """ if not self._resizable: - return + self.skipTest("Not checking resize for this test category.")
SIZE = Size("64 MiB") an_fs = self._fs_class(size=SIZE) + if not an_fs.formattable: + self.skipTest("can not create filesystem %s" % an_fs.name) + an_fs.device = self.loopDevices[0] self.assertIsNone(an_fs.create()) an_fs.updateSizeInfo() @@ -264,9 +267,12 @@ def testNoExplicitTargetSize2(self):
def testShrink(self): if not self._resizable: - return + self.skipTest("Not checking resize for this test category.")
an_fs = self._fs_class() + if not an_fs.formattable: + self.skipTest("can not create filesystem %s" % an_fs.name) + an_fs.device = self.loopDevices[0] self.assertIsNone(an_fs.create()) an_fs.updateSizeInfo() @@ -294,9 +300,12 @@ def testShrink(self):
def testTooSmall(self): if not self._resizable: - return + self.skipTest("Not checking resize for this test category.")
an_fs = self._fs_class() + if not an_fs.formattable: + self.skipTest("can not create or resize filesystem %s" % an_fs.name) + an_fs.device = self.loopDevices[0] self.assertIsNone(an_fs.create()) an_fs.updateSizeInfo() @@ -311,9 +320,12 @@ def testTooSmall(self):
def testTooBig(self): if not self._resizable: - return + self.skipTest("Not checking resize for this test category.")
an_fs = self._fs_class() + if not an_fs.formattable: + self.skipTest("can not create filesystem %s" % an_fs.name) + an_fs.device = self.loopDevices[0] self.assertIsNone(an_fs.create()) an_fs.updateSizeInfo() @@ -328,9 +340,12 @@ def testTooBig(self):
def testTooBig2(self): if not self._resizable: - return + self.skipTest("Not checking resize for this test category.")
an_fs = self._fs_class() + if not an_fs.formattable: + self.skipTest("can not create filesystem %s" % an_fs.name) + an_fs.device = self.loopDevices[0] self.assertIsNone(an_fs.create()) an_fs.updateSizeInfo()
From: mulhern amulhern@redhat.com
Related: #12
Check if the filesystem has resizing abilities instead.
Signed-off-by: mulhern amulhern@redhat.com --- tests/formats_test/fs_test.py | 14 ------------ tests/formats_test/fstesting.py | 49 +++++++++++++++++++++-------------------- 2 files changed, 25 insertions(+), 38 deletions(-)
diff --git a/tests/formats_test/fs_test.py b/tests/formats_test/fs_test.py index 2b22de3..71fa524 100755 --- a/tests/formats_test/fs_test.py +++ b/tests/formats_test/fs_test.py @@ -12,7 +12,6 @@
class Ext2FSTestCase(fstesting.FSAsRoot): _fs_class = fs.Ext2FS - _resizable = True
class Ext3FSTestCase(Ext2FSTestCase): _fs_class = fs.Ext3FS @@ -22,42 +21,34 @@ class Ext4FSTestCase(Ext3FSTestCase):
class FATFSTestCase(fstesting.FSAsRoot): _fs_class = fs.FATFS - _resizable = False
class EFIFSTestCase(FATFSTestCase): _fs_class = fs.EFIFS
class BTRFSTestCase(fstesting.FSAsRoot): _fs_class = fs.BTRFS - _resizable = False
@unittest.skip("Unable to create GFS2 filesystem.") class GFS2TestCase(fstesting.FSAsRoot): _fs_class = fs.GFS2 - _resizable = False
class JFSTestCase(fstesting.FSAsRoot): _fs_class = fs.JFS - _resizable = False
class ReiserFSTestCase(fstesting.FSAsRoot): _fs_class = fs.ReiserFS - _resizable = False
class XFSTestCase(fstesting.FSAsRoot): _fs_class = fs.XFS - _resizable = False
class HFSTestCase(fstesting.FSAsRoot): _fs_class = fs.HFS - _resizable = False
class AppleBootstrapFSTestCase(HFSTestCase): _fs_class = fs.AppleBootstrapFS
class HFSPlusTestCase(fstesting.FSAsRoot): _fs_class = fs.HFSPlus - _resizable = False
class MacEFIFSTestCase(HFSPlusTestCase): _fs_class = fs.MacEFIFS @@ -65,24 +56,20 @@ class MacEFIFSTestCase(HFSPlusTestCase): @unittest.skip("Unable to create because NTFS._formattable is False.") class NTFSTestCase(fstesting.FSAsRoot): _fs_class = fs.NTFS - _resizable = True
@unittest.skip("Unable to create because device fails deviceCheck().") class NFSTestCase(fstesting.FSAsRoot): _fs_class = fs.NFS - _resizable = False
class NFSv4TestCase(NFSTestCase): _fs_class = fs.NFSv4
class Iso9660FS(fstesting.FSAsRoot): _fs_class = fs.Iso9660FS - _resizable = False
@unittest.skip("Too strange to test using this framework.") class NoDevFSTestCase(fstesting.FSAsRoot): _fs_class = fs.NoDevFS - _resizable = False
class DevPtsFSTestCase(NoDevFSTestCase): _fs_class = fs.DevPtsFS @@ -104,7 +91,6 @@ class USBFSTestCase(NoDevFSTestCase):
class BindFSTestCase(fstesting.FSAsRoot): _fs_class = fs.BindFS - _resizable = False
class SimpleTmpFSTestCase(loopbackedtestcase.LoopBackedTestCase):
diff --git a/tests/formats_test/fstesting.py b/tests/formats_test/fstesting.py index b8c4944..3740f94 100644 --- a/tests/formats_test/fstesting.py +++ b/tests/formats_test/fstesting.py @@ -11,15 +11,21 @@ from blivet.size import Size, ROUND_DOWN from blivet.formats import fs
+def can_resize(an_fs): + """ Returns True if this filesystem has all necessary resizing tools + available. + + :param an_fs: a filesystem object + """ + resize_tasks = (an_fs._resize, an_fs._sizeinfo, an_fs._minsize) + return all(not t.availabilityErrors for t in resize_tasks) + @add_metaclass(abc.ABCMeta) class FSAsRoot(loopbackedtestcase.LoopBackedTestCase):
_fs_class = abc.abstractproperty( doc="The class of the filesystem being tested on.")
- _resizable = abc.abstractproperty( - doc="Should we expect to be able to resize this filesystem.") - _DEVICE_SIZE = Size("100 MiB")
def __init__(self, methodName='runTest'): @@ -83,7 +89,7 @@ def testInstantiation(self): self.assertEqual(an_fs.resizable, False)
# sizes - expected_min_size = Size(0) if self._resizable else an_fs._minSize + expected_min_size = Size(0) if can_resize(an_fs) else an_fs._minSize self.assertEqual(an_fs.minSize, expected_min_size)
self.assertEqual(an_fs.maxSize, an_fs._maxSize) @@ -97,7 +103,7 @@ def testInstantiation(self): an_fs = self._fs_class(size=NEW_SIZE)
# sizes - expected_min_size = Size(0) if self._resizable else an_fs._minSize + expected_min_size = Size(0) if can_resize(an_fs) else an_fs._minSize self.assertEqual(an_fs.minSize, expected_min_size)
self.assertEqual(an_fs.maxSize, an_fs._maxSize) @@ -116,7 +122,7 @@ def testCreation(self): self.assertTrue(an_fs.exists) self.assertIsNone(an_fs.doCheck())
- expected_min_size = Size(0) if self._resizable else an_fs._minSize + expected_min_size = Size(0) if can_resize(an_fs) else an_fs._minSize self.assertEqual(an_fs.minSize, expected_min_size)
self.assertEqual(an_fs.maxSize, an_fs._maxSize) @@ -192,7 +198,7 @@ def testResize(self): # CHECKME: target size is still 0 after updatedSizeInfo is called. self.assertEqual(an_fs.size, Size(0) if an_fs.resizable else an_fs._size)
- if not self._resizable: + if not can_resize(an_fs): self.assertFalse(an_fs.resizable) # Not resizable, so can not do resizing actions. with self.assertRaises(FSError): @@ -221,10 +227,10 @@ def testNoExplicitTargetSize(self): # _size if it is not set when size is calculated. Note that _targetSize # gets value of _size in constructor, so if _size is set to not-zero # in constructor call behavior would be different. - if not self._resizable: - self.skipTest("Not checking resize for this test category.")
an_fs = self._fs_class() + if not can_resize(an_fs): + self.skipTest("Not checking resize for this test category.") if not an_fs.formattable: self.skipTest("can not create filesystem %s" % an_fs.name)
@@ -244,11 +250,10 @@ def testNoExplicitTargetSize2(self): """ Because _targetSize has been set to size in constructor the resize action resizes filesystem to that size. """ - if not self._resizable: - self.skipTest("Not checking resize for this test category.") - SIZE = Size("64 MiB") an_fs = self._fs_class(size=SIZE) + if not can_resize(an_fs): + self.skipTest("Not checking resize for this test category.") if not an_fs.formattable: self.skipTest("can not create filesystem %s" % an_fs.name)
@@ -266,10 +271,9 @@ def testNoExplicitTargetSize2(self): self._test_sizes(an_fs)
def testShrink(self): - if not self._resizable: - self.skipTest("Not checking resize for this test category.") - an_fs = self._fs_class() + if not can_resize(an_fs): + self.skipTest("Not checking resize for this test category.") if not an_fs.formattable: self.skipTest("can not create filesystem %s" % an_fs.name)
@@ -299,10 +303,9 @@ def testShrink(self): self._test_sizes(an_fs)
def testTooSmall(self): - if not self._resizable: - self.skipTest("Not checking resize for this test category.") - an_fs = self._fs_class() + if not can_resize(an_fs): + self.skipTest("Not checking resize for this test category.") if not an_fs.formattable: self.skipTest("can not create or resize filesystem %s" % an_fs.name)
@@ -319,10 +322,9 @@ def testTooSmall(self): self._test_sizes(an_fs)
def testTooBig(self): - if not self._resizable: - self.skipTest("Not checking resize for this test category.") - an_fs = self._fs_class() + if not can_resize(an_fs): + self.skipTest("Not checking resize for this test category.") if not an_fs.formattable: self.skipTest("can not create filesystem %s" % an_fs.name)
@@ -339,10 +341,9 @@ def testTooBig(self): self._test_sizes(an_fs)
def testTooBig2(self): - if not self._resizable: - self.skipTest("Not checking resize for this test category.") - an_fs = self._fs_class() + if not can_resize(an_fs): + self.skipTest("Not checking resize for this test category.") if not an_fs.formattable: self.skipTest("can not create filesystem %s" % an_fs.name)
In reply to line 21 of tests/formats_test/fstesting.py:
I like ``not any()`` more in checks like this, but that is just a personal preference.
In reply to line 21 of tests/formats_test/fstesting.py:
Done.
From: mulhern amulhern@redhat.com
Related: #12
These showed up because fewer tests are skipped with the finer granularity.
Signed-off-by: mulhern amulhern@redhat.com --- tests/formats_test/fstesting.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/tests/formats_test/fstesting.py b/tests/formats_test/fstesting.py index 3740f94..5f919ca 100644 --- a/tests/formats_test/fstesting.py +++ b/tests/formats_test/fstesting.py @@ -50,7 +50,7 @@ def _test_sizes(self, an_fs): else: expected_size = _size # If the size can be obtained it will not be 0 - if an_fs._info.implemented: + if not an_fs._sizeinfo.availabilityErrors: self.assertNotEqual(expected_size, Size(0)) self.assertTrue(expected_size <= self._DEVICE_SIZE) # Otherwise it will be 0, assuming the device was not initialized @@ -60,7 +60,7 @@ def _test_sizes(self, an_fs): self.assertEqual(an_fs.size, expected_size)
# Only the resizable filesystems can figure out their current min size - if an_fs._resize: + if not an_fs._sizeinfo.availabilityErrors: expected_min_size = min_size else: expected_min_size = an_fs._minSize @@ -74,7 +74,7 @@ def _test_sizes(self, an_fs): self.assertEqual(an_fs.currentSize, _size)
# Free is the actual size - the minimum size - self.assertEqual(an_fs.free, _size - expected_min_size) + self.assertEqual(an_fs.free, max(Size(0), _size - expected_min_size))
# target size is set by side-effect self.assertEqual(an_fs.targetSize, an_fs._targetSize)
From: mulhern amulhern@redhat.com
Related: #12
Signed-off-by: mulhern amulhern@redhat.com --- tests/formats_test/selinux_test.py | 8 ++++++++ 1 file changed, 8 insertions(+)
diff --git a/tests/formats_test/selinux_test.py b/tests/formats_test/selinux_test.py index 516e07f..b5c62eb 100755 --- a/tests/formats_test/selinux_test.py +++ b/tests/formats_test/selinux_test.py @@ -28,6 +28,10 @@ def testMountingExt2FS(self): """ LOST_AND_FOUND_CONTEXT = 'system_u:object_r:lost_found_t:s0' an_fs = fs.Ext2FS(device=self.loopDevices[0], label="test") + + if not an_fs.formattable or not an_fs.mountable: + self.skipTest("can not create or mount filesystem %s" % an_fs.name) + self.assertIsNone(an_fs.create())
blivet.flags.installer_mode = False @@ -61,6 +65,10 @@ def testMountingExt2FS(self): def testMountingXFS(self): """ XFS does not have a lost+found directory. """ an_fs = fs.XFS(device=self.loopDevices[0], label="test") + + if not an_fs.formattable or not an_fs.mountable: + self.skipTest("can not create or mount filesystem %s" % an_fs.name) + self.assertIsNone(an_fs.create())
blivet.flags.installer_mode = False
From: mulhern amulhern@redhat.com
Related: #12
Signed-off-by: mulhern amulhern@redhat.com --- tests/action_test.py | 9 +++++++++ tests/clearpart_test.py | 6 ++++++ tests/devices_test/lvm_test.py | 11 +++++++++++ 3 files changed, 26 insertions(+)
diff --git a/tests/action_test.py b/tests/action_test.py index f030153..c370886 100644 --- a/tests/action_test.py +++ b/tests/action_test.py @@ -24,6 +24,15 @@ from blivet.deviceaction import ActionAddMember from blivet.deviceaction import ActionRemoveMember
+DEVICE_CLASSES = [ + DiskDevice, + PartitionDevice, + MDRaidArrayDevice, + LVMVolumeGroupDevice, + LVMLogicalVolumeDevice +] + +@unittest.skipUnless(all(not x.unavailableTypeDependencies() for x in DEVICE_CLASSES), "some unsupported device classes required for this test") class DeviceActionTestCase(StorageTestCase): """ DeviceActionTestSuite """
diff --git a/tests/clearpart_test.py b/tests/clearpart_test.py index 80d43ee..830a37a 100644 --- a/tests/clearpart_test.py +++ b/tests/clearpart_test.py @@ -6,6 +6,12 @@ from parted import PARTITION_NORMAL from blivet.flags import flags
+DEVICE_CLASSES = [ + blivet.devices.DiskDevice, + blivet.devices.PartitionDevice +] + +@unittest.skipUnless(all(not x.unavailableTypeDependencies() for x in DEVICE_CLASSES), "some unsupported device classes required for this test") class ClearPartTestCase(unittest.TestCase): def setUp(self): flags.testing = True diff --git a/tests/devices_test/lvm_test.py b/tests/devices_test/lvm_test.py index 12f0807..7cdfcc9 100644 --- a/tests/devices_test/lvm_test.py +++ b/tests/devices_test/lvm_test.py @@ -14,6 +14,17 @@ from blivet.devices import LVMVolumeGroupDevice from blivet.size import Size
+DEVICE_CLASSES = [ + LVMLogicalVolumeDevice, + LVMSnapShotDevice, + LVMThinLogicalVolumeDevice, + LVMThinPoolDevice, + LVMThinSnapShotDevice, + LVMVolumeGroupDevice, + StorageDevice +] + +@unittest.skipUnless(all(not x.unavailableTypeDependencies() for x in DEVICE_CLASSES), "some unsupported device classes required for this test") class LVMDeviceTest(unittest.TestCase): def testLVMSnapShotDeviceInit(self): pv = StorageDevice("pv1", fmt=blivet.formats.getFormat("lvmpv"),
Looks good to me other than the minor comments above.
Added label: ACK.
Closed.
anaconda-patches@lists.fedorahosted.org