diff --git a/CHANGELOG.md b/CHANGELOG.md index 119400f0c..a155d6a15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Configuration guides to setup production HTTP server (apache2, nginx and caddy) on SLES. +### Changed +- agent: Make RacksDB library optional with lazy loading only when enabled in + configuration (#683). Contribution from @faganihajizada. + ### Fixed - agent: Import of ClusterShell NodeSet class (#682). Contribution from @faganihajizada. diff --git a/slurmweb/apps/agent.py b/slurmweb/apps/agent.py index 03b2acd5b..0472188bb 100644 --- a/slurmweb/apps/agent.py +++ b/slurmweb/apps/agent.py @@ -9,8 +9,6 @@ import logging from rfl.web.tokens import RFLTokenizedRBACWebApp -from racksdb.errors import RacksDBSchemaError, RacksDBFormatError -from racksdb.web.app import RacksDBWebBlueprint try: from werkzeug.middleware import dispatcher @@ -65,6 +63,11 @@ def __init__(self, seed): # If enabled, load RacksDB blueprint and fail with error if unable to load # schema or database. if self.settings.racksdb.enabled: + # Lazy load RacksDB module to avoid failing on missing optional external + # dependency when feature is actually disabled. + from racksdb.errors import RacksDBSchemaError, RacksDBFormatError + from racksdb.web.app import RacksDBWebBlueprint + try: self.register_blueprint( RacksDBWebBlueprint( diff --git a/slurmweb/tests/apps/test_agent.py b/slurmweb/tests/apps/test_agent.py index 4e668c0dd..a0e45db49 100644 --- a/slurmweb/tests/apps/test_agent.py +++ b/slurmweb/tests/apps/test_agent.py @@ -5,11 +5,12 @@ # SPDX-License-Identifier: MIT import sys +import unittest from unittest import mock from slurmweb.errors import SlurmwebConfigurationError -from ..lib.agent import TestAgentBase +from ..lib.agent import TestAgentBase, is_racksdb_available class TestAgentApp(TestAgentBase): @@ -22,6 +23,7 @@ def test_app_loaded(self): with self.assertNoLogs("slurmweb", level="ERROR"): self.setup_client() + @unittest.skipIf(not is_racksdb_available(), "RacksDB not installed") def test_app_racksdb_format_error(self): with self.assertLogs("slurmweb", level="ERROR") as cm: self.setup_client(racksdb_format_error=True) @@ -33,6 +35,7 @@ def test_app_racksdb_format_error(self): ], ) + @unittest.skipIf(not is_racksdb_available(), "RacksDB not installed") def test_app_racksdb_schema_error(self): with self.assertLogs("slurmweb", level="ERROR") as cm: self.setup_client(racksdb_schema_error=True) diff --git a/slurmweb/tests/lib/agent.py b/slurmweb/tests/lib/agent.py index 0c61e21f0..37547be12 100644 --- a/slurmweb/tests/lib/agent.py +++ b/slurmweb/tests/lib/agent.py @@ -8,7 +8,7 @@ from unittest import mock import tempfile import os - +from importlib.util import find_spec import werkzeug from flask import Blueprint, jsonify @@ -18,7 +18,6 @@ from rfl.permissions.rbac import ANONYMOUS_ROLE from slurmweb.apps import SlurmwebAppSeed from slurmweb.apps.agent import SlurmwebAppAgent -from racksdb.errors import RacksDBFormatError, RacksDBSchemaError from .utils import ( mock_slurmrestd_responses, @@ -27,6 +26,11 @@ ) +def is_racksdb_available(): + """Check if RacksDB is available for testing.""" + return find_spec("racksdb") is not None + + CONF_TPL = """ [service] cluster=test @@ -147,6 +151,13 @@ def setup_client( anonymous_enabled=True, use_token=True, ): + # Check if RacksDB is available for mocking + try: + from racksdb.errors import RacksDBFormatError, RacksDBSchemaError + except ModuleNotFoundError: + # RacksDB not available, disable it in config + racksdb = False + self.setup_agent_conf( slurmrestd_parameters=slurmrestd_parameters, racksdb=racksdb, @@ -154,14 +165,27 @@ def setup_client( cache=cache, ) - # Start the app with mocked RacksDB web blueprint - with mock.patch("slurmweb.apps.agent.RacksDBWebBlueprint") as m: - if racksdb_format_error: - m.side_effect = RacksDBFormatError("fake db format error") - elif racksdb_schema_error: - m.side_effect = RacksDBSchemaError("fake db schema error") - else: - m.return_value = FakeRacksDBWebBlueprint() + if racksdb: + # RacksDB is available, start app with mocked RacksDB web blueprint + with mock.patch("racksdb.web.app.RacksDBWebBlueprint") as m: + if racksdb_format_error: + m.side_effect = RacksDBFormatError("fake db format error") + elif racksdb_schema_error: + m.side_effect = RacksDBSchemaError("fake db schema error") + else: + m.return_value = FakeRacksDBWebBlueprint() + self.app = SlurmwebAppAgent( + SlurmwebAppSeed.with_parameters( + debug=False, + log_flags=["ALL"], + log_component=None, + debug_flags=[], + conf_defs=self.conf_defs, + conf=self.conf.name, + ) + ) + else: + # RacksDB disabled in config or not available self.app = SlurmwebAppAgent( SlurmwebAppSeed.with_parameters( debug=False, @@ -172,6 +196,7 @@ def setup_client( conf=self.conf.name, ) ) + if not anonymous_enabled: self.app.policy.disable_anonymous() diff --git a/slurmweb/tests/lib/gateway.py b/slurmweb/tests/lib/gateway.py index 22756666b..28d25eb98 100644 --- a/slurmweb/tests/lib/gateway.py +++ b/slurmweb/tests/lib/gateway.py @@ -14,14 +14,16 @@ import jinja2 from rfl.authentication.user import AuthenticatedUser, AnonymousUser -from racksdb.version import get_version as racksdb_get_version + from slurmweb.version import get_version from slurmweb.apps import SlurmwebAppSeed from slurmweb.apps.gateway import SlurmwebAppGateway from slurmweb.apps.gateway import SlurmwebAgent, SlurmwebAgentRacksDBSettings +from slurmweb.views.agent import racksdb_get_version from .utils import SlurmwebCustomTestResponse + CONF_TPL = """ [agents] url=http://localhost diff --git a/slurmweb/tests/views/test_agent.py b/slurmweb/tests/views/test_agent.py index 1b65a66c7..3930a23e6 100644 --- a/slurmweb/tests/views/test_agent.py +++ b/slurmweb/tests/views/test_agent.py @@ -10,14 +10,13 @@ from ClusterShell.NodeSet import NodeSet -from racksdb.version import get_version as racksdb_get_version - from slurmweb.version import get_version from slurmweb.slurmrestd.errors import ( SlurmrestConnectionError, SlurmrestdInvalidResponseError, ) from slurmweb.cache import CachingService +from slurmweb.views.agent import racksdb_get_version from ..lib.agent import TestAgentBase from ..lib.utils import all_slurm_api_versions, flask_404_description diff --git a/slurmweb/tests/views/test_agent_racksdb.py b/slurmweb/tests/views/test_agent_racksdb.py index 9ce2fac56..eac20d80e 100644 --- a/slurmweb/tests/views/test_agent_racksdb.py +++ b/slurmweb/tests/views/test_agent_racksdb.py @@ -4,11 +4,13 @@ # # SPDX-License-Identifier: MIT +import unittest -from ..lib.agent import TestAgentBase +from ..lib.agent import TestAgentBase, is_racksdb_available from ..lib.utils import flask_404_description +@unittest.skipIf(not is_racksdb_available(), "RacksDB not installed") class TestAgentRacksDBEnabledRequest(TestAgentBase): def setUp(self): self.setup_client() @@ -23,6 +25,7 @@ def test_request_racksdb(self): ) +@unittest.skipIf(not is_racksdb_available(), "RacksDB not installed") class TestAgentRacksDBUnabledRequest(TestAgentBase): def setUp(self): self.setup_client(racksdb_format_error=True) diff --git a/slurmweb/views/agent.py b/slurmweb/views/agent.py index 4e9bd1d49..562123073 100644 --- a/slurmweb/views/agent.py +++ b/slurmweb/views/agent.py @@ -9,7 +9,6 @@ from flask import Response, current_app, jsonify, abort, request from rfl.web.tokens import rbac_action, check_jwt -from racksdb.version import get_version as racksdb_get_version from ..version import get_version from ..errors import SlurmwebCacheError, SlurmwebMetricsDBError @@ -26,6 +25,16 @@ logger = logging.getLogger(__name__) +def racksdb_get_version(): + """Get RacksDB version if available, or return 'N/A' if not installed.""" + try: + from racksdb.version import get_version + + return get_version() + except ModuleNotFoundError: + return "N/A (not installed)" + + def version(): return Response(f"Slurm-web agent v{get_version()}\n", mimetype="text/plain")