From 172ec675dcba57c3a6e14340809bceee39283560 Mon Sep 17 00:00:00 2001 From: Frank Brehm Date: Wed, 19 Apr 2017 18:06:58 +0200 Subject: [PATCH] Splitted pp_lib/test_home_app.py into two different modules --- pp_lib/homes_admin.py | 322 ++++++++++++++++++++++++++++++++++++++++ pp_lib/test_home_app.py | 206 ++----------------------- test-home | 2 - 3 files changed, 331 insertions(+), 199 deletions(-) create mode 100644 pp_lib/homes_admin.py diff --git a/pp_lib/homes_admin.py b/pp_lib/homes_admin.py new file mode 100644 index 0000000..085573a --- /dev/null +++ b/pp_lib/homes_admin.py @@ -0,0 +1,322 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@author: Frank Brehm +@contact: frank.brehm@pixelpark.com +@copyright: © 2017 by Frank Brehm, Berlin +@summary: The base module all maintaining scripts for the home directories +""" +from __future__ import absolute_import + +# Standard modules +import sys +import os +import logging +import logging.config +import re +import traceback +import textwrap +import pwd +import copy +import glob + +# Third party modules +import six + +# 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 PpHomesAdminError(PpCfgAppError): + pass + + +# ============================================================================= +class PpHomesAdminApp(PpConfigApplication): + """ + Base class for applications maintaining the global Home directories. + """ + + # /mnt/nfs + default_chroot_homedir = os.sep + os.path.join('mnt', 'nfs') + # /home + default_home_root = os.sep + 'home' + + # /etc/pixelpark/exclude_homes + default_exclude_file = os.sep + os.path.join('etc', 'pixelpark', 'exclude_homes') + + comment_re = re.compile(r'\s*#.*') + + # ------------------------------------------------------------------------- + def __init__(self, appname=None, description=None, version=__version__): + + self.default_mail_recipients = [ + 'admin.berlin@pixelpark.com' + ] + self.default_mail_cc = [] + + 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.exclude_file = self.default_exclude_file + + self.exclude_dirs = [] + self.passwd_home_dirs = [] + self.unnecessary_dirs = [] + + super(PpHomesAdminApp, self).__init__( + appname=appname, version=version, description=description, + cfg_stems='homes-admin' + ) + + # ------------------------------------------------------------------------- + def init_arg_parser(self): + + homes_group = self.arg_parser.add_argument_group('Homes administration options.') + + homes_group.add_argument( + '-R', '--chroot-dir', + metavar='DIR', dest='chroot_homedir', + help=("Directory, where the {h!r} share is mounted from the " + "NFS server. Maybe '/', default: {d!r}.").format( + h=self.default_home_root, d=self.default_chroot_homedir) + ) + + homes_group.add_argument( + '-H', '--homes', + metavar='DIR', dest='home_root', + help=("The shared directory on the NFS server for all home directories. " + "Default: {!r}.").format(self.default_home_root) + ) + + homes_group.add_argument( + '-E', '--exclude-file', + metavar='FILE', dest='exclude_file', + help=("The file containing all directories underneath {h!r}, which are " + "excluded from all operations. Default: {f!r}.").format( + h=self.default_home_root, f=self.default_exclude_file) + ) + + super(PpHomesAdminApp, self).init_arg_parser() + + # ------------------------------------------------------------------------- + def perform_config(self): + + super(PpHomesAdminApp, self).perform_config() + + for section_name in self.cfg.keys(): + + if self.verbose > 3: + LOG.debug("Checking config section {!r} ...".format(section_name)) + + section = self.cfg[section_name] + + if section_name.lower() not in ( + 'test-home', 'test_home', 'testhome', 'homes', 'admin') : + continue + + if self.verbose > 2: + LOG.debug("Evaluating config section {n!r}:\n{s}".format( + n=section_name, s=pp(section))) + + if section_name.lower() == 'homes': + + 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 PpHomesAdminError(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 PpHomesAdminError(msg) + self.home_root_abs = v + + elif section_name.lower() == 'admin': + + if 'exclude_file' in section: + v = section['exclude_file'] + if not os.path.isabs(v): + msg = ( + "The path of file of excluded directories must be an " + "absolute pathname (found [{s}]/exclude_file " + "=> {v!r} in configuration.").format(s=section_name, v=v) + raise PpHomesAdminError(msg) + self.exclude_file = v + + if hasattr(self.args, 'chroot_homedir') and self.args.chroot_homedir: + v = self.args.chroot_homedir + if not os.path.isabs(v): + msg = ( + "The chrooted path of the home directories must be an " + "absolute pathname (got {!r} as command line parameter).").format(v) + raise PpHomesAdminError(msg) + self.chroot_homedir = v + + if hasattr(self.args, 'home_root') and self.args.home_root: + v = self.args.home_root + if not os.path.isabs(v): + msg = ( + "The root path of the home directories must be an " + "absolute pathname (got {!r} as command line parameter).").format(v) + raise PpHomesAdminError(msg) + self.home_root_abs = v + + if hasattr(self.args, 'exclude_file') and self.args.exclude_file: + v = self.args.exclude_file + if not os.path.isabs(v): + msg = ( + "The path of file of excluded directories must be an " + "absolute pathname (got {!r} as command line parameter).").format(v) + raise PpHomesAdminError(msg) + self.exclude_file = v + + 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 read_exclude_dirs(self): + + LOG.info("Reading exclude file {!r} ...".format(self.exclude_file)) + upper_dir = os.pardir + os.sep + + if not os.path.exists(self.exclude_file): + msg = "Exclude file {!r} does not exists.".format(self.exclude_file) + LOG.error(msg) + return + + if not os.path.isfile(self.exclude_file): + msg = "Exclude file {!r} is not a regular file.".format(self.exclude_file) + LOG.error(msg) + return + + if not os.access(self.exclude_file, os.R_OK): + msg = "No read access to exclude file {!r}.".format(self.exclude_file) + LOG.error(msg) + return + + open_args = {} + if six.PY3: + open_args['encoding'] = 'utf-8' + open_args['errors'] = 'surrogateescape' + + with open(self.exclude_file, 'r', **open_args) as fh: + lnr = 0 + for line in fh.readlines(): + lnr += 1 + line = line.strip() + if not line: + continue + line = self.comment_re.sub('', line) + if not line: + continue + if self.verbose > 3: + LOG.debug("Evaluating line {l!r} (file {f!r}, line {lnr}).".format( + l=line, f=self.exclude_file, lnr=lnr)) + tokens = self.whitespace_re.split(line) + for token in tokens: + if not os.path.isabs(token): + LOG.warn(( + "Entry {e!r} in file {f!r}, line {l}, " + "is not an absolute path.").format( + e=token, f=self.exclude_file, l=lnr)) + continue + home_relative = os.path.relpath(token, self.home_root_abs) + if token == os.sep or home_relative.startswith(upper_dir): + LOG.warn(( + "Entry {e!r} in file {f!r}, line {l}, " + "is outside home root {h!r}.").format( + e=token, f=self.exclude_file, l=lnr, h=self.home_root_abs)) + continue + if token not in self.exclude_dirs: + self.exclude_dirs.append(token) + + self.exclude_dirs.sort(key=str.lower) + + LOG.debug("Found {} directories to exclude.".format(len(self.exclude_dirs))) + if self.verbose > 2: + LOG.debug("Found directories to exclude:\n{}".format(pp(self.exclude_dirs))) + + # ------------------------------------------------------------------------- + def read_passwd_homes(self): + + LOG.info("Reading all home directories from 'getent passwd' ...") + + upper_dir = os.pardir + os.sep + entries = pwd.getpwall() + + for entry in entries: + home = entry.pw_dir + 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 home not in self.passwd_home_dirs: + self.passwd_home_dirs.append(home) + + self.passwd_home_dirs.sort(key=str.lower) + + LOG.debug("Found {} home directories in passwd.".format(len(self.passwd_home_dirs))) + if self.verbose > 2: + LOG.debug("Home directories in passwd:\n{}".format(pp(self.passwd_home_dirs))) + + # ------------------------------------------------------------------------- + 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) + + +# ============================================================================= + +if __name__ == "__main__": + + pass + +# ============================================================================= + +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 list diff --git a/pp_lib/test_home_app.py b/pp_lib/test_home_app.py index 1c89a69..e225d7c 100644 --- a/pp_lib/test_home_app.py +++ b/pp_lib/test_home_app.py @@ -30,53 +30,30 @@ from .errors import FunctionNotImplementedError, PpAppError from .common import pp, terminal_can_colors, to_bytes, to_bool -from .cfg_app import PpCfgAppError, PpConfigApplication +from .cfg_app import PpCfgAppError -__version__ = '0.4.4' +from .homes_admin import PpHomesAdminError, PpHomesAdminApp + +__version__ = '0.5.1' LOG = logging.getLogger(__name__) # ============================================================================= -class PpTestHomeError(PpCfgAppError): +class PpTestHomeError(PpHomesAdminError): pass # ============================================================================= -class PpTestHomeApp(PpConfigApplication): +class PpTestHomeApp(PpHomesAdminApp): """ Class for the 'test-home' application to check for unnacessary home directories. """ - # /mnt/nfs - default_chroot_homedir = os.sep + os.path.join('mnt', 'nfs') - # /home - default_home_root = os.sep + 'home' - - # /etc/pixelpark/exclude_homes - default_exclude_file = os.sep + os.path.join('etc', 'pixelpark', 'exclude_homes') - - comment_re = re.compile(r'\s*#.*') - # ------------------------------------------------------------------------- def __init__(self, appname=None, version=__version__): - self.default_mail_recipients = [ - 'admin.berlin@pixelpark.com' - ] - self.default_mail_cc = [] - self.default_reply_to = 'noreply@pixelpark.com' - 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.exclude_file = self.default_exclude_file - - self.exclude_dirs = [] - self.passwd_home_dirs = [] - self.unnecessary_dirs = [] - description = textwrap.dedent('''\ This scripts detects unnecessary home directories - without an appropriate home directory in the passwd database and not excluded @@ -85,61 +62,12 @@ class PpTestHomeApp(PpConfigApplication): super(PpTestHomeApp, self).__init__( appname=appname, version=version, description=description, - cfg_stems='test-home' ) - self.initialized = True - - # ------------------------------------------------------------------------- - def perform_config(self): - - super(PpTestHomeApp, self).perform_config() + if not self.mail_recipients: + self.exit(5) - 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 ('test-home', 'test_home', 'testhome') : - 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 '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 'exclude_file' in section: - v = section['exclude_file'] - if not os.path.isabs(v): - msg = ( - "The path of file of excluded directories must be an " - "absolute pathname (found [{s}]/exclude_file " - "=> {v!r} in configuration.").format(s=section_name, v=v) - raise PpMkHomeError(msg) - self.exclude_file = v - - 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) + self.initialized = True # ------------------------------------------------------------------------- def _run(self): @@ -149,122 +77,6 @@ class PpTestHomeApp(PpConfigApplication): self.check_homes() self.send_results() - # ------------------------------------------------------------------------- - def read_exclude_dirs(self): - - LOG.info("Reading exclude file {!r} ...".format(self.exclude_file)) - upper_dir = os.pardir + os.sep - - if not os.path.exists(self.exclude_file): - msg = "Exclude file {!r} does not exists.".format(self.exclude_file) - LOG.error(msg) - return - - if not os.path.isfile(self.exclude_file): - msg = "Exclude file {!r} is not a regular file.".format(self.exclude_file) - LOG.error(msg) - return - - if not os.access(self.exclude_file, os.R_OK): - msg = "No read access to exclude file {!r}.".format(self.exclude_file) - LOG.error(msg) - return - - open_args = {} - if six.PY3: - open_args['encoding'] = 'utf-8' - open_args['errors'] = 'surrogateescape' - - with open(self.exclude_file, 'r', **open_args) as fh: - lnr = 0 - for line in fh.readlines(): - lnr += 1 - line = line.strip() - if not line: - continue - line = self.comment_re.sub('', line) - if not line: - continue - if self.verbose > 3: - LOG.debug("Evaluating line {l!r} (file {f!r}, line {lnr}).".format( - l=line, f=self.exclude_file, lnr=lnr)) - tokens = self.whitespace_re.split(line) - for token in tokens: - if not os.path.isabs(token): - LOG.warn(( - "Entry {e!r} in file {f!r}, line {l}, " - "is not an absolute path.").format( - e=token, f=self.exclude_file, l=lnr)) - continue - home_relative = os.path.relpath(token, self.home_root_abs) - if token == os.sep or home_relative.startswith(upper_dir): - LOG.warn(( - "Entry {e!r} in file {f!r}, line {l}, " - "is outside home root {h!r}.").format( - e=token, f=self.exclude_file, l=lnr, h=self.home_root_abs)) - continue - if token not in self.exclude_dirs: - self.exclude_dirs.append(token) - - self.exclude_dirs.sort(key=str.lower) - - LOG.debug("Found {} directories to exclude.".format(len(self.exclude_dirs))) - if self.verbose > 2: - LOG.debug("Found directories to exclude:\n{}".format(pp(self.exclude_dirs))) - - # ------------------------------------------------------------------------- - def read_passwd_homes(self): - - LOG.info("Reading all home directories from 'getent passwd' ...") - - upper_dir = os.pardir + os.sep - entries = pwd.getpwall() - - for entry in entries: - home = entry.pw_dir - 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 home not in self.passwd_home_dirs: - self.passwd_home_dirs.append(home) - - self.passwd_home_dirs.sort(key=str.lower) - - LOG.debug("Found {} home directories in passwd.".format(len(self.passwd_home_dirs))) - if self.verbose > 2: - LOG.debug("Home directories in passwd:\n{}".format(pp(self.passwd_home_dirs))) - - # ------------------------------------------------------------------------- - 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): diff --git a/test-home b/test-home index 32eb03e..05b4684 100755 --- a/test-home +++ b/test-home @@ -39,5 +39,3 @@ app() sys.exit(0) # vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 - -# vim: ts=4 -- 2.39.5