Source code for ucsschool.importer.frontend.cmdline

#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Univention UCS@school
# Copyright 2016-2025 Univention GmbH
#
# https://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <http://www.gnu.org/licenses/>.

"""Base class for UCS@school import tool cmdline frontends."""
import grp
import logging
import os
import pprint
import pwd
import sys
from datetime import datetime
from typing import List  # noqa: F401

import six

from ucsschool.lib.models.utils import (
    UniFileHandler,
    UniStreamHandler,
    get_file_handler,
    get_stream_handler,
)

from ..configuration import Configuration, setup_configuration
from ..exceptions import InitialisationError
from ..factory import setup_factory
from ..reader.csv_reader import CsvReader
from .parse_user_import_cmdline import ParseUserImportCmdline

CENTRAL_LOG_DIR = "/var/log/univention/ucs-school-import"
LAST_FAIL_LOG_SYMLINK = os.path.join(CENTRAL_LOG_DIR, "LAST-FAIL")
LAST_LOG_DEBUG_SYMLINK = os.path.join(CENTRAL_LOG_DIR, "LAST-LOG-DEBUG")
LAST_LOG_ERROR_SYMLINK = os.path.join(CENTRAL_LOG_DIR, "LAST-LOG-ERROR")


[docs] class CommandLine(object): import_initiator = "unknown" def __init__(self): self.logger = None # type: logging.Logger self.args = None self.config = None self.factory = None self.errors = [] self.user_import_summary_str = "" self._error_log_handler = None
[docs] def parse_cmdline(self): parser = ParseUserImportCmdline() self.args = parser.parse_cmdline() return self.args
[docs] def setup_logging(self, stdout=False, filename=None, uid=None, gid=None, mode=None): # we're called twice: # once after parsing the cmdline, if no `-v` was given, INFO is used, # then again after reading the configuration files, the loglevel may be different now self.logger = logging.getLogger("ucsschool") self.logger.setLevel(logging.DEBUG) # update existing stdout loggers, add one if none exist stream_handlers = [ _handler for _handler in self.logger.handlers if isinstance(_handler, UniStreamHandler) ] for handler in stream_handlers: handler.setLevel(logging.DEBUG if stdout else logging.INFO) if not stream_handlers: self.logger.addHandler(get_stream_handler("DEBUG" if stdout else "INFO")) # add debug and error file handlers if non exist (and a filename was given) file_handlers = [ _handler for _handler in self.logger.handlers if isinstance(_handler, UniFileHandler) ] if filename and not file_handlers: self.logger.addHandler(get_file_handler("DEBUG", filename, uid=uid, gid=gid, mode=mode)) self.create_symlink(filename, LAST_LOG_DEBUG_SYMLINK) log_dir = os.path.dirname(filename) error_log_path = os.path.join(log_dir, "ucs-school-import-error.log") # set INFO level now, so the configuration will also end up in the logfile # will be raised to ERROR directly after logging the configuration self._error_log_handler = get_file_handler( "INFO", error_log_path, uid=uid, gid=gid, mode=mode ) self.logger.addHandler(self._error_log_handler) self.create_symlink(error_log_path, LAST_LOG_ERROR_SYMLINK) return self.logger
[docs] def setup_config(self): configs = self.configuration_files if self.args.conffile and self.args.conffile not in configs: configs.append(self.args.conffile) try: self.config = setup_configuration(configs, **self.args.settings) except InitialisationError as exc: self.logger.error("Used configuration files: %s.", configs) self.logger.error("Used command line arguments: %r", self.args.settings) try: # if it was a config check error, the config singleton already exists config = Configuration() self.logger.error("Configuration is:\n%s", pprint.pformat(config)) except InitialisationError: pass raise exc return self.config
@property def configuration_files(self): # type: () -> List[str] """ IMPLEMENTME to add module specific configuration files: res = super(YouClass, self).configuration_files res.append("/your/config.json") return res :return: list of filenames :rtype: list(str) """ return [ "/usr/share/ucs-school-import/configs/global_defaults.json", "/var/lib/ucs-school-import/configs/global.json", ]
[docs] def do_import(self): importer = self.factory.make_mass_importer(self.config["dry_run"]) self.logger.info("------ Starting mass import... ------") try: importer.mass_import() except Exception: # pylint: disable=broad-except logfile = os.path.realpath(LAST_LOG_DEBUG_SYMLINK) self.create_symlink(logfile, LAST_FAIL_LOG_SYMLINK) if logfile.startswith("/var/lib/ucs-school-import"): job_id = os.path.split(os.path.dirname(logfile))[-1] job_id = "job_{:0>4}".format(job_id) else: job_id = "cmdline" link = os.path.join( CENTRAL_LOG_DIR, "FAIL_{:%Y-%m-%d_%H:%M:%S}_{}".format(datetime.now(), job_id) ) self.create_symlink(logfile, link) etype, exc, etraceback = sys.exc_info() six.reraise(etype, exc, etraceback) finally: self.errors = importer.errors self.user_import_summary_str = importer.user_import_stats_str # log result to error log (was logged before at INFO level) if self.user_import_summary_str: log_msgs = ( "------ User import statistics ------\n" "{}\n" "------ End of user import statistics ------".format(self.user_import_summary_str) ) record = self.logger.makeRecord( self.logger.name, logging.INFO, "ucs-school-import-error.log", 0, log_msgs, (), None, "user_import_stats_str", None, ) self._error_log_handler.handle(record) self.logger.info("------ Mass import finished. ------")
[docs] def prepare_import(self): self.parse_cmdline() # early logging configured by cmdline self.setup_logging(self.args.verbose, self.args.logfile) self.logger.info("Loading UCS@school import configuration...") self.setup_config() # logging configured by config file self.setup_logging(self.config["verbose"], self.config["logfile"]) self.logger.info("------ UCS@school import tool starting ------") self.logger.info( "Import started by %s (class %r).", self.import_initiator, self.__class__.__name__ ) filename = self.config["input"]["filename"] encoding = CsvReader.get_encoding(filename) try: with open(filename, encoding=encoding) as fin: line = fin.readline() self.logger.info("First line of %r:\n%r", self.config["input"]["filename"], line) except (LookupError, UnicodeDecodeError): self.logger.warning( ( "Detected encoding {0} is not a valid default encoding." " If no compatible custom reader class is used," " the program will fail in subsequent steps." ).format(encoding) ) self.logger.info("------ UCS@school import tool configured ------") self.logger.info("Used configuration files: %s.", self.config.conffiles) self.logger.info("Using command line arguments: %r", self.args.settings) self.logger.info("Configuration is:\n%s", pprint.pformat(self.config)) self._error_log_handler.setLevel("ERROR") self.factory = setup_factory(self.config["factory"])
[docs] def main(self): # type: () -> int try: self.prepare_import() self.do_import() if self.errors: # at least one non-fatal error msg = "Import finished normally but with errors." self.logger.warning(msg) return 2 except InitialisationError as exc: if exc.log_traceback: log = self.logger.exception else: log = self.logger.error log(str(exc)) return 1 except Exception as exc: # pylint: disable=broad-except self.logger.exception("{}: {!s}".format(exc.__class__.__name__, exc)) return 1