From 2dcc654b70c65f115002735b59ebf60362175968 Mon Sep 17 00:00:00 2001 From: jo Date: Tue, 1 Mar 2022 13:22:24 +0100 Subject: [PATCH] feat(api): load config using shared helpers - add django settings module documentation - use default for previously required fields BREAKING CHANGE: The API command line interface require the configuration file to be present. The default configuration file path is `/etc/airtime/airtime.conf` --- .github/workflows/test.yml | 18 +-- api/install/systemd/libretime-api.service | 1 + api/libretime_api/cli.py | 1 + api/libretime_api/permissions.py | 2 +- api/libretime_api/settings/README.md | 7 + api/libretime_api/settings/__init__.py | 46 ++++++ .../{settings.py => settings/_internal.py} | 141 ++++++------------ api/libretime_api/settings/_schema.py | 12 ++ api/libretime_api/tests/test_permissions.py | 2 +- api/libretime_api/tests/test_views.py | 4 +- api/libretime_api/utils.py | 30 ---- api/setup.py | 1 + 12 files changed, 119 insertions(+), 146 deletions(-) create mode 100644 api/libretime_api/settings/README.md create mode 100644 api/libretime_api/settings/__init__.py rename api/libretime_api/{settings.py => settings/_internal.py} (54%) create mode 100644 api/libretime_api/settings/_schema.py delete mode 100644 api/libretime_api/utils.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0df75e23e..788caf41f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -192,6 +192,7 @@ jobs: postgres: image: postgres env: + POSTGRES_USER: libretime POSTGRES_PASSWORD: libretime options: >- --health-cmd pg_isready @@ -204,7 +205,8 @@ jobs: run: shell: bash env: - LIBRETIME_CONFIG_FILEPATH: /tmp/libretime-test.conf + LIBRETIME_GENERAL_API_KEY: test_key + LIBRETIME_DATABASE_HOST: postgres steps: - uses: actions/checkout@v2 @@ -216,20 +218,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip-${{ matrix.context }} - - name: Setup libretime configuration - run: | - cat < $LIBRETIME_CONFIG_FILEPATH - [general] - api_key = test_key - [database] - host = postgres - port = 5432 - name = libretime - user = postgres - password = libretime - EOF - cat $LIBRETIME_CONFIG_FILEPATH - - name: Test run: make test working-directory: ${{ matrix.context }} diff --git a/api/install/systemd/libretime-api.service b/api/install/systemd/libretime-api.service index e938740a6..e036aa985 100644 --- a/api/install/systemd/libretime-api.service +++ b/api/install/systemd/libretime-api.service @@ -7,6 +7,7 @@ NotifyAccess=all KillSignal=SIGQUIT Environment=LIBRETIME_LOG_FILEPATH=/var/log/libretime/api.log +Environment=LIBRETIME_CONFIG_FILEPATH=/etc/airtime/airtime.conf ExecStart=/usr/bin/uwsgi /etc/airtime/libretime-api.ini User=libretime-api diff --git a/api/libretime_api/cli.py b/api/libretime_api/cli.py index 2e8854318..7e94f7df0 100755 --- a/api/libretime_api/cli.py +++ b/api/libretime_api/cli.py @@ -7,6 +7,7 @@ import sys def main(): os.environ.setdefault("DJANGO_SETTINGS_MODULE", "libretime_api.settings") + os.environ.setdefault("LIBRETIME_CONFIG_FILEPATH", "/etc/airtime/airtime.conf") try: from django.core.management import execute_from_command_line except ImportError as exc: diff --git a/api/libretime_api/permissions.py b/api/libretime_api/permissions.py index 22dc5a34f..916a5eba7 100644 --- a/api/libretime_api/permissions.py +++ b/api/libretime_api/permissions.py @@ -55,7 +55,7 @@ def check_authorization_header(request): if auth_header.startswith("Api-Key"): token = auth_header.split()[1] - if token == settings.CONFIG.get("general", "api_key"): + if token == settings.CONFIG.general.api_key: return True return False diff --git a/api/libretime_api/settings/README.md b/api/libretime_api/settings/README.md new file mode 100644 index 000000000..7f8b5d509 --- /dev/null +++ b/api/libretime_api/settings/README.md @@ -0,0 +1,7 @@ +# Django settings + +The structure of the django settings module is the following: + +- the `__init__.py` (`libretime_api.settings`) module is the django settings entrypoint. The module contains bindings between the user configuration and the django settings. **Advanced users** may edit this file to better integrate the LibreTime API in their setup. +- the `_internal.py` module contains application settings for django. +- the `_schema.py` module contains the schema for the user configuration parsing and validation. diff --git a/api/libretime_api/settings/__init__.py b/api/libretime_api/settings/__init__.py new file mode 100644 index 000000000..0ecc8fbe2 --- /dev/null +++ b/api/libretime_api/settings/__init__.py @@ -0,0 +1,46 @@ +# pylint: disable=unused-import +from os import getenv + +from ._internal import ( + AUTH_PASSWORD_VALIDATORS, + AUTH_USER_MODEL, + DEBUG, + INSTALLED_APPS, + MIDDLEWARE, + REST_FRAMEWORK, + ROOT_URLCONF, + TEMPLATES, + TEST_RUNNER, + WSGI_APPLICATION, + setup_logger, +) +from ._schema import Config + +API_VERSION = "2.0.0" + +LIBRETIME_LOG_FILEPATH = getenv("LIBRETIME_LOG_FILEPATH") +LIBRETIME_CONFIG_FILEPATH = getenv("LIBRETIME_CONFIG_FILEPATH") + +CONFIG = Config(filepath=LIBRETIME_CONFIG_FILEPATH) + +SECRET_KEY = CONFIG.general.api_key +ALLOWED_HOSTS = ["*"] + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "HOST": CONFIG.database.host, + "PORT": CONFIG.database.port, + "NAME": CONFIG.database.name, + "USER": CONFIG.database.user, + "PASSWORD": CONFIG.database.password, + } +} + +LANGUAGE_CODE = "en-us" +TIME_ZONE = "UTC" +USE_I18N = True +USE_L10N = True +USE_TZ = True + +LOGGING = setup_logger(LIBRETIME_LOG_FILEPATH) diff --git a/api/libretime_api/settings.py b/api/libretime_api/settings/_internal.py similarity index 54% rename from api/libretime_api/settings.py rename to api/libretime_api/settings/_internal.py index 89acea7cf..71604c202 100644 --- a/api/libretime_api/settings.py +++ b/api/libretime_api/settings/_internal.py @@ -1,32 +1,8 @@ -import os - -from .utils import get_random_string, read_config_file - -API_VERSION = "2.0.0" - -LIBRETIME_LOG_FILEPATH = os.getenv("LIBRETIME_LOG_FILEPATH") -LIBRETIME_CONFIG_FILEPATH = os.getenv( - "LIBRETIME_CONFIG_FILEPATH", - "/etc/airtime/airtime.conf", -) -LIBRETIME_STATIC_ROOT = os.getenv( - "LIBRETIME_STATIC_ROOT", - "/usr/share/airtime/api", -) -CONFIG = read_config_file(LIBRETIME_CONFIG_FILEPATH) - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = get_random_string(CONFIG.get("general", "api_key", fallback="")) +from os import getenv +from typing import Optional # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = os.getenv("LIBRETIME_DEBUG", False) - -ALLOWED_HOSTS = ["*"] - +DEBUG = getenv("LIBRETIME_DEBUG") # Application definition @@ -72,22 +48,6 @@ TEMPLATES = [ WSGI_APPLICATION = "libretime_api.wsgi.application" - -# Database -# https://docs.djangoproject.com/en/3.0/ref/settings/#databases - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.postgresql", - "NAME": CONFIG.get("database", "name", fallback="libretime"), - "USER": CONFIG.get("database", "user", fallback="libretime"), - "PASSWORD": CONFIG.get("database", "password", fallback="libretime"), - "HOST": CONFIG.get("database", "host", fallback="localhost"), - "PORT": CONFIG.get("database", "port", fallback="5432"), - } -} - - # Password validation # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators @@ -129,66 +89,53 @@ REST_FRAMEWORK = { "URL_FIELD_NAME": "item_url", } - -# Internationalization -# https://docs.djangoproject.com/en/3.0/topics/i18n/ - -LANGUAGE_CODE = "en-us" - -TIME_ZONE = "UTC" - -USE_I18N = True - -USE_L10N = True - -USE_TZ = True - - AUTH_USER_MODEL = "libretime_api.User" TEST_RUNNER = "libretime_api.tests.runners.ManagedModelTestRunner" -LOGGING_HANDLERS = { - "console": { - "level": "INFO", - "class": "logging.StreamHandler", - "formatter": "simple", - }, -} - -if LIBRETIME_LOG_FILEPATH is not None: - LOGGING_HANDLERS["file"] = { - "level": "DEBUG", - "class": "logging.FileHandler", - "filename": LIBRETIME_LOG_FILEPATH, - "formatter": "verbose", +# Logging +def setup_logger(log_filepath: Optional[str]): + logging_handlers = { + "console": { + "level": "INFO", + "class": "logging.StreamHandler", + "formatter": "simple", + }, } -LOGGING = { - "version": 1, - "disable_existing_loggers": False, - "formatters": { - "simple": { - "format": "{levelname} {message}", - "style": "{", + if log_filepath is not None: + logging_handlers["file"] = { + "level": "DEBUG", + "class": "logging.FileHandler", + "filename": log_filepath, + "formatter": "verbose", + } + + return { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "simple": { + "format": "{levelname} {message}", + "style": "{", + }, + "verbose": { + "format": "{asctime} {module} {levelname} {message}", + "style": "{", + }, }, - "verbose": { - "format": "{asctime} {module} {levelname} {message}", - "style": "{", + "handlers": logging_handlers, + "loggers": { + "django": { + "handlers": logging_handlers.keys(), + "level": "INFO", + "propagate": True, + }, + "libretime_api": { + "handlers": logging_handlers.keys(), + "level": "INFO", + "propagate": True, + }, }, - }, - "handlers": LOGGING_HANDLERS, - "loggers": { - "django": { - "handlers": LOGGING_HANDLERS.keys(), - "level": "INFO", - "propagate": True, - }, - "libretime_api": { - "handlers": LOGGING_HANDLERS.keys(), - "level": "INFO", - "propagate": True, - }, - }, -} + } diff --git a/api/libretime_api/settings/_schema.py b/api/libretime_api/settings/_schema.py new file mode 100644 index 000000000..80b1b876f --- /dev/null +++ b/api/libretime_api/settings/_schema.py @@ -0,0 +1,12 @@ +from libretime_shared.config import ( + BaseConfig, + DatabaseConfig, + GeneralConfig, + RabbitMQConfig, +) + + +class Config(BaseConfig): + general: GeneralConfig + database: DatabaseConfig = DatabaseConfig() + rabbitmq: RabbitMQConfig = RabbitMQConfig() diff --git a/api/libretime_api/tests/test_permissions.py b/api/libretime_api/tests/test_permissions.py index 685080080..0735fe4ea 100644 --- a/api/libretime_api/tests/test_permissions.py +++ b/api/libretime_api/tests/test_permissions.py @@ -33,7 +33,7 @@ class TestIsSystemTokenOrUser(APITestCase): self.assertFalse(allowed) def test_token_correct(self): - token = settings.CONFIG.get("general", "api_key") + token = settings.CONFIG.general.api_key request = APIRequestFactory().get(self.path) request.user = AnonymousUser() request.META["Authorization"] = f"Api-Key {token}" diff --git a/api/libretime_api/tests/test_views.py b/api/libretime_api/tests/test_views.py index 9f65838e6..aa695dd38 100644 --- a/api/libretime_api/tests/test_views.py +++ b/api/libretime_api/tests/test_views.py @@ -14,7 +14,7 @@ class TestFileViewSet(APITestCase): @classmethod def setUpTestData(cls): cls.path = "/api/v2/files/{id}/download/" - cls.token = settings.CONFIG.get("general", "api_key") + cls.token = settings.CONFIG.general.api_key def test_invalid(self): path = self.path.format(id="a") @@ -49,7 +49,7 @@ class TestScheduleViewSet(APITestCase): @classmethod def setUpTestData(cls): cls.path = "/api/v2/schedule/" - cls.token = settings.CONFIG.get("general", "api_key") + cls.token = settings.CONFIG.general.api_key def test_schedule_item_full_length(self): music_dir = baker.make( diff --git a/api/libretime_api/utils.py b/api/libretime_api/utils.py deleted file mode 100644 index c188f86d1..000000000 --- a/api/libretime_api/utils.py +++ /dev/null @@ -1,30 +0,0 @@ -import random -import string -import sys -from configparser import ConfigParser - - -def read_config_file(config_filepath): - """Parse the application's config file located at config_path.""" - config = ConfigParser() - try: - with open(config_filepath, encoding="utf-8") as config_file: - config.read_file(config_file) - except OSError as error: - print( - f"Unable to read config file at {config_filepath}: {error.strerror}", - file=sys.stderr, - ) - return ConfigParser() - except Exception as error: - print(error, file=sys.stderr) - raise error - return config - - -def get_random_string(seed): - """Generates a random string based on the given seed""" - choices = string.ascii_letters + string.digits + string.punctuation - seed = seed.encode("utf-8") - rand = random.Random(seed) - return [rand.choice(choices) for i in range(16)] diff --git a/api/setup.py b/api/setup.py index 776866f15..77665173f 100644 --- a/api/setup.py +++ b/api/setup.py @@ -43,6 +43,7 @@ setup( "dev": [ "model_bakery", "psycopg2-binary", + f"libretime-shared @ file://localhost/{here.parent / 'shared'}#egg=libretime_shared", ], }, )