From 7b20092b152faf213a41799b8ecf726f42c0cf4f Mon Sep 17 00:00:00 2001 From: Frank Brehm Date: Thu, 23 Mar 2017 15:40:17 +0100 Subject: [PATCH] Adding quota-check and pp_lib/quota_check.py --- pp_lib/quota_check.py | 263 ++++++++++++++++++++++++++++++++++++++++++ quota-check | 43 +++++++ 2 files changed, 306 insertions(+) create mode 100644 pp_lib/quota_check.py create mode 100755 quota-check diff --git a/pp_lib/quota_check.py b/pp_lib/quota_check.py new file mode 100644 index 0000000..6ec3bc0 --- /dev/null +++ b/pp_lib/quota_check.py @@ -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 index 0000000..b4bf343 --- /dev/null +++ b/quota-check @@ -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 ' +__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 -- 2.39.5