--- /dev/null
+#!/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