diff --git a/cmapi/cmapi_server/__main__.py b/cmapi/cmapi_server/__main__.py index 8f61960c4..e92eccf91 100644 --- a/cmapi/cmapi_server/__main__.py +++ b/cmapi/cmapi_server/__main__.py @@ -29,7 +29,7 @@ from cmapi_server.failover_agent import FailoverAgent from cmapi_server.managers.application import AppManager from cmapi_server.managers.process import MCSProcessManager from cmapi_server.managers.certificate import CertificateManager -from cmapi_server.state_checks import run_state_checks +from cmapi_server.invariant_checks import run_invariant_checks from failover.node_monitor import NodeMonitor from mcs_node_control.models.dbrm_socket import SOCK_TIMEOUT, DBRMSocketHandler from mcs_node_control.models.node_config import NodeConfig @@ -153,7 +153,7 @@ if __name__ == '__main__': CertificateManager.renew_certificate() # Run checks, if some of them fail -- log and exit - run_state_checks() + run_invariant_checks() app = cherrypy.tree.mount(root=None, config=CMAPI_CONF_PATH) root_config = { diff --git a/cmapi/cmapi_server/invariant_checks.py b/cmapi/cmapi_server/invariant_checks.py new file mode 100644 index 000000000..15f36997c --- /dev/null +++ b/cmapi/cmapi_server/invariant_checks.py @@ -0,0 +1,95 @@ +import logging +import os +import sys + +from mr_kot import Runner, RunResult, Status, check, check_all, fact, parametrize +from mr_kot_fs_validators import GroupIs, HasMode, IsDir, OwnerIs + +from cmapi_server import helpers +from cmapi_server.constants import MCS_DATA_PATH +from mcs_node_control.models.node_config import NodeConfig + +logger = logging.getLogger(__name__) + + +def run_invariant_checks() -> RunResult: + """Run invariant checks, log warnings/errors/failures, and exit the process on failure/error. + """ + logger.info('Starting invariant checks') + try: + runner = Runner() + result = runner.run() + except Exception: + logger.exception('Got unexpected exception while checking invariants') + sys.exit(1) + + # Log each fail/error/warning for diagnostics + for item in result.items: + if item.status in (Status.FAIL, Status.ERROR, Status.WARN): + fn = logger.warning if item.status == Status.WARN else logger.error + fn( + 'Invariant check with id=%s produced %s: %r', + item.id, item.status, item.evidence, + ) + + logger.info('Stats: overall=%s counts=%s', result.overall, {k.value: v for k, v in result.counts.items() if v != 0}) + if result.overall in (Status.FAIL, Status.ERROR): + logger.error('Invariant checks failed, exiting') + sys.exit(1) + else: + logger.info('Invariant checks passed') + + return result + + +### Facts + +@fact +def storage_type() -> str: + """Provides storage type: shared_fs or s3.""" + return 's3' if NodeConfig().s3_enabled() else 'shared_fs' + +@fact +def is_shared_fs(storage_type: str) -> bool: + return storage_type == 'shared_fs' + +@fact +def dispatcher_name() -> str: + """Provides environment dispatcher name: systemd or container""" + cfg = helpers.get_config_parser() + name, _ = helpers.get_dispatcher_name_and_path(cfg) + return name + +@fact +def is_systemd_disp(dispatcher_name: str) -> bool: + return dispatcher_name == 'systemd' + + +### Checks + +REQUIRED_LOCAL_DIRS = [ + os.path.join(MCS_DATA_PATH, 'data1'), + os.path.join(MCS_DATA_PATH, 'data1', 'systemFiles'), + os.path.join(MCS_DATA_PATH, 'data1', 'systemFiles', 'dbrm'), +] + +@check(selector='is_shared_fs') +@parametrize('dir', values=REQUIRED_LOCAL_DIRS, fail_fast=True) +def required_dirs_perms(dir: str) -> tuple[Status, str]: + status, ev = check_all( + dir, + IsDir(), + HasMode('1755'), + ) + return (status, ev) + +@check(selector='is_shared_fs, is_systemd_disp') +@parametrize('dir', values=REQUIRED_LOCAL_DIRS, fail_fast=True) +def required_dirs_ownership(dir: str) -> tuple[Status, str]: + # Check ownership only when not in containers + status, ev = check_all( + dir, + OwnerIs('mysql'), + GroupIs('mysql'), + ) + return (status, ev) diff --git a/cmapi/cmapi_server/pyproject.toml b/cmapi/cmapi_server/pyproject.toml deleted file mode 100644 index 2d08d5825..000000000 --- a/cmapi/cmapi_server/pyproject.toml +++ /dev/null @@ -1,25 +0,0 @@ -[tool.ruff] -line-length = 80 -target-version = "py39" -# Enable common rule sets -select = [ - "E", # pycodestyle errors - "F", # pyflakes: undefined names, unused imports, etc. - "I", # isort: import sorting - "B", # flake8-bugbear: common bugs and anti-patterns - "UP", # pyupgrade: use modern Python syntax - "N", # pep8-naming: naming conventions -] - -ignore = [] - -# Exclude cache and temporary directories -exclude = [ - "__pycache__", -] - -[tool.ruff.format] -quote-style = "single" - -[tool.ruff.lint.isort] -force-single-line = false \ No newline at end of file diff --git a/cmapi/cmapi_server/state_checks.py b/cmapi/cmapi_server/state_checks.py deleted file mode 100644 index 9aaf08e46..000000000 --- a/cmapi/cmapi_server/state_checks.py +++ /dev/null @@ -1,70 +0,0 @@ -from __future__ import annotations - -import logging -import os -import sys - -from mcs_node_control.models.node_config import NodeConfig -from mr_kot import Status, check, check_all, fact, parametrize -from mr_kot.runner import Runner, RunResult -from mr_kot_fs_validators import GroupIs, HasMode, IsDir, OwnerIs - -from cmapi_server.constants import MCS_DATA_PATH - -logger = logging.getLogger(__name__) - - -def run_state_checks(*, verbose: bool = False) -> RunResult: - """Run state checks, log warnings/errors/failures, and exit the process on failure/error. - """ - logger.info("Starting invariant checks") - try: - runner = Runner(verbose=verbose, include_tags=True) - result = runner.run() - except Exception: - logger.exception('Unexpected exception during state checks') - sys.exit(1) - - # Log each fail/error/warning for diagnostics - for item in result.items: - if item.status in (Status.FAIL, Status.ERROR, Status.WARN): - logger.error( - 'State check with id=%s produced %s: %r', - item.id, item.status, item.evidence, - ) - - logger.info('Stats: overall=%s counts=%s', result.overall, {k.value: v for k, v in result.counts.items() if v != 0}) - if result.overall in (Status.FAIL, Status.ERROR): - logger.error('State check failed, exiting') - sys.exit(1) - - return result - -@fact -def storage_type() -> str: - """Provides storage type: shared_fs or s3.""" - return 's3' if NodeConfig().s3_enabled() else 'shared_fs' - -@fact -def is_shared_fs(storage_type: str) -> bool: - return storage_type == 'shared_fs' - -@check(selector='is_shared_fs') -@parametrize( - 'required_local_dir', - values=[ - os.path.join(MCS_DATA_PATH, 'data1'), - os.path.join(MCS_DATA_PATH, 'data1', 'systemFiles'), - os.path.join(MCS_DATA_PATH, 'data1', 'systemFiles', 'dbrm'), - ], - fail_fast=True, -) -def required_dirs_fs_state(required_local_dir: str) -> tuple[Status, str]: - status, ev = check_all( - required_local_dir, - IsDir(), - HasMode('1755'), - OwnerIs('mysql'), - GroupIs('mysql'), - ) - return (status, ev) diff --git a/cmapi/pyproject.toml b/cmapi/pyproject.toml index 371a0f357..df5e56d23 100644 --- a/cmapi/pyproject.toml +++ b/cmapi/pyproject.toml @@ -23,6 +23,7 @@ exclude = [ quote-style = "single" [tool.ruff.lint.isort] +known-first-party = ["cmapi_server", "failover", "mcs_node_control", "tracing"] force-single-line = false combine-as-imports = true diff --git a/cmapi/requirements.in b/cmapi/requirements.in index 3262082d0..3fa0df02b 100644 --- a/cmapi/requirements.in +++ b/cmapi/requirements.in @@ -18,6 +18,6 @@ Routes==2.5.1 typer==0.15.2 pydantic==2.11.7 sentry-sdk==2.34.1 -# State checks -mr_kot==0.8.1 +# Invariant checks +mr_kot==0.8.3 mr_kot_fs_validators==0.1.0 \ No newline at end of file diff --git a/cmapi/requirements.txt b/cmapi/requirements.txt index 6a7183bb7..1d2030f3f 100644 --- a/cmapi/requirements.txt +++ b/cmapi/requirements.txt @@ -144,7 +144,7 @@ more-itertools==10.7.0 # cherrypy # jaraco-functools # jaraco-text -mr-kot==0.8.1 +mr-kot==0.8.3 # via -r requirements.in mr-kot-fs-validators==0.1.0 # via -r requirements.in