Skip to content

Commit

Permalink
WIP: implement _SnapFsConfigManager using slurmutils
Browse files Browse the repository at this point in the history
  • Loading branch information
jedel1043 committed Sep 4, 2024
1 parent 57e507e commit 1312b9f
Show file tree
Hide file tree
Showing 5 changed files with 249 additions and 109 deletions.
8 changes: 8 additions & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
# lib deps
slurmutils ~= 0.6.0
python-dotenv ~= 1.0.1
pyyaml >= 6.0.2

# tests deps
coverage[toml] ~= 7.6
pyfakefs ~= 5.6.0
pytest ~= 7.2
pytest-order ~= 1.1
86 changes: 68 additions & 18 deletions lib/charms/hpc_libs/v0/slurm_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,24 @@ def _on_install(self, _) -> None:
"SnapManager",
]

import json
import logging
import os
import re
import socket
import subprocess
from abc import ABC, abstractmethod
from collections.abc import Mapping
from contextlib import AbstractContextManager
from enum import Enum
from functools import partial
from pathlib import Path
from typing import Any, Optional, Type
from typing import Any, Callable, Optional, Type, TypeAlias

import dotenv
import yaml
from slurmutils.editors import slurmconfig, slurmdbdconfig
from slurmutils.exceptions import ModelError
from slurmutils.models.model import BaseModel as SlurmBaseConfig

# The unique Charmhub library identifier, never change it
LIBID = "541fd767f90b40539cf7cd6e7db8fabf"
Expand All @@ -92,7 +97,7 @@ def _on_install(self, _) -> None:
LIBPATCH = 6

# Charm library dependencies to fetch during `charmcraft pack`.
PYDEPS = ["pyyaml>=6.0.2", "python-dotenv~=1.0.1"]
PYDEPS = ["pyyaml>=6.0.2", "python-dotenv~=1.0.1", "slurmutils~=0.6.0"]

_logger = logging.getLogger(__name__)
_acronym = re.compile(r"(?<=[A-Z])(?=[A-Z][a-z])")
Expand Down Expand Up @@ -381,7 +386,7 @@ def get(self, key: Optional[str] = None) -> Any:

def set(self, config: Mapping[str, Any]) -> None:
"""Set configuration for Slurm component."""
# Sanity check to avoid modifying the .env if the input keys are invalid.
# Check to avoid modifying the .env if the input keys are invalid.
for key in config.keys():
if key not in self._keys:
raise SlurmOpsError(
Expand All @@ -393,7 +398,7 @@ def set(self, config: Mapping[str, Any]) -> None:

def unset(self, *keys: str) -> None:
"""Unset configuration for Slurm component."""
# Sanity check to avoid modifying the .env if the input keys are invalid.
# Check to avoid modifying the .env if the input keys are invalid.
for key in keys:
if key not in self._keys:
raise SlurmOpsError(
Expand All @@ -407,27 +412,64 @@ def unset(self, *keys: str) -> None:
dotenv.unset_key(self._DOTENV_FILE, self.config_to_env_var(key))


class _SnapConfigManager(ConfigManager):
"""Control configuration of a Slurm component using Snap."""
_EditorFetcher: TypeAlias = Callable[[], AbstractContextManager[SlurmBaseConfig]]
_ConfigFetcher: TypeAlias = Callable[[], SlurmBaseConfig]
_ConfigDeleter: TypeAlias = Callable[[], list]

def __init__(self, service: ServiceType) -> None:
self._name = service.config_name

# TODO: probably move this abstraction (or something similar that enables the same) to slurmutils
class _SnapFsConfigManager(ConfigManager, ABC):
"""Control configuration of a Slurm component using a file."""

def __init__(
self, editor: _EditorFetcher, config: _ConfigFetcher, deleter=_ConfigDeleter
) -> None:
self._editor_fetcher = editor
self._config_fetcher = config
self._config_deleter = deleter

def get(self, key: Optional[str] = None) -> Any:
"""Get specific configuration value for Slurm component."""
key = f"{self._name}.{key}" if key else self._name
config = json.loads(_snap("get", "-d", "slurm", key))
return config[key]
config = self._config_fetcher().dict()

try:
for prop in key.split("."):
if isinstance(config, dict):
config = config[prop]
elif isinstance(config, list):
config = config[int(prop)]
else:
raise SlurmOpsError(f"invalid configuration key `{key}`")
return config
except ModelError as e:
raise SlurmOpsError(e.message)
except KeyError:
raise SlurmOpsError(f"invalid configuration key `{key}`")

def set(self, config: Mapping[str, Any]) -> None:
"""Set configuration for Slurm component."""
args = [f"{self._name}.{k}={json.dumps(v)}" for k, v in config.items()]
_snap("set", "slurm", *args)
with self._config_fetcher() as current_config:
new_config = current_config.from_dict(config)
current_config.update(new_config)

def unset(self, *keys: str) -> None:
"""Unset configuration for Slurm component."""
args = [f"{self._name}.{k}" for k in keys] if len(keys) > 0 else [self._name]
_snap("unset", "slurm", *args)
if len(keys) == 0:
self._config_deleter()
return

with self._config_fetcher() as current_config:
for key in keys:
rest, last = key.rsplit(".", 1)
base = current_config
try:
for prop in rest.split("."):
base = getattr(base, prop)
del base[last]
except ModelError as e:
raise SlurmOpsError(e.message)
except KeyError:
raise SlurmOpsError(f"invalid configuration key `{key}`")


class _SnapMungeKeyManager(MungeKeyManager):
Expand Down Expand Up @@ -470,8 +512,16 @@ class SnapManager(SlurmOpsManager):
"""Slurm ops manager that uses Snap as its package manager."""

_CONFIG_SERVICES = {
ServiceType.SLURMCTLD: _SnapConfigManager(ServiceType.SLURMCTLD),
ServiceType.SLURMDBD: _SnapConfigManager(ServiceType.SLURMDBD),
ServiceType.SLURMCTLD: _SnapFsConfigManager(
editor=partial(slurmconfig.edit, "/var/snap/slurm/common/etc/slurm/slurm.conf"),
config=partial(slurmconfig.load, "/var/snap/slurm/common/etc/slurm/slurm.conf"),
deleter=partial(os.remove, "/var/snap/slurm/common/etc/slurm/slurm.conf"),
),
ServiceType.SLURMDBD: _SnapFsConfigManager(
editor=partial(slurmdbdconfig.edit, "/var/snap/slurm/common/etc/slurm/slurmdbd.conf"),
config=partial(slurmdbdconfig.load, "/var/snap/slurm/common/etc/slurm/slurmdbd.conf"),
deleter=partial(os.remove, "/var/snap/slurm/common/etc/slurm/slurmdbd.conf"),
),
ServiceType.MUNGED: _SnapEnvConfigManager(ServiceType.MUNGED.value, ["max-thread-count"]),
ServiceType.SLURMD: _SnapEnvConfigManager(ServiceType.SLURMD.value, ["config-server"]),
ServiceType.PROMETHEUS_EXPORTER: _SnapEnvConfigManager(
Expand Down
3 changes: 0 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
ops ~= 2.5
slurmutils ~= 0.4.0
python-dotenv ~= 1.0.1
pyyaml >= 6.0.2
Loading

0 comments on commit 1312b9f

Please sign in to comment.