]> Frank Brehm's Git Trees - pixelpark/admin-tools.git/commitdiff
Adding quota-check and pp_lib/quota_check.py
authorFrank Brehm <frank.brehm@pixelpark.com>
Thu, 23 Mar 2017 14:40:17 +0000 (15:40 +0100)
committerFrank Brehm <frank.brehm@pixelpark.com>
Thu, 23 Mar 2017 14:40:17 +0000 (15:40 +0100)
pp_lib/quota_check.py [new file with mode: 0644]
quota-check [new file with mode: 0755]

diff --git a/pp_lib/quota_check.py b/pp_lib/quota_check.py
new file mode 100644 (file)
index 0000000..6ec3bc0
--- /dev/null
@@ -0,0 +1,263 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+@author: Frank Brehm
+@contact: frank.brehm@pixelpark.com
+@copyright: © 2017 by Frank Brehm, Berlin
+@summary: The module for the quota-check application object.
+"""
+from __future__ import absolute_import
+
+# Standard modules
+import sys
+import os
+import datetime
+import logging
+import logging.config
+import re
+import traceback
+import textwrap
+import pwd
+import copy
+import glob
+
+# Third party modules
+import six
+import yaml
+
+# Own modules
+from .global_version import __version__ as __global_version__
+
+from .errors import FunctionNotImplementedError, PpAppError
+
+from .common import pp, terminal_can_colors, to_bytes, to_bool
+
+from .cfg_app import PpCfgAppError, PpConfigApplication
+
+__version__ = '0.1.1'
+LOG = logging.getLogger(__name__)
+
+
+# =============================================================================
+class PpQuotaCheckError(PpCfgAppError):
+    pass
+
+
+# =============================================================================
+class PpQuotaCheckApp(PpConfigApplication):
+    """
+    Class for the 'quota-check' application to check the utilization
+    of the home share on the NFS server.
+    """
+
+    # /mnt/nfs
+    default_chroot_homedir = os.sep + os.path.join('mnt', 'nfs')
+    # /home
+    default_home_root = os.sep + 'home'
+
+    default_quota_kb = 300 * 1024
+
+    default_status_dir = os.sep + os.path.join('var', 'lib', 'quota-check')
+    default_statusfile_base = 'quota-check.yaml'
+
+    # -------------------------------------------------------------------------
+    def __init__(self, appname=None, version=__version__):
+
+        self.chroot_homedir = self.default_chroot_homedir
+        self.home_root_abs = self.default_home_root
+        self.home_root_rel = os.path.relpath(self.home_root_abs, os.sep)
+
+        self.quota_kb = self.default_quota_kb
+
+        self.status_dir = self.default_status_dir
+        self.statusfile_base = self.default_statusfile_base
+        self.statusfile = os.path.join(self.status_dir, self.statusfile_base)
+
+        self.passwd_data = {}
+        self.map_uid = {}
+
+        description = textwrap.dedent('''\
+            This checks the utilization of the home directories on the NFS server
+            and sends a mail per request about all home directories, which are
+            exceeding the given quota (default {} MB).
+            ''').strip().format(self.default_quota_kb)
+
+        super(PpQuotaCheckApp, self).__init__(
+            appname=appname, version=version, description=description,
+            cfg_stems='quota-check'
+        )
+
+        self.initialized = True
+
+    # -------------------------------------------------------------------------
+    def perform_config(self):
+
+        super(PpQuotaCheckApp, self).perform_config()
+
+        for section_name in self.cfg.keys():
+
+            if self.verbose > 3:
+                LOG.debug("Checking config section {!r} ...".format(section_name))
+
+            if section_name.lower() not in ('quota-check', 'quota_check', 'quotacheck') :
+                continue
+
+            section = self.cfg[section_name]
+            if self.verbose > 2:
+                LOG.debug("Evaluating config section {n!r}:\n{s}".format(
+                    n=section_name, s=pp(section)))
+
+            if 'quota_mb' in section:
+                v = section['quota_mb']
+                quota = self.quota_kb / 1024
+                try:
+                    quota = int(v)
+                except (ValueError, TypeError) as e:
+                    msg = "Found invalid quota MB {!r} in configuration.".format(v)
+                    LOG.error(msg)
+                else:
+                    if quota < 0:
+                        msg = "Found invalid quota MB {!r} in configuration.".format(quota)
+                        LOG.error(msg)
+                    else:
+                        self.quota_kb = quota * 1024
+
+            if 'quota_kb' in section:
+                v = section['quota_kb']
+                quota = self.quota_kb
+                try:
+                    quota = int(v)
+                except (ValueError, TypeError) as e:
+                    msg = "Found invalid quota KB {!r} in configuration.".format(v)
+                    LOG.error(msg)
+                else:
+                    if quota < 0:
+                        msg = "Found invalid quota KB {!r} in configuration.".format(quota)
+                        LOG.error(msg)
+                    else:
+                        self.quota_kb = quota
+
+            if 'chroot_homedir' in section:
+                v = section['chroot_homedir']
+                if not os.path.isabs(v):
+                    msg = (
+                        "The chrooted path of the home directories must be an "
+                        "absolute pathname (found [{s}]/chroot_homedir "
+                        "=> {v!r} in configuration.").format(s=section_name, v=v)
+                    raise PpMkHomeError(msg)
+                self.chroot_homedir = v
+
+            if 'home_root' in section:
+                v = section['home_root']
+                if not os.path.isabs(v):
+                    msg = (
+                        "The root path of the home directories must be an "
+                        "absolute pathname (found [{s}]/home_root "
+                        "=> {v!r} in configuration.").format(s=section_name, v=v)
+                    raise PpMkHomeError(msg)
+                self.home_root_abs = v
+
+            if 'status_file' in section:
+                v = section['status_file']
+                if os.path.isabs(v):
+                    self.status_dir = os.path.normpath(os.path.dirname(v))
+                    self.statusfile_base = os.path.basename(v)
+                    self.statusfile = os.path.normpath(v)
+                else:
+                    self.statusfile = os.path.normpath(
+                        os.path.join(self.status_dir, v))
+                    self.status_dir = os.path.dirname(self.statusfile)
+                    self.statusfile_base = os.path.basename(self.statusfile)
+
+        self.home_root_rel = os.path.relpath(self.home_root_abs, os.sep)
+        self.home_root_real = os.path.join(self.chroot_homedir, self.home_root_rel)
+
+    # -------------------------------------------------------------------------
+    def _run(self):
+
+        self.read_passwd_data()
+
+    # -------------------------------------------------------------------------
+    def read_passwd_data(self):
+
+        LOG.info("Reading all necessary data from 'getent passwd' ...")
+
+        upper_dir = os.pardir + os.sep
+        entries = pwd.getpwall()
+
+        for entry in entries:
+            home = entry.pw_dir
+            user_name = entry.pw_name
+            uid = entry.pw_uid
+            if not home:
+                continue
+            home_relative = os.path.relpath(home, self.home_root_abs)
+            if home == os.sep or home_relative.startswith(upper_dir):
+                if self.verbose > 1:
+                    LOG.debug((
+                        "Home directory {d!r} of user {u!r} "
+                        "is outside home root {h!r}.").format(
+                        d=home, u=entry.pw_name, h=self.home_root_abs))
+                continue
+            if user_name not in self.passwd_data:
+                self.passwd_data[user_name] = entry
+            if uid not in self.map_uid:
+                self.map_uid[uid] = user_name
+
+        LOG.debug("Found {} appropriate user entries in passwd.".format(
+            len(self.passwd_data.keys())))
+        if self.verbose > 2:
+            LOG.debug("User data in passwd:\n{}".format(pp(self.passwd_data)))
+
+    # -------------------------------------------------------------------------
+    def check_homes(self):
+
+        LOG.info("Checking for unnecessary home directories ...")
+
+        glob_pattern = os.path.join(self.home_root_real, '*')
+        all_home_entries = glob.glob(glob_pattern)
+
+        for path in all_home_entries:
+            if not os.path.isdir(path):
+                continue
+            home_rel = os.sep + os.path.relpath(path, self.chroot_homedir)
+            if self.verbose > 2:
+                LOG.debug("Checking {p!r} ({h!r}) ...".format(
+                    p=path, h=home_rel))
+            if home_rel in self.passwd_home_dirs:
+                continue
+            if home_rel in self.exclude_dirs:
+                continue
+            LOG.debug("Marking {!r} as unnecessary.".format(home_rel))
+            self.unnecessary_dirs.append(home_rel)
+
+        self.unnecessary_dirs.sort(key=str.lower)
+
+    # -------------------------------------------------------------------------
+    def send_results(self):
+
+        if not self.unnecessary_dirs:
+            LOG.debug("No unnecessary home directories, nothing to inform.")
+            return
+
+        subject = 'Nicht benötigte Home-Verzeichnisse'
+        body = textwrap.dedent('''\
+            Die folgenden Home-Verzeichnisse befinden sich weder
+            in der lokalen passwd-Datei, im LDAP oder in der exclude-Liste.
+            Sie können damit archiviert und gelöscht werden.''')
+        body += '\n\n'
+        for home in self.unnecessary_dirs:
+            body += ' - ' + home + '\n'
+
+        self.send_mail(subject, body)
+
+
+# =============================================================================
+
+if __name__ == "__main__":
+
+    pass
+
+# =============================================================================
+
+# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list
diff --git a/quota-check b/quota-check
new file mode 100755 (executable)
index 0000000..b4bf343
--- /dev/null
@@ -0,0 +1,43 @@
+#!/usr/bin/env python3
+
+# Standard modules
+import sys
+import os
+import logging
+import locale
+
+# own modules:
+cur_dir = os.getcwd()
+base_dir = cur_dir
+
+if sys.argv[0] != '' and sys.argv[0] != '-c':
+    cur_dir = os.path.dirname(sys.argv[0])
+if os.path.exists(os.path.join(cur_dir, 'pp_lib')):
+    sys.path.insert(0, os.path.abspath(cur_dir))
+
+from pp_lib.common import pp
+
+from pp_lib.quota_check import PpQuotaCheckApp
+
+log = logging.getLogger(__name__)
+
+__author__ = 'Frank Brehm <frank.brehm@pixelpark.com>'
+__copyright__ = '(C) 2017 by Frank Brehm, Pixelpark GmbH, Berlin'
+
+appname = os.path.basename(sys.argv[0])
+
+locale.setlocale(locale.LC_ALL, '')
+
+app = PpQuotaCheckApp(appname=appname)
+app.initialized = True
+
+if app.verbose > 2:
+    print("{c}-Object:\n{a}".format(c=app.__class__.__name__, a=app))
+
+app()
+
+sys.exit(0)
+
+# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
+
+# vim: ts=4