diff --git a/CHANGELOG.md b/CHANGELOG.md index a0bcbc48..100e1e69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,13 +9,21 @@ and this project adheres to ## [unreleased] ### Added -- docs: Mention support of RHEL 10, Fedora 43, SLES and openSUSE 15 and 16. +- cli: Environment variables `RACKSDB_DB`, `RACKSDB_SCHEMA`, + `RACKSDB_EXTENSIONS`, and to set default schema, extensions, and database + paths when the matching `racksdb` / `racksdb-web` option is omitted (#149). +- web: Support the same environment variables as CLI. +- docs: + - Mention support of RHEL 10, Fedora 43, SLES and openSUSE 15 and 16. + - Mention support of `RACKSDB_DB`, `RACKSDB_SCHEMA` and `RACKSDB_EXTENSIONS` + environment variables in `racksdb` and `racksdb-web` manpages. + +### Fixed +- docs: Add missing system dependency `libpango1.0-dev` to install from sources, + reported by @astappiev (#148). ### Removed -- docs: - - Drop support of Fedora 41. - - Add missing system dependency `libpango1.0-dev` to install from sources, - reported by @astappiev (#148). +- docs: Drop support of Fedora 41. ## [0.6.0] - 2025-10-16 diff --git a/docs/modules/usage/pages/racksdb-web.adoc b/docs/modules/usage/pages/racksdb-web.adoc index 3b57bce2..6a5329c7 100644 --- a/docs/modules/usage/pages/racksdb-web.adoc +++ b/docs/modules/usage/pages/racksdb-web.adoc @@ -27,16 +27,19 @@ endif::[] Path to database. Both files and directories paths are accepted. If the path is a directory, all YAML files in this directory are loaded, recursively. If the path does not exist, an error is reported. Default value is - [.path]#`/var/lib/racksdb/`# directory. + [.path]#`/var/lib/racksdb/`# directory or environment variable + [.cli-opt]#*RACKSDB_DB*# if set. [.cli-opt]#*-s, --schema*=#[.cli-optval]##_SCHEMA_##:: Path to RacksDB schema YAML file. If the file does not exist, an error is - reported. Default value is [.path]#`/usr/share/racksdb/schemas/racksdb.yml`#. + reported. Default value is [.path]#`/usr/share/racksdb/schemas/racksdb.yml`# + or environment variable [.cli-opt]#*RACKSDB_SCHEMA*# if set. [.cli-opt]#*-e, --ext*=#[.cli-optval]##_EXT_##:: Path to optional RacksDB schema extensions. If the file does not exist, it is silently ignored by RacksDB. Default value is - [.path]#`/etc/racksdb/extensions.yml`#. + [.path]#`/etc/racksdb/extensions.yml`# or environment variable + [.cli-opt]#*RACKSDB_EXTENSIONS*# if set. [.cli-opt]#*--host*=#[.cli-optval]##_HOST_##:: The hostname to listen for incoming requests. Set to `0.0.0.0` to listen on @@ -61,6 +64,22 @@ endif::[] without value, default UI path [.path]#`/usr/share/racksdb/frontend`# is used. When a path is provided, `racksdb-web` loads UI files from this path. +== Environment variables + +When a command-line option above is omitted, `racksdb-web` uses the following +environment variables if they are set to a non-empty value: + +[.cli-opt]#*RACKSDB_SCHEMA*#:: + Path to the RacksDB schema YAML file. + +[.cli-opt]#*RACKSDB_EXTENSIONS*#:: + Path to optional schema extensions. + +[.cli-opt]#*RACKSDB_DB*#:: + Path to the database. + +Command-line options always override environment variables. + == Exit status *0*:: diff --git a/docs/modules/usage/pages/racksdb.adoc b/docs/modules/usage/pages/racksdb.adoc index 3f2b3de5..78c81370 100644 --- a/docs/modules/usage/pages/racksdb.adoc +++ b/docs/modules/usage/pages/racksdb.adoc @@ -28,16 +28,19 @@ endif::[] Path to database. Both files and directories paths are accepted. If the path is a directory, all YAML files in this directory are loaded, recursively. If the path does not exist, an error is reported. Default value is - [.path]#`/var/lib/racksdb/`# directory. + [.path]#`/var/lib/racksdb/`# directory or environment variable + [.cli-opt]#*RACKSDB_DB*# if set. [.cli-opt]#*-s, --schema*=#[.cli-optval]##_SCHEMA_##:: Path to RacksDB schema YAML file. If the file does not exist, an error is - reported. Default value is [.path]#`/usr/share/racksdb/schemas/racksdb.yml`#. + reported. Default value is [.path]#`/usr/share/racksdb/schemas/racksdb.yml`# + or environment variable [.cli-opt]#*RACKSDB_SCHEMA*# if set. [.cli-opt]#*-e, --ext*=#[.cli-optval]##_EXT_##:: Path to optional RacksDB schema extensions. If the file does not exist, it is silently ignored by RacksDB. Default value is - [.path]#`/etc/racksdb/extensions.yml`#. + [.path]#`/etc/racksdb/extensions.yml`# or environment variable + [.cli-opt]#*RACKSDB_EXTENSIONS*# if set. == Commands @@ -373,6 +376,22 @@ Generate graphical representation of _noisy_ datacenter room and dump coordinates of racks in JSON format in a file named `noisy-coordinates.json`. ==== +== Environment variables + +When a command-line option above is omitted, `racksdb` uses the following +environment variables if they are set to a non-empty value: + +[.cli-opt]#*RACKSDB_SCHEMA*#:: + Path to the RacksDB schema YAML file. + +[.cli-opt]#*RACKSDB_EXTENSIONS*#:: + Path to optional schema extensions. + +[.cli-opt]#*RACKSDB_DB*#:: + Path to the database. + +Command-line options always override environment variables. + == Exit status *0*:: diff --git a/racksdb/env.py b/racksdb/env.py new file mode 100644 index 00000000..d79f0d6e --- /dev/null +++ b/racksdb/env.py @@ -0,0 +1,25 @@ +# Copyright (c) 2025 Rackslab +# +# This file is part of RacksDB. +# +# SPDX-License-Identifier: MIT + +"""Environment variable names and helpers for RacksDB CLI entry points.""" + +import os +from pathlib import Path +import typing as t + + +class RacksDBEnv: + SCHEMA = "RACKSDB_SCHEMA" + EXTENSIONS = "RACKSDB_EXTENSIONS" + DB = "RACKSDB_DB" + + +def env_or_default(env_key: str, fallback: t.Union[str, Path]) -> Path: + """Path from ``os.environ[env_key]`` if set and non-empty, else *fallback*.""" + val = os.environ.get(env_key) + if val: + return Path(val) + return Path(fallback) diff --git a/racksdb/exec.py b/racksdb/exec.py index 61959fbc..15bbc01b 100644 --- a/racksdb/exec.py +++ b/racksdb/exec.py @@ -22,6 +22,7 @@ ) from .generic.dumpers import DBDumperFactory, SchemaDumperFactory from . import RacksDB +from .env import RacksDBEnv, env_or_default from .drawers import InfrastructureDrawer, AxonometricInfrastructureDrawer, RoomDrawer from .drawers.parameters import DrawingParameters from .errors import RacksDBError @@ -57,21 +58,21 @@ def __init__(self, cmd_args: t.Optional[t.List[str]] = None): "-s", "--schema", help="Schema to load (default: %(default)s)", - default=RacksDB.DEFAULT_SCHEMA, + default=env_or_default(RacksDBEnv.SCHEMA, RacksDB.DEFAULT_SCHEMA), type=Path, ) parser.add_argument( "-e", "--ext", help="Path to extensions of schema (default: %(default)s)", - default=RacksDB.DEFAULT_EXT, + default=env_or_default(RacksDBEnv.EXTENSIONS, RacksDB.DEFAULT_EXT), type=Path, ) parser.add_argument( "-b", "--db", help="Database to load (default: %(default)s)", - default=RacksDB.DEFAULT_DB, + default=env_or_default(RacksDBEnv.DB, RacksDB.DEFAULT_DB), type=Path, ) diff --git a/racksdb/tests/test_env.py b/racksdb/tests/test_env.py new file mode 100644 index 00000000..0944915c --- /dev/null +++ b/racksdb/tests/test_env.py @@ -0,0 +1,39 @@ +# Copyright (c) 2025 Rackslab +# +# This file is part of RacksDB. +# +# SPDX-License-Identifier: MIT + +import os +import unittest +from pathlib import Path +from unittest import mock + +from racksdb.env import env_or_default + + +class TestEnvOrDefault(unittest.TestCase): + def test_uses_fallback_when_key_missing(self): + key = "RACKSDB_TEST_ENV_OR_DEFAULT_MISSING" + self.assertNotIn(key, os.environ) + p = env_or_default(key, "/tmp/fallback") + self.assertEqual(p, Path("/tmp/fallback")) + + def test_uses_fallback_when_empty_string(self): + key = "RACKSDB_TEST_ENV_OR_DEFAULT_EMPTY" + with mock.patch.dict(os.environ, {key: ""}, clear=False): + p = env_or_default(key, "/tmp/fallback") + self.assertEqual(p, Path("/tmp/fallback")) + + def test_uses_environment_when_set(self): + key = "RACKSDB_TEST_ENV_OR_DEFAULT_SET" + with mock.patch.dict(os.environ, {key: "/from/env"}, clear=False): + p = env_or_default(key, "/tmp/fallback") + self.assertEqual(p, Path("/from/env")) + + def test_fallback_accepts_path(self): + key = "RACKSDB_TEST_ENV_OR_DEFAULT_PATH_FALLBACK" + fb = Path("/tmp/fallback_path") + with mock.patch.dict(os.environ, {key: ""}, clear=False): + p = env_or_default(key, fb) + self.assertEqual(p, fb) diff --git a/racksdb/tests/test_exec.py b/racksdb/tests/test_exec.py index 68175b7e..1f51028c 100644 --- a/racksdb/tests/test_exec.py +++ b/racksdb/tests/test_exec.py @@ -4,6 +4,7 @@ # # SPDX-License-Identifier: MIT +import unittest from unittest import mock import io import tempfile @@ -15,6 +16,7 @@ import yaml from racksdb import RacksDB +from racksdb.env import RacksDBEnv from racksdb.exec import RacksDBExec from racksdb.version import get_version @@ -695,3 +697,78 @@ def test_autopager_used_in_views(self): RacksDBExec(CMD_BASE_ARGS + ["datacenters"]) autopager.assert_called_once() autopager.return_value.__enter__.assert_called_once() + + +class TestRacksDBExecLoadArguments(unittest.TestCase): + """``RacksDB.load()`` receives paths from argparse defaults (env + fallbacks).""" + + @staticmethod + def _mock_db_for_dump(): + mdb = mock.MagicMock() + mdb._loader.content = {} + return mdb + + def test_load_args_use_class_defaults_when_env_keys_empty(self): + with mock.patch.dict( + os.environ, + { + RacksDBEnv.SCHEMA: "", + RacksDBEnv.EXTENSIONS: "", + RacksDBEnv.DB: "", + }, + clear=False, + ): + with mock.patch("racksdb.exec.RacksDB.load") as load_mock: + load_mock.return_value = self._mock_db_for_dump() + with mock.patch("sys.stdout", new=io.StringIO()): + RacksDBExec(["dump"]) + load_mock.assert_called_once_with( + Path(RacksDB.DEFAULT_SCHEMA), + Path(RacksDB.DEFAULT_EXT), + Path(RacksDB.DEFAULT_DB), + ) + + def test_load_args_use_environment_when_set(self): + env = { + RacksDBEnv.SCHEMA: "/env/schema.yml", + RacksDBEnv.EXTENSIONS: "/env/extensions.yml", + RacksDBEnv.DB: "/env/db", + } + with mock.patch.dict(os.environ, env, clear=False): + with mock.patch("racksdb.exec.RacksDB.load") as load_mock: + load_mock.return_value = self._mock_db_for_dump() + with mock.patch("sys.stdout", new=io.StringIO()): + RacksDBExec(["dump"]) + load_mock.assert_called_once_with( + Path("/env/schema.yml"), + Path("/env/extensions.yml"), + Path("/env/db"), + ) + + def test_load_args_prefer_cli_over_environment(self): + with mock.patch.dict( + os.environ, + { + RacksDBEnv.SCHEMA: "/wrong/schema.yml", + RacksDBEnv.EXTENSIONS: "", + RacksDBEnv.DB: "/wrong/db", + }, + clear=False, + ): + with mock.patch("racksdb.exec.RacksDB.load") as load_mock: + load_mock.return_value = self._mock_db_for_dump() + with mock.patch("sys.stdout", new=io.StringIO()): + RacksDBExec( + [ + "--schema", + "/cli/schema.yml", + "--db", + "/cli/db", + "dump", + ] + ) + load_mock.assert_called_once_with( + Path("/cli/schema.yml"), + Path(RacksDB.DEFAULT_EXT), + Path("/cli/db"), + ) diff --git a/racksdb/tests/web/test_app.py b/racksdb/tests/web/test_app.py index 2ca2e322..71720e85 100644 --- a/racksdb/tests/web/test_app.py +++ b/racksdb/tests/web/test_app.py @@ -4,12 +4,16 @@ # # SPDX-License-Identifier: MIT +import os import unittest from unittest import mock import io +from pathlib import Path import werkzeug +from racksdb import RacksDB +from racksdb.env import RacksDBEnv from racksdb.web.app import RacksDBWebApp, merge_args_parameters from ..lib.web import RacksDBCustomTestResponse @@ -153,3 +157,72 @@ def test_merge_list(self): args = {"parameters.key1": "value1,value2"} merge_args_parameters(content, args) self.assertEqual(content, {"key1": ["value1", "value2"]}) + + +class TestRacksDBWebAppLoadArguments(unittest.TestCase): + """``RacksDB.load()`` arguments from ``RacksDBWebApp`` argparse defaults.""" + + @staticmethod + def _mock_db(): + return mock.MagicMock() + + def test_load_args_use_class_defaults_when_env_keys_empty(self): + with mock.patch.dict( + os.environ, + { + RacksDBEnv.SCHEMA: "", + RacksDBEnv.EXTENSIONS: "", + RacksDBEnv.DB: "", + }, + clear=False, + ): + with mock.patch("racksdb.web.app.RacksDB.load") as load_mock: + load_mock.return_value = self._mock_db() + RacksDBWebApp([]) + load_mock.assert_called_once_with( + schema=Path(RacksDB.DEFAULT_SCHEMA), + ext=Path(RacksDB.DEFAULT_EXT), + db=Path(RacksDB.DEFAULT_DB), + ) + + def test_load_args_use_environment_when_set(self): + env = { + RacksDBEnv.SCHEMA: "/env/schema.yml", + RacksDBEnv.EXTENSIONS: "/env/extensions.yml", + RacksDBEnv.DB: "/env/db", + } + with mock.patch.dict(os.environ, env, clear=False): + with mock.patch("racksdb.web.app.RacksDB.load") as load_mock: + load_mock.return_value = self._mock_db() + RacksDBWebApp([]) + load_mock.assert_called_once_with( + schema=Path("/env/schema.yml"), + ext=Path("/env/extensions.yml"), + db=Path("/env/db"), + ) + + def test_load_args_prefer_cli_over_environment(self): + with mock.patch.dict( + os.environ, + { + RacksDBEnv.SCHEMA: "/wrong/schema.yml", + RacksDBEnv.EXTENSIONS: "", + RacksDBEnv.DB: "/wrong/db", + }, + clear=False, + ): + with mock.patch("racksdb.web.app.RacksDB.load") as load_mock: + load_mock.return_value = self._mock_db() + RacksDBWebApp( + [ + "--schema", + "/cli/schema.yml", + "--db", + "/cli/db", + ] + ) + load_mock.assert_called_once_with( + schema=Path("/cli/schema.yml"), + ext=Path(RacksDB.DEFAULT_EXT), + db=Path("/cli/db"), + ) diff --git a/racksdb/web/app.py b/racksdb/web/app.py index 9681fd6c..121ec4a3 100644 --- a/racksdb/web/app.py +++ b/racksdb/web/app.py @@ -17,6 +17,7 @@ from rfl.log import setup_logger from .. import RacksDB +from ..env import RacksDBEnv, env_or_default from ..errors import RacksDBError, RacksDBRequestError, RacksDBNotFoundError from ..version import get_version from ..views import RacksDBViews @@ -324,21 +325,21 @@ def __init__(self, cmd_args: t.Optional[t.List[str]] = None): "-s", "--schema", help="Schema to load (default: %(default)s)", - default=RacksDB.DEFAULT_SCHEMA, + default=env_or_default(RacksDBEnv.SCHEMA, RacksDB.DEFAULT_SCHEMA), type=Path, ) parser.add_argument( "-e", "--ext", help="Path to extensions of schema (default: %(default)s)", - default=RacksDB.DEFAULT_EXT, + default=env_or_default(RacksDBEnv.EXTENSIONS, RacksDB.DEFAULT_EXT), type=Path, ) parser.add_argument( "-b", "--db", help="Database to load (default: %(default)s)", - default=RacksDB.DEFAULT_DB, + default=env_or_default(RacksDBEnv.DB, RacksDB.DEFAULT_DB), type=Path, ) parser.add_argument(