Skip to content

Commit d7fce48

Browse files
authored
Merge pull request #197 from NYU-RTS/slurmrest-updates
slurmrest updates
2 parents ff65857 + 3fda6ad commit d7fce48

6 files changed

Lines changed: 202 additions & 53 deletions

File tree

coldfront/config/logging.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import socket
2+
13
from django.contrib.messages import constants as messages
4+
25
from coldfront.core.utils.common import import_from_settings
3-
import socket
46

57
# ------------------------------------------------------------------------------
68
# ColdFront logging config
@@ -24,22 +26,22 @@
2426
LOGGING = {
2527
"version": 1,
2628
"disable_existing_loggers": False,
27-
"root": {"level": "INFO", "handlers": ["console", "file"]},
29+
"root": {"level": "DEBUG", "handlers": ["console", "file"]},
2830
"formatters": {
2931
"standard": {
30-
"format": "{levelname} {asctime} {module} {thread:d} {message}",
32+
"format": "{levelname} {asctime} {module} {lineno:d} {message}",
3133
"style": "{",
3234
},
3335
},
3436
"handlers": {
3537
"console": {
3638
"class": "logging.StreamHandler",
37-
"level": "INFO",
39+
"level": "DEBUG",
3840
"formatter": "standard",
3941
},
4042
"file": {
4143
"class": "logging.handlers.RotatingFileHandler",
42-
"level": "INFO",
44+
"level": "DEBUG",
4345
"formatter": "standard",
4446
"filename": LOG_FILE,
4547
"maxBytes": 1024 * 1024,

coldfront/plugins/slurmrest/management/commands/slurmrest_check.py

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,14 @@
66
import sys
77

88
from django.core.management.base import BaseCommand
9+
from slurm_rest_api_client.models.v0043_account import V0043Account
10+
from slurm_rest_api_client.types import Unset
911

10-
from coldfront.core.resource.models import Resource, ResourceAttribute
1112
from coldfront.core.allocation.models import Allocation
13+
from coldfront.core.resource.models import Resource, ResourceAttribute
1214
from coldfront.core.utils.common import import_from_settings
13-
1415
from coldfront.plugins.slurmrest.utils import SlurmCluster
1516

16-
from slurm_rest_api_client.models.v0043_account import V0043Account
17-
from slurm_rest_api_client.types import Unset
18-
1917
SLURMREST_CLUSTER_ATTRIBUTE_NAME = import_from_settings("SLURMREST_CLUSTER_ATTRIBUTE_NAME", [])
2018
SLURM_IGNORE_USERS = import_from_settings("SLURM_IGNORE_USERS", [])
2119
SLURM_IGNORE_ACCOUNTS = import_from_settings("SLURM_IGNORE_ACCOUNTS", [])
@@ -102,7 +100,7 @@ def check_consistency(self, slurm_cluster, coldfront_resource):
102100

103101
for account in cluster_accounts:
104102
if account.name == "root" or self._skip_account(account.name):
105-
logger.debug("Ignoring account %s", account["name"])
103+
logger.debug(f"Ignoring account: {account.name}")
106104
continue
107105

108106
if isinstance(account.associations, Unset):
@@ -111,52 +109,64 @@ def check_consistency(self, slurm_cluster, coldfront_resource):
111109
continue
112110

113111
if account.name in allocation_dict:
114-
logger.debug("Slurm account %s found in ColdFront", account.name)
112+
logger.debug(f"SLURM account: {account.name} found in ColdFront")
115113
allocation_users = allocation_dict[account.name].allocationuser_set.filter(status__name="Active")
116114

117115
for association in account.associations:
118116
# Only SLURM devs know whey some associations are two way (account, cluster)
119117
# when most others are 4-way (user, account, cluster, partition)
120-
if not association["user"]:
118+
if not association.user:
121119
continue
122120

123-
username = association["user"]
121+
username = association.user
124122
if username == "root" or self._skip_user(username, account.name):
125123
logger.debug("Ignoring user %s in account %s", username, account.name)
126124
continue
127125
if username in [au.user.username for au in allocation_users]:
128126
logger.debug(
129-
"Slurm user %s in account %s found in ColdFront",
130-
username,
131-
account.name,
127+
f"SLURM user: {username} in account: {account.name} found in ColdFront",
132128
)
133129
else:
134130
logger.warning(
135-
"Slurm user %s has no association with account %s in ColdFront, removing association",
136-
account.name,
137-
username,
131+
f"SLURM user: {username} has no association with account: {account.name} in ColdFront, removing association",
138132
)
133+
try:
134+
slurm_cluster.delete_association_user_account(username, account.name, self.noop)
135+
except RuntimeError:
136+
logging.warning(
137+
f"Could not delete association for user: {username} and account: {account.name}"
138+
)
139+
continue
140+
139141
else:
142+
# Allocation has been removed in Coldfront!
140143
for association in account.associations:
141144
# Only SLURM devs know whey some associations are two way (account, cluster)
142145
# when most others are 4-way (user, account, cluster, partition)
143-
if not association["user"]:
146+
if not association.user:
144147
continue
145148

146-
username = association["user"]
149+
username = association.user
147150
if username == "root" or self._skip_user(username, account.name):
148-
logger.debug("Ignoring user %s in account %s", username, account.name)
151+
logger.debug(f"Ignoring user: {username} in account: {account.name}")
149152
continue
150153

151-
logger.warning(
152-
"Slurm account %s with user %s not found in ColdFront. Removing association.",
153-
account.name,
154-
username,
154+
logger.info(
155+
f"Deleted SLURM account: {account.name} has user: {username}. Removing association.",
155156
)
156157

158+
try:
159+
slurm_cluster.delete_association_user_account(username, account.name, self.noop)
160+
except RuntimeError:
161+
logging.warning(
162+
f"Could not delete association for user: {username} and account: {account.name}"
163+
)
164+
continue
165+
# Once the SLURM account has no users associated, remove the slurm account as well
166+
157167
def handle(self, *args, **options):
158168

159-
verbosity = int(options["verbosity"])
169+
verbosity: int = options["verbosity"]
160170
root_logger = logging.getLogger("")
161171
if verbosity == 0:
162172
root_logger.setLevel(logging.ERROR)
@@ -171,7 +181,7 @@ def handle(self, *args, **options):
171181
self.sync = True
172182
logger.warning("Syncing Slurm with ColdFront")
173183

174-
self.noop = SLURM_NOOP
184+
self.noop: bool = SLURM_NOOP
175185
if options["noop"]:
176186
self.noop = True
177187
logger.warning("NOOP enabled")
@@ -188,7 +198,6 @@ def handle(self, *args, **options):
188198
self.filter_account = options["account"]
189199

190200
logger.info(f"Checking Slurm cluster: {cluster_name}")
191-
slurm_cluster = SlurmCluster(endpoint=options["endpoint"], token=options["token"])
192201

193202
if cluster_name in SLURM_IGNORE_CLUSTERS:
194203
logger.warning("Ignoring cluster %s. Nothing to do.", cluster_name)
@@ -206,4 +215,5 @@ def handle(self, *args, **options):
206215
SLURMREST_CLUSTER_ATTRIBUTE_NAME,
207216
)
208217

209-
self.check_consistency(slurm_cluster, coldfront_resource)
218+
with SlurmCluster(endpoint=options["endpoint"], token=options["token"]) as slurm_cluster:
219+
self.check_consistency(slurm_cluster, coldfront_resource)

coldfront/plugins/slurmrest/tests/integration.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33

44
# test to confirm that the command "slurm_check" appears in the CLI
55
def test_slurm_check_command():
6-
from coldfront.plugins.slurmrest.management.commands.slurm_check import Command
6+
from coldfront.plugins.slurmrest.management.commands.slurmrest_check import Command
77

8-
assert hasattr(Command, "handle"), "Command 'slurm_check' should have a 'handle' method"
8+
assert hasattr(Command, "handle"), "Command 'slurmrest_check' should have a 'handle' method"

coldfront/plugins/slurmrest/utils.py

Lines changed: 154 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,68 @@
22
#
33
# SPDX-License-Identifier: AGPL-3.0-or-later
44

5-
import sys
65
import logging
6+
import sys
77

8+
from slurm_rest_api_client import Client
9+
from slurm_rest_api_client.api.slurm import slurm_v0043_delete_jobs
10+
from slurm_rest_api_client.api.slurmdb import (
11+
slurmdb_v0043_delete_account,
12+
slurmdb_v0043_delete_association,
13+
slurmdb_v0043_get_accounts,
14+
slurmdb_v0043_get_associations,
15+
slurmdb_v0043_post_associations,
16+
)
17+
from slurm_rest_api_client.models.v0043_account import V0043Account
18+
from slurm_rest_api_client.models.v0043_assoc import V0043Assoc
19+
from slurm_rest_api_client.models.v0043_assoc_max import V0043AssocMax
20+
from slurm_rest_api_client.models.v0043_assoc_max_jobs import V0043AssocMaxJobs
21+
from slurm_rest_api_client.models.v0043_kill_jobs_msg import V0043KillJobsMsg
22+
from slurm_rest_api_client.models.v0043_kill_jobs_resp_job import V0043KillJobsRespJob
23+
from slurm_rest_api_client.models.v0043_openapi_assocs_resp import V0043OpenapiAssocsResp
24+
from slurm_rest_api_client.models.v0043_uint_32_no_val_struct import V0043Uint32NoValStruct
825
from tenacity import (
26+
before_sleep_log,
927
retry,
10-
wait_exponential,
11-
stop_after_attempt,
1228
retry_if_not_exception_type,
13-
before_sleep_log,
29+
stop_after_attempt,
30+
wait_exponential,
1431
)
15-
from slurm_rest_api_client import Client
16-
from slurm_rest_api_client.models.v0043_account import V0043Account
17-
from slurm_rest_api_client.api.slurmdb import slurmdb_v0043_get_accounts
18-
1932

2033
logger = logging.getLogger(__name__)
2134

2235

2336
def log_request(request):
24-
logger.info(f"Request event hook: {request.method} {request.url} - Waiting for response")
37+
logger.debug(f"Request event hook: {request.method} {request.url} - Waiting for response")
2538

2639

2740
def log_response(response):
2841
request = response.request
29-
logger.info(f"Response event hook: {request.method} {request.url} - Status {response.status_code}")
42+
logger.debug(f"Response event hook: {request.method} {request.url} - Status {response.status_code}")
3043

3144

3245
class SlurmCluster:
3346
def __init__(self, endpoint, token):
47+
self.endpoint = endpoint
48+
self.token = token
49+
# for most operations, re-use the root client
3450
self.root_client: Client = Client(
35-
base_url=endpoint,
51+
base_url=self.endpoint,
3652
headers={
3753
"X-SLURM-USER-NAME": "root",
38-
"X-SLURM-USER-TOKEN": token,
54+
"X-SLURM-USER-TOKEN": self.token,
3955
},
4056
httpx_args={"event_hooks": {"request": [log_request], "response": [log_response]}},
4157
)
4258

59+
# use as context manager resource to clean up client
60+
def __enter__(self):
61+
return self
62+
63+
def __exit__(self, exc_type, exc_value, traceback):
64+
self.root_client.get_httpx_client().close()
65+
return False
66+
4367
@retry(
4468
wait=wait_exponential(multiplier=2, min=2, max=10),
4569
stop=stop_after_attempt(10),
@@ -48,9 +72,122 @@ def __init__(self, endpoint, token):
4872
retry_error_callback=lambda _: sys.exit(1), # exit if SLURM cannot be reached :(
4973
)
5074
def get_accounts(self) -> list[V0043Account]:
51-
with self.root_client as client:
52-
resp = slurmdb_v0043_get_accounts.sync(client=client, with_associations=str("true"))
53-
if resp:
54-
return resp.accounts
75+
resp = slurmdb_v0043_get_accounts.sync(client=self.root_client, with_associations=str("true"))
76+
if resp:
77+
return resp.accounts
78+
else:
79+
raise ConnectionError("Could not get list of accounts from SLURM endpoint")
80+
81+
# retry the whole block atomically
82+
@retry(
83+
wait=wait_exponential(multiplier=2, min=2, max=10),
84+
stop=stop_after_attempt(3),
85+
retry=retry_if_not_exception_type(ConnectionError),
86+
before_sleep=before_sleep_log(logging.getLogger(__name__), logging.WARNING),
87+
)
88+
def delete_association_user_account(self, username: str, account: str, noop: bool = False) -> None:
89+
if noop:
90+
logging.info(f"noop enabled: skip deleting association between user: {username} and acconut: {account}")
91+
return
92+
93+
# for some operations, instantiate a user client when possible
94+
# note that this needs the X-SLURM-USER-NAME header upon instantiation
95+
this_user_client: Client = Client(
96+
base_url=self.endpoint,
97+
headers={
98+
"X-SLURM-USER-NAME": f"{username}",
99+
"X-SLURM-USER-TOKEN": self.token,
100+
},
101+
httpx_args={"event_hooks": {"request": [log_request], "response": [log_response]}},
102+
)
103+
104+
with this_user_client as client:
105+
# start by deleting active jobs for this user
106+
body_job_kill: V0043KillJobsMsg = V0043KillJobsMsg(user_name=username, account=account)
107+
jobs_delete_resp = slurm_v0043_delete_jobs.sync(client=client, body=body_job_kill)
108+
if jobs_delete_resp:
109+
deletion_statuses: list[V0043KillJobsRespJob] = jobs_delete_resp.status
110+
for status in deletion_statuses:
111+
logging.debug(f"deletion status: {status}")
112+
if jobs_delete_resp.errors:
113+
for error in jobs_delete_resp.errors:
114+
logging.error(f"error deleting job: {error}")
115+
raise RuntimeError(f"Could not delete jobs for user: {username} with account: {account}")
55116
else:
56-
raise ConnectionError("Could not get list of accounts from SLURM endpoint")
117+
raise ConnectionError(f"Could not delete jobs for user: {username} with account: {account}")
118+
119+
# set maxsubmit to 0 as root
120+
max_submit_assoc: V0043Assoc = V0043Assoc(
121+
user=username,
122+
account=account,
123+
max_=V0043AssocMax(V0043AssocMaxJobs(total=V0043Uint32NoValStruct(number=0))),
124+
)
125+
body_max_submit: V0043OpenapiAssocsResp = V0043OpenapiAssocsResp(associations=[max_submit_assoc])
126+
maxsubmit_set_resp = slurmdb_v0043_post_associations.sync(client=self.root_client, body=body_max_submit)
127+
if maxsubmit_set_resp:
128+
logger.info("resp")
129+
if maxsubmit_set_resp.errors:
130+
for error in maxsubmit_set_resp.errors:
131+
logging.error(f"error posting association: {error}")
132+
raise RuntimeError(f"Could not set maxsubmit=0 for user: {username} with account: {account}")
133+
else:
134+
raise ConnectionError(f"Could not set maxsubmit=0 for user: {username} with account: {account}")
135+
136+
default_assoc_get_resp = slurmdb_v0043_get_associations.sync(
137+
client=self.root_client, filter_to_only_defaults=str(True)
138+
)
139+
if not default_assoc_get_resp:
140+
raise ConnectionError(f"Could not get default association for user: {username}")
141+
if default_assoc_get_resp.errors:
142+
raise RuntimeError(f"Could not get default association for user: {username}")
143+
144+
default_account_for_this_user = default_assoc_get_resp.associations[0].account
145+
if default_account_for_this_user == account:
146+
logger.info(
147+
f"User: {username} has set the account: {account} as their default. Re-setting their default account."
148+
)
149+
# Assuming that everyone on the cluster is part of the account "users"!
150+
set_default_account_accoc: V0043Assoc = V0043Assoc(user=username, account="users", is_default=True)
151+
default_assoc_set_body: V0043OpenapiAssocsResp = V0043OpenapiAssocsResp(
152+
associations=[set_default_account_accoc]
153+
)
154+
155+
default_assoc_set_resp = slurmdb_v0043_post_associations.sync(
156+
client=self.root_client, body=default_assoc_set_body
157+
)
158+
if not default_assoc_set_resp:
159+
raise ConnectionError(f"Could not set default association for user: {username}")
160+
if default_assoc_set_resp.errors:
161+
raise RuntimeError(f"Could not set default association for user: {username}")
162+
163+
# finally delete the association!
164+
delete_assoc_resp = slurmdb_v0043_delete_association.sync(
165+
client=self.root_client, user=username, account=account
166+
)
167+
if not delete_assoc_resp:
168+
raise ConnectionError(f"Could not delete association for user: {username} and account: {account}")
169+
if delete_assoc_resp.errors:
170+
raise RuntimeError(f"Could not delete association for user: {username} and account: {account}")
171+
172+
return
173+
174+
@retry(
175+
wait=wait_exponential(multiplier=2, min=2, max=10),
176+
stop=stop_after_attempt(3),
177+
retry=retry_if_not_exception_type(ConnectionError),
178+
before_sleep=before_sleep_log(logging.getLogger(__name__), logging.WARNING),
179+
)
180+
def delete_slurm_account(self, account: str, noop: bool = False) -> None:
181+
if noop:
182+
logging.info(f"noop enabled: skip deleting acconut: {account}")
183+
return
184+
185+
account_delete_resp = slurmdb_v0043_delete_account.sync(client=self.root_client, account_name=account)
186+
187+
if not account_delete_resp:
188+
raise ConnectionError(f"Could not delete account: {account}")
189+
if account_delete_resp.errors:
190+
raise RuntimeError(f"Could not delete account: {account}")
191+
for account_removed in account_delete_resp.removed_accounts:
192+
logging.info(f"Removed SLURM account: {account_removed}")
193+
return

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,4 @@ plugins = ["mypy_django_plugin.main"]
109109
django_settings_module = "coldfront.config.settings"
110110

111111
[tool.uv.sources]
112-
slurm-rest-api-client = { git = "https://github.com/NYU-RTS/rts-slurm-python-sdk", branch = "slimmer-cleaner-client-sdk" }
112+
slurm-rest-api-client = { git = "https://github.com/NYU-RTS/rts-slurm-python-sdk"}

0 commit comments

Comments
 (0)