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 | 195 ++++++++++++++++++++++++++++++ blivet/tasks/fsreadlabel.py | 133 +++++++++++++++++++++ blivet/tasks/fsresize.py | 148 +++++++++++++++++++++++ blivet/tasks/fssize.py | 166 ++++++++++++++++++++++++++ blivet/tasks/fssync.py | 89 ++++++++++++++ blivet/tasks/fswritelabel.py | 116 ++++++++++++++++++ tests/pylint/pylint-false-positives | 3 + 12 files changed, 1629 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..a70e45e --- /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.application("dosfsck") + 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.application_by_package("e2fsck", availability.E2FSPROGS_PACKAGE) + 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.application("fsck.hfsplus") + 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..18eeb79 --- /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.application_by_package("dumpe2fs", availability.E2FSPROGS_PACKAGE) + options = ["-h"] + +class JFSInfo(FSInfo): + ext = availability.JFSTUNE_APP + options = ["-l"] + +class NTFSInfo(FSInfo): + ext = availability.application("ntfsinfo") + options = ["-m"] + +class ReiserFSInfo(FSInfo): + ext = availability.application("debugreiserfs") + options = [] + +class XFSInfo(FSInfo): + ext = availability.application("xfs_db") + 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..33cd50f --- /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): + + can_label = 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 can_label(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.application("mkfs.btrfs") + label_option = None + + @property + def args(self): + return [] + +class Ext2FSMkfs(FSMkfs): + ext = availability.application_by_package("mke2fs", availability.E2FSPROGS_PACKAGE) + 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.application("mkdosfs") + label_option = "-n" + + @property + def args(self): + return [] + +class GFS2Mkfs(FSMkfs): + ext = availability.application("mkfs.gfs2") + label_option = None + + @property + def args(self): + return ["-j", "1", "-p", "lock_nolock", "-O"] + +class HFSMkfs(FSMkfs): + ext = availability.application("hformat") + label_option = "-l" + + @property + def args(self): + return [] + +class HFSPlusMkfs(FSMkfs): + ext = availability.application("mkfs.hfsplus") + label_option = "-v" + + @property + def args(self): + return [] + +class JFSMkfs(FSMkfs): + ext = availability.application("mkfs.jfs") + label_option = "-L" + + @property + def args(self): + return ["-q"] + +class NTFSMkfs(FSMkfs): + ext = availability.application("mkntfs") + label_option = "-L" + + @property + def args(self): + return [] + +class ReiserFSMkfs(FSMkfs): + ext = availability.application("mkreiserfs") + label_option = "-l" + + @property + def args(self): + return ["-f", "-f"] + +class XFSMkfs(FSMkfs): + ext = availability.application("mkfs.xfs") + 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 can_label(self): + return False diff --git a/blivet/tasks/fsmount.py b/blivet/tasks/fsmount.py new file mode 100644 index 0000000..39a11b7 --- /dev/null +++ b/blivet/tasks/fsmount.py @@ -0,0 +1,195 @@ +# 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 _availabilityErrors(self): + errors = super(FSMount, self)._availabilityErrors + + canmount = (self.mountType in fslib.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): + canmount = True + break + + if not 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 None + :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..8b61b02 --- /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._app()) + 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..46023d1 --- /dev/null +++ b/blivet/tasks/fsresize.py @@ -0,0 +1,148 @@ +# 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 unit specifier is interpreted not as bytes, but block size + 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 supress 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..e4a7507 --- /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 or None + :rtype: :class:`~.size.Size` or NoneType + :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.application("df") + + 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..5cf82b6 --- /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): + """ Info application for XFS. """ + + ext = availability.application("xfs_freeze") + + 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..bcdff7c --- /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.application("reiserfstune") + + @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 diff --git a/tests/pylint/pylint-false-positives b/tests/pylint/pylint-false-positives index d33c566..c02fff7 100644 --- a/tests/pylint/pylint-false-positives +++ b/tests/pylint/pylint-false-positives @@ -20,6 +20,9 @@ ^blivet/blivet.py:[[:digit:]]+: [E1101(no-member), Blivet.savePassphrase] Instance of 'DeviceTree' has no '_DeviceTree__luksDevs' member$ ^blivet/blivet.py:[[:digit:]]+: [E1101(no-member), Blivet.savePassphrase] Instance of 'DeviceTree' has no '_DeviceTree__passphrases' member$ ^blivet/formats/__init__.py:[[:digit:]]+: [W0201(attribute-defined-outside-init), DeviceFormat._setDevice] Attribute '_device' defined outside __init__$ +^blivet/tasks/.*py:[[:digit:]]+: [W0223(abstract-method), .*] Method 'available' is abstract in class 'Task' but is not overridden$ +^blivet/tasks/.*py:[[:digit:]]+: [W0223(abstract-method), .*] Method 'doTask' is abstract in class 'Task' but is not overridden$ +^blivet/tasks/.*py:[[:digit:]]+: [W0223(abstract-method), .*] Method 'doTask' is abstract in class 'UnimplementedTask' but is not overridden$ ^dm.c: [[:digit:]]+: not running as root returning empty list$ ^tests/devicelibs_test/mdraid_test.py:[[:digit:]]+: [E1003(bad-super-call), ([[:alnum:].]+).__init__] Bad first argument YES given to super()$ ^tests/devicelibs_test/raid_test.py:[[:digit:]]+: [E1120(no-value-for-parameter), [[:alnum:].]+] No value [[:alnum:] ]+ 'member_count' in [[:alnum:] ]+ call$