cleaner way to sort out unwanted file system info: bool flag
authorChristian Herdtweck <christian.herdtweck@intra2net.com>
Fri, 5 Feb 2016 10:14:09 +0000 (11:14 +0100)
committerChristian Herdtweck <christian.herdtweck@intra2net.com>
Fri, 5 Feb 2016 10:14:09 +0000 (11:14 +0100)
src/file_helpers.py
src/test_helpers.py

index cafeaea..79eb2b3 100644 (file)
@@ -29,6 +29,18 @@ Featuring::
 * * a wrapper around "df"
 * * a wrapper around statvfs (default since faster without forking)
 
+What I found out on the way about filesystems:
+* usually size != used + available
+* there are several sources that list "all" filesystems, each omiting different
+  things:
+* * /etc/fstab   --> maintained by admin and tools
+* * /etc/mtab    --> usually a link to /proc/mounts
+* * /proc/mounts --> usually the longest
+* * df           --> usually the shortest of all
+* with all those virtual filesystems in memory and bind-mounts, things get
+  complicated!
+* forking for df takes time!
+
 .. codeauthor:: Intra2net
 """
 
@@ -71,6 +83,12 @@ FS_FILL_METHOD_STATVFS = 'statvfs'
 IGNORE_MOUNT_TYPES = 'cgroup', 'pstore'
 
 
+#: filesystems that usually do not correspond to something on disc
+#: (except maybe swap)
+NOT_REAL_FILESYSTEMS = 'none', 'shmfs', 'procfs', 'tmpfs', 'ramfs', 'proc', \
+                       'rootfs', 'sysfs', 'devpts', 'sunrpc', 'nfsd'
+
+
 class FilesystemFillState:
     """ representation of 1 line of the 'df' command
 
@@ -94,21 +112,18 @@ class FilesystemFillState:
                .format(self.name, self.mount_point, int(round(self.capacity)))
 
 
-def get_filesystem_fill_states(method=FS_FILL_METHOD_STATVFS, *args, **kwargs):
+def get_filesystem_fill_states(method=FS_FILL_METHOD_STATVFS):
     """ get fill state on all filesystems
 
     :param method: FS_FILL_METHOD_DF (use df_wrapper, forks) or
-                   FS_FILL_METHOD_STATVFS (uses get_all_statvfs)
+                   FS_FILL_METHOD_STATVFS (uses get_all_mounts(False), default)
     all other args and kwargs are forwarded
     """
 
     if method == FS_FILL_METHOD_DF:
-        return df_wrapper(*args, **kwargs)
+        return df_wrapper()
     else:
-        # could do this more equal to df-output by
-        # - removing file systems with size 0
-        # - detecting multiple entries for same filesystem
-        return get_all_statvfs_fills(*args, **kwargs)
+        return get_all_statvfs_fills(include_duplicates=False)
 
 
 def df_wrapper():
@@ -202,12 +217,11 @@ def df_wrapper():
     return result
 
 
-def get_all_statvfs_fills(*mounts):
+def get_all_statvfs_fills(mounts=None, include_duplicates=True):
     """ run get_fill_from_statvfs on given MountPoints or get_all_mounts """
 
-    if not mounts:
-        mounts = get_all_mounts()
-
+    if mounts is None:
+        mounts = get_all_mounts(include_duplicates)
     return [get_fill_from_statvfs(mount) for mount in mounts]
 
 
@@ -297,14 +311,21 @@ MOUNT_REGEXP = r'^\s*(?P<spec>.+)' \
                  '\s+(?P<freq>\d+)' \
                  '\s+(?P<passno>\d+)\s*$'
 
-def get_all_mounts():
+def get_all_mounts(include_duplicates=True):
     """ parse /proc/mounts
 
     does not return those with type in :py:data:IGNORE_MOUNT_TYPES
 
+    :param bool include_duplicates: if False, try to list every "real"
+                                    filesystem only once, e.g. ignore bind
+                                    mounts, return rootfs only if no other fs
+                                    is mounted in same file; default: True
     :results: list of :class:`MountPoint`
     """
     result = []
+    rootfs = []
+    files = []
+    specs = []
     with open('/proc/mounts', 'rt') as file_handle:
         for line in file_handle:
             parts = line.split()
@@ -317,8 +338,34 @@ def get_all_mounts():
                 setattr(new_mount, field_name, value)
             if new_mount.vfstype in IGNORE_MOUNT_TYPES:
                 continue
+
+            if not include_duplicates:
+                if new_mount.spec == 'rootfs':
+                    rootfs.append(new_mount)
+                    continue        # deal with rootfs in the end
+                if new_mount.file in files:
+                    warn('multiple non-rootfs mounts in same file {0}!'
+                         .format(new_mount.file))
+                if new_mount.spec in specs \
+                        and new_mount.spec not in NOT_REAL_FILESYSTEMS:
+                    continue        # e.g. bind mounts; ignore this mount
+
+                # if we reach this, this is no duplicate; remember it
+                files.append(new_mount.file)
+                specs.append(new_mount.spec)
             result.append(new_mount)
 
+    # if not include_duplicates:
+    # add rootfs mounts only if no other mount is in same file
+    for root_mount in rootfs:
+        have_mount = False
+        for mount in result:
+            if mount.file == root_mount.file:
+                have_mount = True      # some other mount in same place
+                break
+        if not have_mount:
+            result.append(root_mount)
+
     return result
 
 
index eedcd20..f1bbcf2 100644 (file)
@@ -45,15 +45,11 @@ except ImportError:
 
 from buffers import LogarithmicBuffer
 from file_helpers import get_filesystem_fill_states, FilesystemFillState, \
-                         get_mount_info, get_fill_from_statvfs
+                         get_mount_info, get_fill_from_statvfs, \
+                         NOT_REAL_FILESYSTEMS
 from iter_helpers import pairwise
 
 
-#: filesystems shown by df that usually do not correspond to something on disc
-#: (except maybe swap)
-NOT_REAL_FILESYSTEMS = 'none', 'shmfs', 'procfs', 'tmpfs', 'ramfs'
-
-
 class DiscFullPreventionError(Exception):
     """ Exception raised when disc space gets critical