Skip to content
Snippets Groups Projects
Select Git revision
  • b3df54d3a290c0f639446f817b55d40698b453a8
  • master default protected
2 results

configproxy.py

Blame
  • Forked from protokollsystem / proto3
    Source project has a limited visibility.
    Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    configproxy.py 38.17 KiB
    #!/usr/bin/env python3
    import os
    from common import auth
    
    import logging
    logging.basicConfig(level=logging.INFO)
    logger = logging.getLogger(__name__)
    
    
    def split_line(line, max_length=76):
        words = list(filter(None, map(str.strip, line.split(" "))))
        lines = []
        current_line = []
        current_line_length = 0
        for word in words:
            if (current_line and current_line_length + len(word) + 1 > max_length):
                lines.append(" ".join(current_line))
                current_line = []
                current_line_length = 0
            current_line.append(word)
            current_line_length += len(word) + 1
        if current_line:
            lines.append(" ".join(current_line))
        return lines
    
    
    def prefix_lines(lines, prefix="#  "):
        return map(lambda line: "".join((prefix, line)), lines)
    
    
    class ConfigEntry:
        def __init__(self, name, default, required=True, internal=True,
                     immutable=False, no_sane_default=False,
                     description=None, value=None):
            self.name = name
            self.default = default
            self.required = required
            self.internal = internal
            self.immutable = immutable
            self.no_sane_default = no_sane_default
            self.description = description
            self.value = value
    
        def extract(self, config):
            if self.immutable:
                self.value = self.default
                return
            if not hasattr(config, self.name) and self.required:
                raise ValueError(
                    "Missing required config entry {}!".format(self.name))
            self.value = getattr(config, self.name, self.default)
            if self.no_sane_default and self.value == self.default:
                raise ValueError(
                    "You have not configured {}, which is required!".format(
                        self.name))
    
        def get_example(self):
            if self.immutable:
                return []
            lines = []
            if self.description is not None:
                lines.extend(prefix_lines(split_line(self.description)))
            entry_line = "{} = {}".format(self.name, repr(self.default))
            if not self.required:
                entry_line = "# {}".format(entry_line)
            lines.append(entry_line)
            return lines
    
    
    class ConfigSection:
        def __init__(self, name, entries, check, description=None,
                     deactivatable=False, active=None, imports=None):
            self.name = name
            self.entries = entries
            self.check = check
            self.description = description
            self.deactivatable = deactivatable
            self.active = active
            if isinstance(imports, str):
                imports = [imports]
            self.imports = imports
    
        def extract(self, config):
            if self.deactivatable:
                self.active = getattr(config, "{}_ACTIVE".format(self.name), False)
                if not self.active:
                    return
            for entry in self.entries:
                entry.extract(config)
    
        def get_example(self):
            lines = []
            if self.imports:
                lines.extend(self.imports)
            header_line = (
                "# [{}]".format(self.name) +
                (" (optional)" if self.deactivatable else ""))
            lines.append(header_line)
            if self.description is not None:
                lines.extend(prefix_lines(split_line(self.description)))
                lines.append("")
            if self.deactivatable:
                lines.extend(prefix_lines(split_line("deactivate with")))
                lines.append("# {}_ACTIVE = False".format(self.name))
                lines.append("")
            for entry in self.entries:
                lines.extend(entry.get_example())
                lines.append("")
            lines.append("")
            return "\n".join(lines)
    
        def fill_modules(self, config, public_config):
            if self.deactivatable:
                setattr(config, "{}_ACTIVE".format(self.name), self.active)
                setattr(public_config, "{}_ACTIVE".format(self.name), self.active)
                if not self.active:
                    return
            for entry in self.entries:
                setattr(config, entry.name, entry.value)
                if not entry.internal:
                    setattr(public_config, entry.name, entry.value)
    
    
    def check_choice(option, value, choices):
        if value not in choices:
            raise ValueError(
                "{} is not allowed choices! Should be one of {}, is {}".format(
                    option, choices, value))
    
    
    def check_database(
            SQLALCHEMY_DATABASE_URI,
            SQLALCHEMY_TRACK_MODIFICATIONS):
        from sqlalchemy_utils import database_exists
        from sqlalchemy import exc
        try:
            if not database_exists(SQLALCHEMY_DATABASE_URI):
                raise ValueError(
                    "The database '{}' does not exist! "
                    "Please configure it correctly or create it!".format(
                        SQLALCHEMY_DATABASE_URI))
        except (exc.NoSuchModuleError, exc.OperationalError) as error:
            raise ValueError(
                "The database uri '{}' does not state a valid "
                "database: {}".format(SQLALCHEMY_DATABASE_URI, error))
    
    
    def check_security(SECRET_KEY, SECURITY_KEY, SESSION_PROTECTION):
        MIN_KEY_LENGTH = 20
        if len(SECRET_KEY) < MIN_KEY_LENGTH:
            raise ValueError(
                "Insufficient length of SECRET_KEY, should be at least {}!".format(
                    MIN_KEY_LENGTH))
        if len(SECURITY_KEY) < MIN_KEY_LENGTH:
            raise ValueError(
                "Insufficient length of SECURITY_KEY, should be at "
                "least {}!".format(
                    MIN_KEY_LENGTH))
        check_choice("SESSION_PROTECTION", SESSION_PROTECTION, ["strong"])
    
    
    def check_server_name(SERVER_NAME, PREFERRED_URL_SCHEME):
        # todo: check ip address and server name
        check_choice(
            "PREFERRED_URL_SCHEME", PREFERRED_URL_SCHEME,
            ["http", "https"])
    
    
    def check_debug(DEBUG):
        if DEBUG:
            logger.warning("DEBUG mode is activated!")
    
    
    def check_celery(
            CELERY_BROKER_URL, CELERY_TASK_SERIALIZER, CELERY_ACCEPT_CONTENT):
        pass
        # todo: check broker url
        check_choice(
            "CELERY_TASK_SERIALIZER", CELERY_TASK_SERIALIZER,
            ["pickle"])
        check_choice(
            "CELERY_ACCEPT_CONTENT", CELERY_ACCEPT_CONTENT,
            [["pickle"]])
    
    
    def check_sentry(SENTRY_DSN):
        pass
    
    
    def check_authentication(AUTH_MAX_DURATION, AUTH_BACKENDS):
        if AUTH_MAX_DURATION <= 0:
            raise ValueError(
                "AUTH_MAX_DURATION should be positive, is {}!".format(
                    AUTH_MAX_DURATION))
        if not AUTH_BACKENDS:
            raise ValueError("No authentication backends have been configured!")
    
    
    def check_error_report_context(ERROR_CONTEXT_LINES):
        if ERROR_CONTEXT_LINES < 0:
            raise ValueError(
                "ERROR_CONTEXT_LINES should be positive, is {}!".format(
                    ERROR_CONTEXT_LINES))
    
    
    def check_pagination(PAGE_LENGTH, PAGE_DIFF):
        if PAGE_LENGTH <= 0:
            raise ValueError(
                "PAGE_LENGTH should be positive, is {}!".format(
                    PAGE_LENGTH))
        if PAGE_DIFF < 0:
            raise ValueError(
                "PAGE_DIFF should be positive, is {}!".format(
                    PAGE_DIFF))
    
    
    def check_index_page(
            MAX_INDEX_DAYS, MAX_PAST_INDEX_DAYS,
            MAX_PAST_INDEX_DAYS_BEFORE_REMINDER):
        if MAX_INDEX_DAYS < 0:
            raise ValueError(
                "MAX_INDEX_DAYS should be positive, is {}!".format(
                    MAX_INDEX_DAYS))
        if MAX_PAST_INDEX_DAYS < 0:
            raise ValueError(
                "MAX_PAST_INDEX_DAYS should be positive, is {}!".format(
                    MAX_PAST_INDEX_DAYS))
        if MAX_PAST_INDEX_DAYS_BEFORE_REMINDER < 0:
            raise ValueError(
                "MAX_PAST_INDEX_DAYS_BEFORE_REMINDER should be "
                "positive, is {}!".format(
                    MAX_PAST_INDEX_DAYS_BEFORE_REMINDER))
    
    
    def check_admin_data(ADMIN_MAIL, ADMIN_GROUP):
        if not ADMIN_MAIL:
            raise ValueError("No admin mail address given!")
        if not ADMIN_GROUP:
            raise ValueError("No admin group given!")
    
    
    def check_parser(PARSER_LAZY, FUZZY_MIN_SCORE, PRIVATE_KEYWORDS):
        if PARSER_LAZY:
            logger.warning(
                "Parser lazy mode is activated, this is not meant or useful "
                "for production operation!")
        if not 0 <= FUZZY_MIN_SCORE <= 100:
            raise ValueError(
                "FUZZY_MIN_SCORE should be a percentage from 0 to 100")
        for keyword in PRIVATE_KEYWORDS:
            if not keyword:
                raise ValueError("Invalid private keyword given: {}".format(
                    keyword))
    
    
    def check_rendering(
            FONTS, DOCUMENTS_PATH, LATEX_BULLETPOINTS, HTML_LEVEL_OFFSET,
            LATEX_LOCAL_TEMPLATES, LATEX_LOGO_TEMPLATE, LATEX_GEOMETRY,
            LATEX_PAGESTYLE, LATEX_HEADER_FOOTER, LATEX_ADDITIONAL_PACKAGES,
            LATEX_TEMPLATES):
        for key in ("main", "roman", "sans", "mono"):
            if key not in FONTS:
                raise ValueError("No font for type {} given!".format(key))
            try:
                path = FONTS[key]["path"]
                extension = FONTS[key]["extension"]
                for face in ("regular", "bold", "italic", "bolditalic"):
                    if face not in FONTS[key]:
                        raise ValueError("Missing fontface {} for {} font!".format(
                            face, key))
                    filepath = os.path.join(
                        path, "".join((FONTS[key][face], extension)))
                    if not os.path.exists(filepath):
                        raise ValueError(
                            "Font {} does not exist in the filesystem! "
                            "Expected it at {}".format(
                                FONTS[key][face], filepath))
            except KeyError:
                raise ValueError(
                    "Missing path information in font specification "
                    "{key}: ".format(key=key))
        if not os.path.isdir(DOCUMENTS_PATH):
            raise ValueError(
                "Document savedir does not exist! Should be at {}.".format(
                    DOCUMENTS_PATH))
        if not LATEX_BULLETPOINTS:
            raise ValueError("LATEX_BULLETPOINTS is empty.")
        if not 1 <= HTML_LEVEL_OFFSET <= 4:
            raise ValueError(
                "HTML_LEVEL_OFFSET should be from 1 to 4, but is {}".format(
                    HTML_LEVEL_OFFSET))
        # todo: check templates stuff
    
    
    def check_mail(MAIL_FROM, MAIL_HOST, MAIL_USER, MAIL_PASSWORD,
                   MAIL_USE_TLS, MAIL_USE_STARTTLS):
        if MAIL_USE_TLS and MAIL_USE_STARTTLS:
            raise ValueError(
                "Both TLS and STARTTLS are set for Mail! Please set only one.")
        from utils import MailManager
        import socket
        import smtplib
        import ssl
        mail_config = Config()
        mail_config.MAIL_ACTIVE = True
        mail_config.MAIL_FROM = MAIL_FROM
        mail_config.MAIL_HOST = MAIL_HOST
        mail_config.MAIL_USER = MAIL_USER
        mail_config.MAIL_PASSWORD = MAIL_PASSWORD
        mail_config.MAIL_USE_TLS = MAIL_USE_TLS
        mail_config.MAIL_USE_STARTTLS = MAIL_USE_STARTTLS
        mail_manager = MailManager(mail_config)
        try:
            if not mail_manager.check():
                raise ValueError("The mail connection is improperly configured.")
        except (ConnectionRefusedError, socket.gaierror) as error:
            raise ValueError("Mail Cannot connect to the server: {}".format(error))
        except smtplib.SMTPAuthenticationError as error:
            raise ValueError("Mail Cannot authenticate: {}".format(error))
        except (ssl.SSLError, smtplib.SMTPNotSupportedError) as error:
            raise ValueError("TLS for Mail does not work: {}".format(error))
        except Exception as error:
            raise ValueError(
                "Testing the mail connection failed: {}".format(error))
    
    
    def check_printing(PRINTING_SERVER, PRINTING_USER, PRINTING_PRINTERS):
        LPR_PATH = "/usr/bin/lpr"
        LPSTAT_PATH = "/usr/bin/lpstat"
        LPOPTIONS_PATH = "/usr/bin/lpoptions"
        for path in [LPR_PATH, LPSTAT_PATH, LPOPTIONS_PATH]:
            if not os.path.exists(path):
                raise ValueError(
                    "{} is not installed! It is required for printing".format(
                        path))
        import subprocess as sp
        list_printers_command = [LPSTAT_PATH, "-h", PRINTING_SERVER, "-p"]
        raw_printers = sp.check_output(list_printers_command).decode("utf-8")
        printers = [
            line.split(" ")[1].lower()
            for line in raw_printers.splitlines()
            if not line.startswith(" ")
        ]
        configured_printers = [key.lower() for key in PRINTING_PRINTERS]
        missing_printers = set(configured_printers) - set(printers)
        if missing_printers:
            raise ValueError(
                "Some configured printers are not available: {}, "
                "available are: {}".format(
                    missing_printers, printers))
    
        def _strip_star(value):
            if value.startswith("*"):
                return value[1:]
            return value
        for printer in configured_printers:
            options_command = [
                LPOPTIONS_PATH, "-h", PRINTING_SERVER, "-p", printer, "-l"]
            raw_options = sp.check_output(options_command).decode("utf-8")
            available_options = {}
            for line in raw_options.splitlines():
                name_part, value_part = line.split(":")
                values = list(map(_strip_star, value_part.strip().split(" ")))
                for name in name_part.split("/"):
                    available_options[name] = values
            for option in PRINTING_PRINTERS[printer]:
                name, value = option.split("=")
                if name not in available_options:
                    logger.warning(
                        "Printer {} has unknown option {} set!".format(
                            printer, name))
                elif value not in available_options[name]:
                    logger.warning(
                        "Printer {} has the invalid value {} specified "
                        "for option {}! Valid values are {}.".format(
                            printer, value, name, available_options[name]))
    
    
    def check_etherpad(ETHERPAD_URL, EMPTY_ETHERPAD):
        import requests
        key = "Server"
        try:
            answer = requests.get(ETHERPAD_URL)
            if answer.status_code != 200:
                raise ValueError(
                    "The etherpad does not return 200 OK at {}".format(
                        ETHERPAD_URL))
            headers = answer.headers
            if key not in headers or "Etherpad" not in headers[key]:
                raise ValueError("{} does not look like an etherpad!".format(
                    ETHERPAD_URL))
        except requests.exceptions.ConnectionError as error:
            raise ValueError("Cannot connect to the etherpad at {}: {}".format(
                ETHERPAD_URL, error))
    
    
    def check_wiki(
            WIKI_TYPE, WIKI_API_URL, WIKI_ANONYMOUS,
            WIKI_USER, WIKI_PASSWORD, WIKI_DOMAIN):
        check_choice("WIKI_TYPE", WIKI_TYPE, ["MEDIAWIKI", "DOKUWIKI"])
        # todo: check the connection
    
    
    def check_calendar(
            CALENDAR_URL, CALENDAR_DEFAULT_DURATION, CALENDAR_MAX_REQUESTS):
        from calendarpush import Client, CalendarException
        try:
            client = Client(url=CALENDAR_URL)
            client.get_calendars()
        except (KeyError, CalendarException) as error:
            raise ValueError("Cannot connect to the calendar at {}!".format(
                CALENDAR_URL))
    
    
    def check_timezone(CALENDAR_TIMEZONE_MAP):
        pass
    
    
    CONFIG_SECTIONS = [
        ConfigSection(
            name="Database",
            entries=[
                ConfigEntry(
                    name="SQLALCHEMY_DATABASE_URI",
                    default="engine://user:password@host/database",
                    required=True, internal=True, no_sane_default=True,
                    description=(
                        "Database connection string. See "
                        "http://docs.sqlalchemy.org/en/latest/core/engines.html "
                        "for details. SQLite does not work with Alembic "
                        "migrations.")),
                ConfigEntry(
                    name="SQLALCHEMY_TRACK_MODIFICATIONS",
                    default=False,
                    required=False, internal=True, immutable=True,
                    description="Necessary option. Do not change."),
            ],
            check=check_database,
            description="Settings for SQLAlchemy"),
        ConfigSection(
            name="Security",
            entries=[
                ConfigEntry(
                    name="SECRET_KEY",
                    default=os.urandom(128),
                    required=True, internal=True,
                    description="Secret random key used for session security."),
                ConfigEntry(
                    name="SECURITY_KEY",
                    default=os.urandom(128),
                    required=True, internal=True,
                    description="Secret random key used for user sessions."),
                ConfigEntry(
                    name="SESSION_PROTECTION",
                    default="strong",
                    required=False, internal=True, immutable=True,
                    description="Flask setting for sessions. Do not change."),
            ],
            check=check_security,
            description="Secret keys and random strings"),
        ConfigSection(
            name="URL Root",
            entries=[
                ConfigEntry(
                    name="SERVER_NAME",
                    default="protokolle.example.com",
                    required=True, internal=False, no_sane_default=True,
                    description="Domain on which this website is hosted"),
                ConfigEntry(
                    name="PREFERRED_URL_SCHEME",
                    default="https",
                    required=False, internal=False,
                    description="Protocol used by this website. "
                                "Either 'http' or 'https'."),
                ConfigEntry(
                    name="CDN_URL",
                    default=None,
                    required=False, internal=False,
                    description="URL to get bootstrap and jQuery from.")
            ],
            check=check_server_name,
            description="Where is the website hosted"),
        ConfigSection(
            name="Debug",
            entries=[
                ConfigEntry(
                    name="DEBUG",
                    default=False,
                    required=False, internal=True,
                    description="Activate debug mode"),
            ],
            check=check_debug,
            description="Debug mode. Do not set in production."),
        ConfigSection(
            name="Celery",
            entries=[
                ConfigEntry(
                    name="CELERY_BROKER_URL",
                    default="redis://localhost:6379/0",
                    required=True, internal=True,
                    description=(
                        "Connection to the celery broker. See http://docs.celery"
                        "project.org/en/latest/userguide/configuration.html")),
                ConfigEntry(
                    name="CELERY_TASK_SERIALIZER",
                    default="pickle",
                    required=False, internal=True, immutable=True,
                    description="How celery serializes tasks. Do not change."),
                ConfigEntry(
                    name="CELERY_ACCEPT_CONTENT",
                    default=['pickle'],
                    required=False, internal=True, immutable=True,
                    description="How celery deserializes tasks. Do not change."),
            ],
            check=check_celery,
            description="Settings for the task scheduler."),
        ConfigSection(
            name="Sentry",
            entries=[
                ConfigEntry(
                    name="SENTRY_DSN",
                    default=None,
                    required=False, internal=True,
                    description=(
                        "Connection string for sentry. See "
                        "https://docs.sentry.io/quickstart/#configure-the-dsn")),
            ],
            check=check_sentry,
            description="Connection information for sentry exception reporting."),
        ConfigSection(
            name="Authentication",
            entries=[
                ConfigEntry(
                    name="AUTH_MAX_DURATION",
                    default=300,
                    required=False, internal=False,
                    description="Time in seconds for which non-permanent "
                                "sessions remain valid"),
                ConfigEntry(
                    name="AUTH_BACKENDS",
                    default=[
                        auth.StaticUserManager((("user", "password", ["group"]),)),
                    ],
                    required=True, internal=True, no_sane_default=True,
                    description="Active Authentication backends"),
            ],
            check=check_authentication,
            description="User Authentication backend settings.",
            imports="from common import auth"),
        ConfigSection(
            name="Error Report Context",
            entries=[
                ConfigEntry(
                    name="ERROR_CONTEXT_LINES",
                    default=3,
                    required=False, internal=False,
                    description=(
                        "Number of lines before and after an error to include "
                        "in the error description")),
            ],
            check=check_error_report_context,
            description="Compiling error context settings"),
        ConfigSection(
            name="Pagination",
            entries=[
                ConfigEntry(
                    name="PAGE_LENGTH",
                    default=20,
                    required=False, internal=False,
                    description="Default number of entries per page"),
                ConfigEntry(
                    name="PAGE_DIFF",
                    default=3,
                    required=False, internal=False,
                    description=(
                        "Number of pages before and after the current one "
                        "with a direct link.")),
            ],
            check=check_pagination,
            description="Pagination settings, used for list pages"),
        ConfigSection(
            name="Index Page",
            entries=[
                ConfigEntry(
                    name="MAX_INDEX_DAYS",
                    default=14,
                    required=False, internal=False,
                    description="Next days to list upcoming meetings on"),
                ConfigEntry(
                    name="MAX_PAST_INDEX_DAYS",
                    default=2,
                    required=False, internal=False,
                    description="Past days to list unfinished meetings on"),
                ConfigEntry(
                    name="MAX_PAST_INDEX_DAYS_BEFORE_REMINDER",
                    default=14,
                    required=False, internal=True,
                    description=(
                        "If a meeting is unfinished this many days after its "
                        "date, a reminder is sent by mail")),
            ],
            check=check_index_page,
            description="Settings for what to show on the index page"),
        ConfigSection(
            name="Admin data",
            entries=[
                ConfigEntry(
                    name="ADMIN_MAIL",
                    default="admin@example.com",
                    required=True, internal=False, no_sane_default=True,
                    description="Mail address to tell users to "
                                "contact in case of errors"),
                ConfigEntry(
                    name="ADMIN_GROUP",
                    default="admin",
                    required=True, internal=False,
                    description="Users with this group are admins and are "
                                "allowed to do and see everything."),
            ],
            check=check_admin_data,
            description="Settings for who is an admin and how to contact them"),
        ConfigSection(
            name="Parser",
            entries=[
                ConfigEntry(
                    name="PARSER_LAZY",
                    default=False,
                    required=False, internal=False,
                    description="Do not enforce some parser policies."),
                ConfigEntry(
                    name="FUZZY_MIN_SCORE",
                    default=90,
                    required=False, internal=False,
                    description="Todos with at least this equality score are "
                                "considered equal whe importing old protocols."),
                ConfigEntry(
                    name="PRIVATE_KEYWORDS",
                    default=["private", "internal", "privat", "intern"],
                    required=False, internal=False,
                    description="Keywords indicating private protocol parts"),
            ],
            check=check_parser,
            description="Settings for the protocol syntax parser"),
        ConfigSection(
            name="Rendering",
            entries=[
                ConfigEntry(
                    name="FONTS",
                    default={
                        "main": {
                            "extension": ".otf",
                            "path": "/usr/share/fonts/gsfonts/",
                            "regular": "NimbusSans-Regular",
                            "bold": "NimbusSans-Bold",
                            "italic": "NimbusSans-Italic",
                            "bolditalic": "NimbusSans-BoldItalic"
                        },
                        "roman": {
                            "extension": ".otf",
                            "path": "/usr/share/fonts/gsfonts/",
                            "regular": "NimbusRoman-Regular",
                            "bold": "NimbusRoman-Bold",
                            "italic": "NimbusRoman-Italic",
                            "bolditalic": "NimbusRoman-BoldItalic"
                        },
                        "sans": {
                            "extension": ".otf",
                            "path": "/usr/share/fonts/gsfonts/",
                            "regular": "NimbusSans-Regular",
                            "bold": "NimbusSans-Bold",
                            "italic": "NimbusSans-Italic",
                            "bolditalic": "NimbusSans-BoldItalic"
                        },
                        "mono": {
                            "extension": ".otf",
                            "path": "/usr/share/fonts/gsfonts/",
                            "regular": "NimbusMonoPS-Regular",
                            "bold": "NimbusMonoPS-Bold",
                            "italic": "NimbusMonoPS-Italic",
                            "bolditalic": "NimbusMonoPS-BoldItalic"
                        }
                    },
                    required=True, internal=False,
                    description="fonts for xelatex"),
                ConfigEntry(
                    name="DOCUMENTS_PATH",
                    default="documents",
                    required=False, internal=True,
                    description="Path to the directory to save protocols in. "
                                "Write access is necessary."),
                ConfigEntry(
                    name="LATEX_BULLETPOINTS",
                    default=[
                        r"\textbullet", r"\normalfont \bfseries \textendash",
                        r"\textasteriskcentered", r"\textperiodcentered"],
                    required=False, internal=False,
                    description="list of bulletpoints to use in latex"),
                ConfigEntry(
                    name="HTML_LEVEL_OFFSET",
                    default=3,
                    required=False, internal=False,
                    description="Header level at which to start with "
                                "HTML headlines"),
                ConfigEntry(
                    name="LATEX_LOCAL_TEMPLATES",
                    default=None,
                    required=False, internal=False,
                    description="path to additional jinja2 templates"),
                ConfigEntry(
                    name="LATEX_LOGO_TEMPLATE",
                    default=None,
                    required=False, internal=False,
                    description="template to include at the top of protocols"),
                ConfigEntry(
                    name="LATEX_GEOMETRY",
                    default="vmargin=1.5cm,hmargin={1.5cm,1.2cm},"
                            "bindingoffset=8mm",
                    required=False, internal=False,
                    description="custom latex page geometry"),
                ConfigEntry(
                    name="LATEX_PAGESTYLE",
                    default=None,
                    required=False, internal=False,
                    description="custom latex pagestyle, e.g. 'fancy'"),
                ConfigEntry(
                    name="LATEX_HEADER_FOOTER",
                    default=False,
                    required=False, internal=False,
                    description="Include a header and footer in protocols"),
                ConfigEntry(
                    name="LATEX_ADDITIONAL_PACKAGES",
                    default=None,
                    required=False, internal=False,
                    description="Include additional latex packages in protocols"),
                ConfigEntry(
                    name="LATEX_TEMPLATES",
                    default=None,
                    required=False, internal=False,
                    description=(
                        "define multiple LaTeX-templates to use with a each "
                        "protocol type individually overriding the general LATEX "
                        "options the LATEX_LOCAL_TEMPLATES parameter is need to "
                        "provide the path for the templates each template must "
                        "be placed in an individual folder named by its ID in "
                        "LATEX_TEMPLATES and must contain the provided template "
                        "files: e.g. the files for the template 'yourtemplate' "
                        "need to be in the folder named 'yourtemplate', and the "
                        "templates provides the files: 'protokoll2.cls' (class), "
                        "'protocol.tex' (protocol), 'decision.tex' (decision) and "
                        "'asta-logo.tex'")),
            ],
            check=check_rendering,
            description="Settings for rendering protocols to pdf, html, etc."),
        ConfigSection(
            name="MAIL",
            entries=[
                ConfigEntry(
                    name="MAIL_FROM",
                    default="protokolle@example.com",
                    required=True, internal=False, no_sane_default=True,
                    description="Mail sender address"),
                ConfigEntry(
                    name="MAIL_HOST",
                    default="mail.example.com:465",
                    required=True, internal=True, no_sane_default=True,
                    description="SMTP Mail server address with port"),
                ConfigEntry(
                    name="MAIL_USER",
                    default="",
                    required=False, internal=True,
                    description="Mail server login user. "
                                "Empty for no authentication."),
                ConfigEntry(
                    name="MAIL_PASSWORD",
                    default="",
                    required=False, internal=True,
                    description="Mail server login password. "
                                "Empty for no authentication."),
                ConfigEntry(
                    name="MAIL_USE_TLS",
                    default=True,
                    required=False, internal=True,
                    description="Use SMPTS (not STARTTLS). Should match port."),
                ConfigEntry(
                    name="MAIL_USE_STARTTLS",
                    default=False,
                    required=False, internal=True,
                    description="Use SMTP with STARTTLS. Should match port."),
    
            ],
            check=check_mail,
            deactivatable=True,
            description="Mail server connection"),
        ConfigSection(
            name="PRINTING",
            entries=[
                ConfigEntry(
                    name="PRINTING_SERVER",
                    default="printsrv.example.com:631",
                    required=True, internal=True, no_sane_default=True,
                    description="CUPS printserver connection"),
                ConfigEntry(
                    name="PRINTING_USER",
                    default="protocols",
                    required=False, internal=True,
                    description="CUPS user for print jobs"),
                ConfigEntry(
                    name="PRINTING_PRINTERS",
                    default={
                        "example_printer":
                            ["Duplex=DuplexNoTumble", "option2=value"],
                        "other_printer": ["list", "of", "options"],
                    },
                    required=True, internal=False, no_sane_default=True,
                    description="printers with corresponding options"),
            ],
            check=check_printing,
            deactivatable=True,
            description="CUPS printing settings"),
        ConfigSection(
            name="ETHERPAD",
            entries=[
                ConfigEntry(
                    name="ETHERPAD_URL",
                    default="https://example.com/etherpad",
                    required=True, internal=False, no_sane_default=True,
                    description=(
                        "URL of the etherpad installation. "
                        "Do not include the '/p'!")),
                ConfigEntry(
                    name="EMPTY_ETHERPAD",
                    default="\n".join([
                        "Welcome to Etherpad!",
                        "",
                        "This pad text is synchronized as you type, so that "
                        "everyone viewing this page sees the same text. This "
                        "allows you to collaborate seamlessly on documents!",
                        "",
                        "Get involved with Etherpad at http://etherpad.org",
                        ""
                    ]),
                    required=False, internal=False,
                    description="The content a new etherpad contains."),
            ],
            check=check_etherpad,
            deactivatable=True,
            description="Etherpad settings"),
        ConfigSection(
            name="WIKI",
            entries=[
                ConfigEntry(
                    name="WIKI_TYPE",
                    default="MEDIAWIKI",
                    required=True, internal=False,
                    description="'MEDIAWIKI' or 'DOKUWIKI'"),
                ConfigEntry(
                    name="WIKI_API_URL",
                    default="https://wiki.example.com/wiki/api.php",
                    required=True, internal=True, no_sane_default=True,
                    description=(
                        "API Endpoint for Mediawiki, "
                        "'https://user:password@wiki.example.com/lib/exe/"
                        "xmlrpc.php' for Dokuwiki")),
                ConfigEntry(
                    name="WIKI_ANONYMOUS",
                    default=False,
                    required=False, internal=True,
                    description="Skip login (only for Mediawiki)"),
                ConfigEntry(
                    name="WIKI_USER",
                    default=None,
                    required=False, internal=True,
                    description="Login user (only for Mediawiki)"),
                ConfigEntry(
                    name="WIKI_PASSWORD",
                    default=None,
                    required=False, internal=True,
                    description="Login password (only for Mediawiki)"),
                ConfigEntry(
                    name="WIKI_DOMAIN",
                    default=None,
                    required=False, internal=True,
                    description="Login domain (only for Mediawiki)"),
            ],
            check=check_wiki,
            deactivatable=True,
            description="Mediawiki or Dokuwiki settings"),
        ConfigSection(
            name="CALENDAR",
            entries=[
                ConfigEntry(
                    name="CALENDAR_URL",
                    default="https://user:password@calendar.example.com/dav/",
                    required=True, internal=True, no_sane_default=True,
                    description="CalDAV server URL"),
                ConfigEntry(
                    name="CALENDAR_DEFAULT_DURATION",
                    default=3,
                    required=False, internal=False,
                    description="Default meeting length in hours"),
                ConfigEntry(
                    name="CALENDAR_MAX_REQUESTS",
                    default=10,
                    required=False, internal=False,
                    description=(
                        "Number of retries before giving a connection attempt up. "
                        "Some CalDAV servers reply randomly with errors.")),
            ],
            check=check_calendar,
            deactivatable=True,
            description="CalDAV settings"),
        ConfigSection(
            name="TIMEZONE",
            entries=[
                ConfigEntry(
                    name="CALENDAR_TIMEZONE_MAP",
                    default={
                        "CET": "Europe/Berlin",
                        "CEST": "Europe/Berline",
                    },
                    required=False, internal=False,
                    description="Timezone abbreviation map. Add as needed."),
            ],
            check=check_timezone,
            description="Settings for translating timezone information."),
    ]
    
    
    class Config:
        def __init__(self, data=None):
            if data is None:
                data = {}
            object.__setattr__(self, "data", data)
    
        def __setattr__(self, key, value):
            self.data[key] = value
    
        def __getattr__(self, key):
            try:
                return self.data[key]
            except KeyError:
                raise AttributeError
    
        def __dir__(self):
            return self.data.keys()
    
    
    def import_config(sections=CONFIG_SECTIONS):
        import config
        for section in sections:
            section.extract(config)
        internal_config, public_config = Config(), Config()
        for section in sections:
            section.fill_modules(internal_config, public_config)
        return internal_config, public_config
    
    
    def check_config(sections=CONFIG_SECTIONS):
        import config
        successful = True
        for section in sections:
            section.extract(config)
            if not section.deactivatable or section.active:
                logger.info("Checking {}".format(section.name))
                try:
                    section.check(
                        **{entry.name: entry.value for entry in section.entries})
                except ValueError as error:
                    logger.error(error.args[0])
                    successful = False
        return successful
    
    
    def write_example_config(
            sections=CONFIG_SECTIONS, filename="config.py.example"):
        example_config = "\n".join([
            section.get_example()
            for section in sections
        ])
        with open(filename, "w") as example_config_file:
            example_config_file.write(example_config)
    
    
    if __name__ == "__main__":
        import argparse
        import sys
        parser = argparse.ArgumentParser(
            description="Create and check the config")
        subparsers = parser.add_subparsers(dest="command")
        subparsers.required = True
    
        check_parser = subparsers.add_parser("check")
        check_parser.add_argument(
            "--log-level", choices=["error", "warning", "info", "debug"])
    
        create_parser = subparsers.add_parser("create")
        create_parser.add_argument("--filename", default="config.py.example")
    
        show_parser = subparsers.add_parser("show")
    
        arguments = parser.parse_args()
    
        if arguments.command == "check":
            if arguments.log_level:
                logger.setLevel(arguments.log_level.upper())
            sys.exit(not check_config())
        elif arguments.command == "create":
            filename = arguments.filename
            write_example_config(filename=filename)
            print("An example config has been generated at {}. "
                  "Please change the requried entries.".format(filename))
        elif arguments.command == "show":
            import pprint
            internal_config, public_config = import_config()
            print("The complete config is:")
            pprint.pprint(internal_config.data)
            print("The public part is:")
            pprint.pprint(public_config.data)