aws-parallelcluster-node 3.15.1__tar.gz → 3.16.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {aws_parallelcluster_node-3.15.1/src/aws_parallelcluster_node.egg-info → aws_parallelcluster_node-3.16.1}/PKG-INFO +1 -1
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/setup.py +1 -1
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1/src/aws_parallelcluster_node.egg-info}/PKG-INFO +1 -1
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/common/schedulers/slurm_commands.py +211 -102
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/common/schedulers/slurm_reservation_commands.py +30 -37
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/slurm_plugin/clustermgtd.py +43 -17
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/slurm_plugin/fleet_manager.py +31 -4
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/slurm_plugin/fleet_status_manager.py +6 -1
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/slurm_plugin/instance_manager.py +17 -3
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/slurm_plugin/resume.py +8 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/slurm_plugin/slurm_resources.py +25 -5
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/LICENSE.txt +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/NOTICE.txt +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/README.md +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/setup.cfg +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/aws/__init__.py +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/aws/common.py +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/aws/ec2.py +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/aws_parallelcluster_node.egg-info/SOURCES.txt +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/aws_parallelcluster_node.egg-info/dependency_links.txt +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/aws_parallelcluster_node.egg-info/entry_points.txt +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/aws_parallelcluster_node.egg-info/not-zip-safe +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/aws_parallelcluster_node.egg-info/requires.txt +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/aws_parallelcluster_node.egg-info/top_level.txt +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/common/__init__.py +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/common/ec2_utils.py +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/common/schedulers/__init__.py +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/common/time_utils.py +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/common/utils.py +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/slurm_plugin/__init__.py +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/slurm_plugin/capacity_block_manager.py +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/slurm_plugin/cluster_event_publisher.py +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/slurm_plugin/common.py +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/slurm_plugin/computemgtd.py +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/slurm_plugin/console_logger.py +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/slurm_plugin/logging/parallelcluster_clustermgtd_logging.conf +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/slurm_plugin/logging/parallelcluster_computemgtd_logging.conf +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/slurm_plugin/logging/parallelcluster_fleet_status_manager_logging.conf +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/slurm_plugin/logging/parallelcluster_resume_logging.conf +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/slurm_plugin/logging/parallelcluster_suspend_logging.conf +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/slurm_plugin/suspend.py +0 -0
- {aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/slurm_plugin/task_executor.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-parallelcluster-node
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.16.1
|
|
4
4
|
Summary: aws-parallelcluster-node provides the scripts for an AWS ParallelCluster node.
|
|
5
5
|
Home-page: https://github.com/aws/aws-parallelcluster-node
|
|
6
6
|
Author: Amazon Web Services
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-parallelcluster-node
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.16.1
|
|
4
4
|
Summary: aws-parallelcluster-node provides the scripts for an AWS ParallelCluster node.
|
|
5
5
|
Home-page: https://github.com/aws/aws-parallelcluster-node
|
|
6
6
|
Author: Amazon Web Services
|
|
@@ -13,6 +13,11 @@ import json
|
|
|
13
13
|
import logging
|
|
14
14
|
import os
|
|
15
15
|
import re
|
|
16
|
+
import shlex
|
|
17
|
+
|
|
18
|
+
# A nosec comment is appended to the following line in order to disable the B404 check.
|
|
19
|
+
# In this file the input of the module subprocess is trusted.
|
|
20
|
+
import subprocess # nosec B404
|
|
16
21
|
from datetime import datetime, timezone
|
|
17
22
|
from typing import Dict, List
|
|
18
23
|
|
|
@@ -60,10 +65,25 @@ SLURM_CONF_DIR = os.path.join(os.path.split(SLURM_BINARIES_DIR)[0], "etc")
|
|
|
60
65
|
SCONTROL = f"sudo {SLURM_BINARIES_DIR}/scontrol"
|
|
61
66
|
SINFO = f"{SLURM_BINARIES_DIR}/sinfo"
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
SCONTROL_NODE_INFO_FIELD_REGEX = re.compile(
|
|
69
|
+
r"^(NodeName=\S+)"
|
|
70
|
+
r"|(NodeAddr=\S+)"
|
|
71
|
+
r"|(NodeHostName=\S+)"
|
|
72
|
+
r"|(?<!Next)(State=\S+)"
|
|
73
|
+
r"|(Partitions=\S+)"
|
|
74
|
+
r"|(SlurmdStartTime=\S+)"
|
|
75
|
+
r"|(LastBusyTime=\S+)"
|
|
76
|
+
r"|(ReservationName=\S+)"
|
|
77
|
+
r"|(InstanceId=\S+)"
|
|
78
|
+
r"|(Reason=.*)",
|
|
79
|
+
re.MULTILINE,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Fields extracted from `scontrol show partitions` output. `(?<!Next)` ensures `State` is matched but
|
|
83
|
+
# `NextState` is not.
|
|
84
|
+
SCONTROL_PARTITION_INFO_FIELD_REGEX = re.compile(
|
|
85
|
+
r"^(PartitionName=\S+)" r"|(?<!Next)(State=\S+)",
|
|
86
|
+
re.MULTILINE,
|
|
67
87
|
)
|
|
68
88
|
|
|
69
89
|
# Set default timeouts for running different slurm commands.
|
|
@@ -129,6 +149,7 @@ def update_nodes(
|
|
|
129
149
|
nodes,
|
|
130
150
|
nodeaddrs=None,
|
|
131
151
|
nodehostnames=None,
|
|
152
|
+
instance_ids=None,
|
|
132
153
|
state=None,
|
|
133
154
|
reason=None,
|
|
134
155
|
raise_on_error=True,
|
|
@@ -150,8 +171,11 @@ def update_nodes(
|
|
|
150
171
|
For example, if updating a state cause failure, but updating nodeaddr cause no failure.
|
|
151
172
|
if we run scontrol update state=fail_state nodeaddr=good_addr nodename=name,
|
|
152
173
|
the scontrol command will fail but nodeaddr will be updated to good_addr.
|
|
174
|
+
|
|
175
|
+
InstanceId is set in the same batched command as NodeAddr so that the node and its backing
|
|
176
|
+
instance are associated atomically.
|
|
153
177
|
"""
|
|
154
|
-
batched_node_info = _batch_node_info(nodes, nodeaddrs, nodehostnames, batch_size=100)
|
|
178
|
+
batched_node_info = _batch_node_info(nodes, nodeaddrs, nodehostnames, instance_ids, batch_size=100)
|
|
155
179
|
|
|
156
180
|
update_cmd = f"{SCONTROL} update"
|
|
157
181
|
if state:
|
|
@@ -160,7 +184,7 @@ def update_nodes(
|
|
|
160
184
|
if reason:
|
|
161
185
|
validate_subprocess_argument(reason)
|
|
162
186
|
update_cmd += f' reason="{reason}"'
|
|
163
|
-
for nodenames, addrs, hostnames in batched_node_info:
|
|
187
|
+
for nodenames, addrs, hostnames, ids in batched_node_info:
|
|
164
188
|
validate_subprocess_argument(nodenames)
|
|
165
189
|
node_info = f"nodename={nodenames}"
|
|
166
190
|
if addrs:
|
|
@@ -169,6 +193,9 @@ def update_nodes(
|
|
|
169
193
|
if hostnames:
|
|
170
194
|
validate_subprocess_argument(hostnames)
|
|
171
195
|
node_info += f" nodehostname={hostnames}"
|
|
196
|
+
if ids:
|
|
197
|
+
validate_subprocess_argument(ids)
|
|
198
|
+
node_info += f" instanceid={ids}"
|
|
172
199
|
# It's safe to use the function affected by B604 since the command is fully built in this code
|
|
173
200
|
run_command( # nosec B604
|
|
174
201
|
f"{update_cmd} {node_info}", raise_on_error=raise_on_error, timeout=command_timeout, shell=True
|
|
@@ -213,6 +240,44 @@ def update_all_partitions(state, reset_node_addrs_hostname):
|
|
|
213
240
|
return False
|
|
214
241
|
|
|
215
242
|
|
|
243
|
+
def reset_nodes_in_inactive_partitions():
|
|
244
|
+
"""
|
|
245
|
+
Reset nodeaddr/nodehostname and set to down the nodes belonging to INACTIVE partitions.
|
|
246
|
+
|
|
247
|
+
This is meant to be called when starting the compute fleet, before bringing partitions back UP, to clean up nodes
|
|
248
|
+
that an INACTIVE partition may have left behind (e.g. dynamic nodes that were powering up without a backing
|
|
249
|
+
instance when protected mode disabled the partition). If not reset, those nodes would be detected as bootstrap
|
|
250
|
+
failures right after start and send the cluster back into protected mode. Only INACTIVE partitions are considered,
|
|
251
|
+
so nodes of active partitions (which may be running jobs) are not affected.
|
|
252
|
+
"""
|
|
253
|
+
try:
|
|
254
|
+
# Collect the node names of INACTIVE partitions, skipping partitions with no nodes to avoid building a
|
|
255
|
+
# malformed (e.g. empty or with consecutive commas) node list.
|
|
256
|
+
inactive_partition_nodes = [
|
|
257
|
+
part.nodenames
|
|
258
|
+
for part in get_partitions_info()
|
|
259
|
+
if PartitionStatus(part.state) == PartitionStatus.INACTIVE and part.nodenames
|
|
260
|
+
]
|
|
261
|
+
if not inactive_partition_nodes:
|
|
262
|
+
log.info("No nodes in INACTIVE partitions, nothing to reset.")
|
|
263
|
+
return
|
|
264
|
+
# Reset only the nodes that actually need it (e.g. nodeaddr still set), mirroring clustermgtd's inactive
|
|
265
|
+
# cleanup.
|
|
266
|
+
nodes_to_reset = {
|
|
267
|
+
node.name for node in get_nodes_info(",".join(inactive_partition_nodes)) if node.needs_reset_when_inactive()
|
|
268
|
+
}
|
|
269
|
+
if nodes_to_reset:
|
|
270
|
+
log.info(
|
|
271
|
+
"Resetting nodeaddr/nodehostname and setting to down the following nodes of INACTIVE partitions: %s",
|
|
272
|
+
sorted(nodes_to_reset),
|
|
273
|
+
)
|
|
274
|
+
reset_nodes(nodes_to_reset, state="down", reason="inactive partition", raise_on_error=False)
|
|
275
|
+
else:
|
|
276
|
+
log.info("No nodes of INACTIVE partitions need to be reset.")
|
|
277
|
+
except Exception as e:
|
|
278
|
+
log.error("Failed when resetting nodes of INACTIVE partitions with error %s", e)
|
|
279
|
+
|
|
280
|
+
|
|
216
281
|
def _batch_attribute(attribute, batch_size, expected_length=None):
|
|
217
282
|
"""Parse an attribute into batches."""
|
|
218
283
|
if type(attribute) is str:
|
|
@@ -223,29 +288,30 @@ def _batch_attribute(attribute, batch_size, expected_length=None):
|
|
|
223
288
|
return [",".join(batch) for batch in grouper(attribute, batch_size)]
|
|
224
289
|
|
|
225
290
|
|
|
226
|
-
def
|
|
227
|
-
"""
|
|
291
|
+
def _batch_optional_attribute(attribute, attribute_label, nodenames, default_batch, batch_size):
|
|
292
|
+
"""Batch an optional per-node attribute, raising if its entry count does not match the nodes."""
|
|
293
|
+
if not attribute:
|
|
294
|
+
return default_batch
|
|
295
|
+
try:
|
|
296
|
+
return _batch_attribute(attribute, batch_size, expected_length=len(nodenames))
|
|
297
|
+
except ValueError:
|
|
298
|
+
log.error("Nodename %s and %s %s contain different number of entries", nodenames, attribute_label, attribute)
|
|
299
|
+
raise
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _batch_node_info(nodenames, nodeaddrs, nodehostnames, instance_ids, batch_size):
|
|
303
|
+
"""Group nodename, nodeaddrs, nodehostnames, instance_ids into batches."""
|
|
228
304
|
if type(nodenames) is str:
|
|
229
305
|
# Only split on , if there is ] before
|
|
230
306
|
# For ex. "node-[1,3,4-5],node-[20,30]" should split into ["node-[1,3,4-5]","node-[20,30]"]
|
|
231
307
|
nodenames = re.split("(?<=]),", nodenames)
|
|
232
308
|
nodename_batch = _batch_attribute(nodenames, batch_size)
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
nodeaddrs_batch = _batch_attribute(nodeaddrs, batch_size, expected_length=len(nodenames))
|
|
238
|
-
except ValueError:
|
|
239
|
-
log.error("Nodename %s and NodeAddr %s contain different number of entries", nodenames, nodeaddrs)
|
|
240
|
-
raise
|
|
241
|
-
if nodehostnames:
|
|
242
|
-
try:
|
|
243
|
-
nodehostnames_batch = _batch_attribute(nodehostnames, batch_size, expected_length=len(nodenames))
|
|
244
|
-
except ValueError:
|
|
245
|
-
log.error("Nodename %s and NodeHostname %s contain different number of entries", nodenames, nodehostnames)
|
|
246
|
-
raise
|
|
309
|
+
default_batch = [None] * len(nodename_batch)
|
|
310
|
+
nodeaddrs_batch = _batch_optional_attribute(nodeaddrs, "NodeAddr", nodenames, default_batch, batch_size)
|
|
311
|
+
nodehostnames_batch = _batch_optional_attribute(nodehostnames, "NodeHostName", nodenames, default_batch, batch_size)
|
|
312
|
+
instance_ids_batch = _batch_optional_attribute(instance_ids, "InstanceId", nodenames, default_batch, batch_size)
|
|
247
313
|
|
|
248
|
-
return zip(nodename_batch, nodeaddrs_batch, nodehostnames_batch)
|
|
314
|
+
return zip(nodename_batch, nodeaddrs_batch, nodehostnames_batch, instance_ids_batch)
|
|
249
315
|
|
|
250
316
|
|
|
251
317
|
def set_nodes_down(nodes, reason):
|
|
@@ -289,6 +355,83 @@ def set_nodes_idle(nodes, reason=None, reset_node_addrs_hostname=False):
|
|
|
289
355
|
update_nodes(nodes=nodes, state="resume", reason=reason, raise_on_error=False)
|
|
290
356
|
|
|
291
357
|
|
|
358
|
+
def _run_scontrol_command(
|
|
359
|
+
scontrol_args: str,
|
|
360
|
+
command_timeout: int = DEFAULT_GET_INFO_COMMAND_TIMEOUT,
|
|
361
|
+
raise_on_error: bool = True,
|
|
362
|
+
) -> str:
|
|
363
|
+
"""
|
|
364
|
+
Run a `scontrol <args>` command as a standalone subprocess and return its stdout.
|
|
365
|
+
|
|
366
|
+
scontrol's exit code, stderr and raw stdout are logged. When scontrol exits non-zero but still returns
|
|
367
|
+
output (e.g. one of the requested nodes does not exist: Slurm prints "Node <x> not found" and exits 1
|
|
368
|
+
while still returning the other nodes), the output is returned and a warning is logged. When it exits
|
|
369
|
+
non-zero with no output, an error is logged and, when raise_on_error, a CalledProcessError is raised.
|
|
370
|
+
"""
|
|
371
|
+
command = f"{SCONTROL} {scontrol_args}"
|
|
372
|
+
log.debug("Executing scontrol command: %s", command)
|
|
373
|
+
try:
|
|
374
|
+
# nosec B603: command is built from trusted/validated input and run without a shell.
|
|
375
|
+
result = subprocess.run( # nosec B603
|
|
376
|
+
shlex.split(command),
|
|
377
|
+
timeout=command_timeout,
|
|
378
|
+
check=False,
|
|
379
|
+
stdout=subprocess.PIPE,
|
|
380
|
+
stderr=subprocess.PIPE,
|
|
381
|
+
encoding="utf-8",
|
|
382
|
+
)
|
|
383
|
+
except subprocess.TimeoutExpired:
|
|
384
|
+
log.error("scontrol command timed out after %s seconds: '%s'", command_timeout, command)
|
|
385
|
+
raise
|
|
386
|
+
except OSError as e:
|
|
387
|
+
log.error("Unable to execute scontrol command '%s'. Failed with exception: %s", command, e)
|
|
388
|
+
raise
|
|
389
|
+
|
|
390
|
+
stdout = result.stdout or ""
|
|
391
|
+
stderr = (result.stderr or "").strip()
|
|
392
|
+
# Raw scontrol output is logged at DEBUG only, so it is not emitted at the default INFO level.
|
|
393
|
+
log.debug("scontrol command '%s' exited with code %s. Raw output:\n%s", command, result.returncode, stdout)
|
|
394
|
+
|
|
395
|
+
if result.returncode != 0:
|
|
396
|
+
if stdout.strip():
|
|
397
|
+
log.warning(
|
|
398
|
+
"scontrol command '%s' returned non-zero exit code %s but produced output; proceeding with "
|
|
399
|
+
"the output. stderr: '%s'",
|
|
400
|
+
command,
|
|
401
|
+
result.returncode,
|
|
402
|
+
stderr,
|
|
403
|
+
)
|
|
404
|
+
else:
|
|
405
|
+
log.error(
|
|
406
|
+
"scontrol command '%s' failed with exit code %s and produced no output. stderr: '%s'",
|
|
407
|
+
command,
|
|
408
|
+
result.returncode,
|
|
409
|
+
stderr,
|
|
410
|
+
)
|
|
411
|
+
if raise_on_error:
|
|
412
|
+
raise subprocess.CalledProcessError(result.returncode, command, output=stdout, stderr=result.stderr)
|
|
413
|
+
|
|
414
|
+
return stdout
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _extract_scontrol_records(raw_output: str, field_regex) -> List[Dict[str, str]]:
|
|
418
|
+
"""
|
|
419
|
+
Split raw scontrol output into records and extract the relevant `key=value` fields of each record.
|
|
420
|
+
|
|
421
|
+
Records are separated by blank lines. For each record, only the fields matched by field_regex are kept,
|
|
422
|
+
returned as a dict mapping the Slurm field name to its value. Records with no matching field are skipped.
|
|
423
|
+
"""
|
|
424
|
+
records = []
|
|
425
|
+
for record in re.split(r"\n\n+", raw_output.strip()):
|
|
426
|
+
fields = {}
|
|
427
|
+
for match in field_regex.finditer(record):
|
|
428
|
+
key, _, value = match.group(0).partition("=")
|
|
429
|
+
fields[key] = value
|
|
430
|
+
if fields:
|
|
431
|
+
records.append(fields)
|
|
432
|
+
return records
|
|
433
|
+
|
|
434
|
+
|
|
292
435
|
def get_nodes_info(nodes: str = "", command_timeout=DEFAULT_GET_INFO_COMMAND_TIMEOUT) -> List[SlurmNode]:
|
|
293
436
|
"""
|
|
294
437
|
Retrieve SlurmNode list from slurm nodelist notation.
|
|
@@ -304,15 +447,11 @@ def get_nodes_info(nodes: str = "", command_timeout=DEFAULT_GET_INFO_COMMAND_TIM
|
|
|
304
447
|
if nodes == "":
|
|
305
448
|
nodes = _get_all_partition_nodes(",".join(PartitionNodelistMapping.instance().get_partitions()))
|
|
306
449
|
|
|
307
|
-
# Validation to sanitize the input argument and make it safe to use the function affected by B604
|
|
308
450
|
validate_subprocess_argument(nodes)
|
|
309
451
|
|
|
310
|
-
# awk is used to replace the \n\n record separator with '######\n'
|
|
311
452
|
# Note: In case the node does not belong to any partition the Partitions field is missing from Slurm output
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
return _parse_nodes_info(nodeinfo_str)
|
|
453
|
+
raw_node_info = _run_scontrol_command(f"show nodes {nodes}", command_timeout=command_timeout)
|
|
454
|
+
return _parse_nodes_info(_extract_scontrol_records(raw_node_info, SCONTROL_NODE_INFO_FIELD_REGEX))
|
|
316
455
|
|
|
317
456
|
|
|
318
457
|
def get_partitions_info(command_timeout=DEFAULT_GET_INFO_COMMAND_TIMEOUT) -> List[SlurmPartition]:
|
|
@@ -321,16 +460,10 @@ def get_partitions_info(command_timeout=DEFAULT_GET_INFO_COMMAND_TIMEOUT) -> Lis
|
|
|
321
460
|
|
|
322
461
|
This function considers only partitions managed by ParallelCluster.
|
|
323
462
|
"""
|
|
324
|
-
partitions =
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
)
|
|
329
|
-
# It's safe to use the function affected by B604 since the command is fully built in this code
|
|
330
|
-
partition_info_str = check_command_output(
|
|
331
|
-
show_partition_info_command, timeout=command_timeout, shell=True # nosec B604
|
|
332
|
-
)
|
|
333
|
-
partitions_info = _parse_partition_name_and_state(partition_info_str)
|
|
463
|
+
partitions = set(PartitionNodelistMapping.instance().get_partitions())
|
|
464
|
+
raw_partition_info = _run_scontrol_command("show partitions", command_timeout=command_timeout)
|
|
465
|
+
partition_records = _extract_scontrol_records(raw_partition_info, SCONTROL_PARTITION_INFO_FIELD_REGEX)
|
|
466
|
+
partitions_info = _parse_partitions_info(partition_records, managed_partitions=partitions or None)
|
|
334
467
|
return [
|
|
335
468
|
SlurmPartition(
|
|
336
469
|
partition_name,
|
|
@@ -341,15 +474,6 @@ def get_partitions_info(command_timeout=DEFAULT_GET_INFO_COMMAND_TIMEOUT) -> Lis
|
|
|
341
474
|
]
|
|
342
475
|
|
|
343
476
|
|
|
344
|
-
def _get_partition_grep_filter(partitions: List[str]) -> str:
|
|
345
|
-
grep_filter = ""
|
|
346
|
-
if partitions:
|
|
347
|
-
grep_filter += " | grep"
|
|
348
|
-
for partition in partitions:
|
|
349
|
-
grep_filter += f' -e "PartitionName={partition}"'
|
|
350
|
-
return grep_filter
|
|
351
|
-
|
|
352
|
-
|
|
353
477
|
def resume_powering_down_nodes():
|
|
354
478
|
"""Resume nodes that are powering_down so that are set in power state right away."""
|
|
355
479
|
# TODO: This function was added due to Slurm ticket 12915. The bug is not reproducible and the ticket was then
|
|
@@ -359,9 +483,23 @@ def resume_powering_down_nodes():
|
|
|
359
483
|
update_nodes(nodes=powering_down_nodes, state="resume", raise_on_error=False)
|
|
360
484
|
|
|
361
485
|
|
|
362
|
-
def
|
|
363
|
-
"""
|
|
364
|
-
|
|
486
|
+
def _parse_partitions_info(partition_records, managed_partitions=None):
|
|
487
|
+
"""
|
|
488
|
+
Extract (partition_name, partition_state) tuples from scontrol partition records.
|
|
489
|
+
|
|
490
|
+
Only partitions in managed_partitions are returned; if managed_partitions is None, all partitions are
|
|
491
|
+
returned. Records missing the name or state are skipped.
|
|
492
|
+
"""
|
|
493
|
+
partitions_info = []
|
|
494
|
+
for fields in partition_records:
|
|
495
|
+
partition_name = fields.get("PartitionName")
|
|
496
|
+
partition_state = fields.get("State")
|
|
497
|
+
if not partition_name or not partition_state:
|
|
498
|
+
continue
|
|
499
|
+
if managed_partitions is not None and partition_name not in managed_partitions:
|
|
500
|
+
continue
|
|
501
|
+
partitions_info.append((partition_name, partition_state))
|
|
502
|
+
return partitions_info
|
|
365
503
|
|
|
366
504
|
|
|
367
505
|
def _get_all_partition_nodes(partition_name, command_timeout=DEFAULT_GET_INFO_COMMAND_TIMEOUT):
|
|
@@ -386,43 +524,14 @@ def _get_slurm_nodes(states=None, partition_name=None, command_timeout=DEFAULT_G
|
|
|
386
524
|
return check_command_output(sinfo_command, timeout=command_timeout, shell=True).splitlines() # nosec B604
|
|
387
525
|
|
|
388
526
|
|
|
389
|
-
def _parse_nodes_info(
|
|
390
|
-
"""
|
|
391
|
-
|
|
392
|
-
# | awk 'BEGIN{{RS="\n\n" ; ORS="######\n";}} {{print}}' | grep -oP "^(NodeName=\S+)|(NodeAddr=\S+)
|
|
393
|
-
# |(NodeHostName=\S+)|(?<!Next)(State=\S+)|(Partitions=\S+)|(SlurmdStartTime=\S+)|(LastBusyTime=\\S+)
|
|
394
|
-
# |(ReservationName=\S+)|(Reason=.*)|(######)"
|
|
395
|
-
# NodeName=compute-dy-c5xlarge-1
|
|
396
|
-
# NodeAddr=1.2.3.4
|
|
397
|
-
# NodeHostName=compute-dy-c5xlarge-1
|
|
398
|
-
# State=IDLE+CLOUD+POWER
|
|
399
|
-
# Partitions=compute,compute2
|
|
400
|
-
# SlurmdStartTime=2023-01-26T09:57:15
|
|
401
|
-
# Reason=some reason
|
|
402
|
-
# ReservationName=root_1
|
|
403
|
-
# ######
|
|
404
|
-
# NodeName=compute-dy-c5xlarge-2
|
|
405
|
-
# NodeAddr=1.2.3.4
|
|
406
|
-
# NodeHostName=compute-dy-c5xlarge-2
|
|
407
|
-
# State=IDLE+CLOUD+POWER
|
|
408
|
-
# Partitions=compute,compute2
|
|
409
|
-
# SlurmdStartTime=2023-01-26T09:57:15
|
|
410
|
-
# Reason=(Code:InsufficientInstanceCapacity)Failure when resuming nodes
|
|
411
|
-
# ######
|
|
412
|
-
# NodeName=compute-dy-c5xlarge-3
|
|
413
|
-
# NodeAddr=1.2.3.4
|
|
414
|
-
# NodeHostName=compute-dy-c5xlarge-3
|
|
415
|
-
# State=IDLE+CLOUD+POWER
|
|
416
|
-
# Partitions=compute,compute2
|
|
417
|
-
# SlurmdStartTime=2023-01-26T09:57:15
|
|
418
|
-
# ######
|
|
419
|
-
# NodeName=compute-dy-c5xlarge-50001
|
|
420
|
-
# NodeAddr=1.2.3.4
|
|
421
|
-
# NodeHostName=compute-dy-c5xlarge-50001
|
|
422
|
-
# State=IDLE+CLOUD+POWER
|
|
423
|
-
# SlurmdStartTime=None
|
|
424
|
-
# ######
|
|
527
|
+
def _parse_nodes_info(node_records: List[Dict[str, str]]) -> List[SlurmNode]:
|
|
528
|
+
"""
|
|
529
|
+
Build SlurmNode objects from scontrol node records extracted by _extract_scontrol_records.
|
|
425
530
|
|
|
531
|
+
Each record is a dict mapping Slurm field names to their values, e.g.:
|
|
532
|
+
{"NodeName": "compute-dy-c5xlarge-1", "NodeAddr": "1.2.3.4", "NodeHostName": "compute-dy-c5xlarge-1",
|
|
533
|
+
"State": "IDLE+CLOUD+POWER", "Partitions": "compute,compute2", "SlurmdStartTime": "2023-01-26T09:57:15"}
|
|
534
|
+
"""
|
|
426
535
|
map_slurm_key_to_arg = {
|
|
427
536
|
"NodeName": "name",
|
|
428
537
|
"NodeAddr": "nodeaddr",
|
|
@@ -433,32 +542,32 @@ def _parse_nodes_info(slurm_node_info: str) -> List[SlurmNode]:
|
|
|
433
542
|
"SlurmdStartTime": "slurmdstarttime",
|
|
434
543
|
"LastBusyTime": "lastbusytime",
|
|
435
544
|
"ReservationName": "reservation_name",
|
|
545
|
+
"InstanceId": "instance_id",
|
|
436
546
|
}
|
|
437
547
|
|
|
438
548
|
date_fields = ["SlurmdStartTime", "LastBusyTime"]
|
|
439
549
|
|
|
440
|
-
node_info = slurm_node_info.split("######\n")
|
|
441
550
|
slurm_nodes = []
|
|
442
|
-
for
|
|
443
|
-
|
|
551
|
+
for fields in node_records:
|
|
552
|
+
if "NodeName" not in fields:
|
|
553
|
+
continue
|
|
444
554
|
kwargs = {}
|
|
445
|
-
for
|
|
446
|
-
key, value = line.split("=", 1)
|
|
555
|
+
for key, value in fields.items():
|
|
447
556
|
if key in date_fields:
|
|
448
557
|
if value not in ["None", "Unknown"]:
|
|
449
558
|
value = datetime.strptime(value, "%Y-%m-%dT%H:%M:%S").astimezone(tz=timezone.utc)
|
|
450
559
|
else:
|
|
451
560
|
value = None
|
|
561
|
+
elif key == "InstanceId" and value == "(null)":
|
|
562
|
+
# Slurm reports an unset InstanceId as "(null)"
|
|
563
|
+
value = None
|
|
452
564
|
kwargs[map_slurm_key_to_arg[key]] = value
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
slurm_nodes.append(node)
|
|
461
|
-
except InvalidNodenameError:
|
|
462
|
-
log.warning("Ignoring node %s because it has an invalid name", kwargs["name"])
|
|
565
|
+
try:
|
|
566
|
+
if is_static_node(kwargs["name"]):
|
|
567
|
+
slurm_nodes.append(StaticNode(**kwargs))
|
|
568
|
+
else:
|
|
569
|
+
slurm_nodes.append(DynamicNode(**kwargs))
|
|
570
|
+
except InvalidNodenameError:
|
|
571
|
+
log.warning("Ignoring node %s because it has an invalid name", kwargs["name"])
|
|
463
572
|
|
|
464
573
|
return slurm_nodes
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and
|
|
10
10
|
# limitations under the License.
|
|
11
11
|
import logging
|
|
12
|
+
import re
|
|
12
13
|
|
|
13
14
|
# A nosec comment is appended to the following line in order to disable the B404 check.
|
|
14
15
|
# In this file the input of the module subprocess is trusted.
|
|
@@ -16,7 +17,12 @@ import subprocess # nosec B404
|
|
|
16
17
|
from datetime import datetime
|
|
17
18
|
from typing import List, Union
|
|
18
19
|
|
|
19
|
-
from common.schedulers.slurm_commands import
|
|
20
|
+
from common.schedulers.slurm_commands import (
|
|
21
|
+
DEFAULT_SCONTROL_COMMAND_TIMEOUT,
|
|
22
|
+
SCONTROL,
|
|
23
|
+
_extract_scontrol_records,
|
|
24
|
+
_run_scontrol_command,
|
|
25
|
+
)
|
|
20
26
|
from common.utils import (
|
|
21
27
|
SlurmCommandError,
|
|
22
28
|
SlurmCommandErrorHandler,
|
|
@@ -30,9 +36,11 @@ from slurm_plugin.slurm_resources import SlurmReservation
|
|
|
30
36
|
logger = logging.getLogger(__name__)
|
|
31
37
|
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
# Fields extracted from raw `scontrol show reservations` output. Only `ReservationName` is anchored at the
|
|
40
|
+
# start of a line; `(?<!Next)` ensures `State` is matched but `NextState` is not.
|
|
41
|
+
SCONTROL_RESERVATION_INFO_FIELD_REGEX = re.compile(
|
|
42
|
+
r"^(ReservationName=\S+)" r"|(?<!Next)(State=\S+)" r"|(Users=\S+)" r"|(Nodes=\S+)",
|
|
43
|
+
re.MULTILINE,
|
|
36
44
|
)
|
|
37
45
|
|
|
38
46
|
|
|
@@ -256,41 +264,26 @@ def get_slurm_reservations_info(
|
|
|
256
264
|
|
|
257
265
|
Official documentation is https://slurm.schedmd.com/reservations.html
|
|
258
266
|
"""
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
# Users=root
|
|
275
|
-
# State=ACTIVE
|
|
276
|
-
# ######
|
|
277
|
-
# ReservationName=root_9
|
|
278
|
-
# Nodes=queue1-st-crt2micro-1
|
|
279
|
-
# Users=root
|
|
280
|
-
# State=ACTIVE
|
|
281
|
-
# ######
|
|
267
|
+
raw_reservations_info = _run_scontrol_command(
|
|
268
|
+
"show reservations", command_timeout=command_timeout, raise_on_error=raise_on_error
|
|
269
|
+
)
|
|
270
|
+
reservation_records = _extract_scontrol_records(raw_reservations_info, SCONTROL_RESERVATION_INFO_FIELD_REGEX)
|
|
271
|
+
|
|
272
|
+
return _parse_reservations_info(reservation_records)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _parse_reservations_info(reservation_records: List[dict]) -> List[SlurmReservation]:
|
|
276
|
+
"""
|
|
277
|
+
Build SlurmReservation objects from scontrol reservation records extracted by _extract_scontrol_records.
|
|
278
|
+
|
|
279
|
+
Each record is a dict mapping Slurm field names to their values, e.g.:
|
|
280
|
+
{"ReservationName": "root_8", "Nodes": "queuep4d-dy-crp4d-[1-5]", "Users": "root", "State": "ACTIVE"}
|
|
281
|
+
"""
|
|
282
282
|
map_slurm_key_to_arg = {"ReservationName": "name", "Nodes": "nodes", "Users": "users", "State": "state"}
|
|
283
283
|
|
|
284
|
-
reservation_info = slurm_reservations_info.split("######\n")
|
|
285
284
|
slurm_reservations = []
|
|
286
|
-
for
|
|
287
|
-
|
|
288
|
-
kwargs
|
|
289
|
-
for line in lines:
|
|
290
|
-
key, value = line.split("=")
|
|
291
|
-
kwargs[map_slurm_key_to_arg[key]] = value
|
|
292
|
-
if lines:
|
|
293
|
-
reservation = SlurmReservation(**kwargs)
|
|
294
|
-
slurm_reservations.append(reservation)
|
|
285
|
+
for fields in reservation_records:
|
|
286
|
+
kwargs = {map_slurm_key_to_arg[key]: value for key, value in fields.items()}
|
|
287
|
+
slurm_reservations.append(SlurmReservation(**kwargs))
|
|
295
288
|
|
|
296
289
|
return slurm_reservations
|
{aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/slurm_plugin/clustermgtd.py
RENAMED
|
@@ -44,6 +44,7 @@ from slurm_plugin.capacity_block_manager import CapacityBlockManager
|
|
|
44
44
|
from slurm_plugin.cluster_event_publisher import ClusterEventPublisher
|
|
45
45
|
from slurm_plugin.common import TIMESTAMP_FORMAT, ScalingStrategy, log_exception, print_with_count
|
|
46
46
|
from slurm_plugin.console_logger import ConsoleLogger
|
|
47
|
+
from slurm_plugin.fleet_manager import INSTANCE_INFO_RETRIEVAL_TIMEOUT_DEFAULT
|
|
47
48
|
from slurm_plugin.instance_manager import InstanceManager
|
|
48
49
|
from slurm_plugin.slurm_resources import (
|
|
49
50
|
CONFIG_FILE_DIR,
|
|
@@ -97,14 +98,30 @@ class ComputeFleetStatusManager:
|
|
|
97
98
|
COMPUTE_FLEET_STATUS_ATTRIBUTE = "status"
|
|
98
99
|
COMPUTE_FLEET_LAST_UPDATED_TIME_ATTRIBUTE = "lastStatusUpdatedTime"
|
|
99
100
|
|
|
101
|
+
# Retry is sized to outlast the transient IMDS unavailability windows observed in the fleet, which are in the
|
|
102
|
+
# order of a few seconds: waits are 1s, 2s and 4s, plus a jitter of up to 1s each to avoid retrying in lockstep
|
|
103
|
+
# with the other daemons polling IMDS. The overall wait is kept well below the clustermgtd loop time, because
|
|
104
|
+
# the fleet status is retrieved synchronously at the beginning of every loop.
|
|
105
|
+
@staticmethod
|
|
106
|
+
@retry(
|
|
107
|
+
wait_exponential_multiplier=500,
|
|
108
|
+
wait_exponential_max=seconds(4),
|
|
109
|
+
wait_jitter_max=seconds(1),
|
|
110
|
+
stop_max_attempt_number=4,
|
|
111
|
+
)
|
|
112
|
+
def _get_fleet_status():
|
|
113
|
+
# Failures are logged by get_status only once the retries are exhausted, so that the ones recovered by the
|
|
114
|
+
# retries do not pollute the log with errors that have no impact on the cluster.
|
|
115
|
+
compute_fleet_raw_data = check_command_output("get-compute-fleet-status.sh", log_error=False)
|
|
116
|
+
log.debug("Retrieved compute fleet data: %s", compute_fleet_raw_data)
|
|
117
|
+
return ComputeFleetStatus(
|
|
118
|
+
json.loads(compute_fleet_raw_data).get(ComputeFleetStatusManager.COMPUTE_FLEET_STATUS_ATTRIBUTE)
|
|
119
|
+
)
|
|
120
|
+
|
|
100
121
|
@staticmethod
|
|
101
122
|
def get_status(fallback=None):
|
|
102
123
|
try:
|
|
103
|
-
|
|
104
|
-
log.debug("Retrieved compute fleet data: %s", compute_fleet_raw_data)
|
|
105
|
-
return ComputeFleetStatus(
|
|
106
|
-
json.loads(compute_fleet_raw_data).get(ComputeFleetStatusManager.COMPUTE_FLEET_STATUS_ATTRIBUTE)
|
|
107
|
-
)
|
|
124
|
+
return ComputeFleetStatusManager._get_fleet_status()
|
|
108
125
|
except Exception as e:
|
|
109
126
|
if isinstance(e, CalledProcessError):
|
|
110
127
|
error = e.stdout.rstrip()
|
|
@@ -115,7 +132,6 @@ class ComputeFleetStatusManager:
|
|
|
115
132
|
error,
|
|
116
133
|
fallback,
|
|
117
134
|
)
|
|
118
|
-
|
|
119
135
|
return fallback
|
|
120
136
|
|
|
121
137
|
@staticmethod
|
|
@@ -143,6 +159,7 @@ class ClustermgtdConfig:
|
|
|
143
159
|
"run_instances_overrides": "/opt/slurm/etc/pcluster/run_instances_overrides.json",
|
|
144
160
|
"create_fleet_overrides": "/opt/slurm/etc/pcluster/create_fleet_overrides.json",
|
|
145
161
|
"fleet_config_file": "/etc/parallelcluster/slurm_plugin/fleet-config.json",
|
|
162
|
+
"instance_info_retrieval_timeout": INSTANCE_INFO_RETRIEVAL_TIMEOUT_DEFAULT,
|
|
146
163
|
# Terminate configs
|
|
147
164
|
"terminate_max_batch_size": 1000,
|
|
148
165
|
# Timeout to wait for node initialization, should be the same as ResumeTimeout
|
|
@@ -253,6 +270,11 @@ class ClustermgtdConfig:
|
|
|
253
270
|
"clustermgtd", "create_fleet_overrides", fallback=self.DEFAULTS.get("create_fleet_overrides")
|
|
254
271
|
)
|
|
255
272
|
self.create_fleet_overrides = read_json(create_fleet_overrides_file, default={})
|
|
273
|
+
self.instance_info_retrieval_timeout = config.getint(
|
|
274
|
+
"clustermgtd",
|
|
275
|
+
"instance_info_retrieval_timeout",
|
|
276
|
+
fallback=self.DEFAULTS.get("instance_info_retrieval_timeout"),
|
|
277
|
+
)
|
|
256
278
|
|
|
257
279
|
def _get_health_check_config(self, config):
|
|
258
280
|
self.disable_ec2_health_check = config.getboolean(
|
|
@@ -448,6 +470,7 @@ class ClusterManager:
|
|
|
448
470
|
run_instances_overrides=config.run_instances_overrides,
|
|
449
471
|
create_fleet_overrides=config.create_fleet_overrides,
|
|
450
472
|
fleet_config=config.fleet_config,
|
|
473
|
+
instance_info_retrieval_timeout=config.instance_info_retrieval_timeout,
|
|
451
474
|
)
|
|
452
475
|
|
|
453
476
|
def _initialize_executor(self, config):
|
|
@@ -729,13 +752,13 @@ class ClusterManager:
|
|
|
729
752
|
return is_instance_status_unhealthy
|
|
730
753
|
|
|
731
754
|
def _update_static_nodes_in_replacement(self, slurm_nodes: List[SlurmNode]):
|
|
732
|
-
"""Remove from self.static_nodes_in_replacement nodes that are up
|
|
755
|
+
"""Remove from self.static_nodes_in_replacement nodes that are up."""
|
|
733
756
|
nodename_to_slurm_nodes_map = {node.name: node for node in slurm_nodes}
|
|
734
757
|
nodes_still_in_replacement = set()
|
|
735
758
|
for nodename in self._static_nodes_in_replacement:
|
|
736
759
|
node = nodename_to_slurm_nodes_map.get(nodename)
|
|
737
|
-
# Consider nodename still in replacement if node is not up
|
|
738
|
-
if node and not node.is_up()
|
|
760
|
+
# Consider nodename still in replacement if node is not up
|
|
761
|
+
if node and not node.is_up():
|
|
739
762
|
nodes_still_in_replacement.add(nodename)
|
|
740
763
|
|
|
741
764
|
# override self._static_nodes_in_replacement with updated list
|
|
@@ -949,7 +972,11 @@ class ClusterManager:
|
|
|
949
972
|
log.info("Found the following unhealthy dynamic nodes: %s", print_with_count(unhealthy_dynamic_nodes))
|
|
950
973
|
self._handle_unhealthy_dynamic_nodes(unhealthy_dynamic_nodes)
|
|
951
974
|
if unhealthy_static_nodes:
|
|
952
|
-
|
|
975
|
+
# Include the node states, which carry the reason the nodes are considered unhealthy.
|
|
976
|
+
log.info(
|
|
977
|
+
"Found the following unhealthy static nodes: %s",
|
|
978
|
+
print_with_count([f"{node} in state {node.state_string}" for node in unhealthy_static_nodes]),
|
|
979
|
+
)
|
|
953
980
|
self._handle_unhealthy_static_nodes(unhealthy_static_nodes)
|
|
954
981
|
|
|
955
982
|
# evaluate partitions to put in protected mode and ICEs nodes to terminate
|
|
@@ -1145,15 +1172,14 @@ class ClusterManager:
|
|
|
1145
1172
|
|
|
1146
1173
|
@staticmethod
|
|
1147
1174
|
def _update_slurm_nodes_with_ec2_info(nodes, cluster_instances):
|
|
1175
|
+
"""Associate EC2 instances with Slurm nodes by matching on instance ID."""
|
|
1148
1176
|
if cluster_instances:
|
|
1149
|
-
|
|
1177
|
+
instance_id_to_slurm_node_map = {node.instance_id: node for node in nodes if node.instance_id}
|
|
1150
1178
|
for instance in cluster_instances:
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
instance.slurm_node = slurm_node
|
|
1156
|
-
break
|
|
1179
|
+
if instance.id in instance_id_to_slurm_node_map:
|
|
1180
|
+
slurm_node = instance_id_to_slurm_node_map[instance.id]
|
|
1181
|
+
slurm_node.instance = instance
|
|
1182
|
+
instance.slurm_node = slurm_node
|
|
1157
1183
|
|
|
1158
1184
|
@staticmethod
|
|
1159
1185
|
def get_instance_id_to_active_node_map(partitions: List[SlurmPartition]) -> Dict:
|
|
@@ -24,6 +24,12 @@ from slurm_plugin.common import print_with_count
|
|
|
24
24
|
|
|
25
25
|
logger = logging.getLogger(__name__)
|
|
26
26
|
|
|
27
|
+
# Total time budget (seconds) and per-attempt backoff cap for retrying DescribeInstances after a CreateFleet
|
|
28
|
+
# launch, to tolerate EC2 API eventual consistency.
|
|
29
|
+
# See https://docs.aws.amazon.com/ec2/latest/devguide/eventual-consistency.html
|
|
30
|
+
INSTANCE_INFO_RETRIEVAL_TIMEOUT_DEFAULT = 90
|
|
31
|
+
INSTANCE_INFO_RETRIEVAL_MAX_BACKOFF = 30
|
|
32
|
+
|
|
27
33
|
|
|
28
34
|
class EC2Instance:
|
|
29
35
|
def __init__(self, id, private_ip, hostname, all_private_ips, launch_time):
|
|
@@ -94,6 +100,7 @@ class FleetManagerFactory:
|
|
|
94
100
|
all_or_nothing,
|
|
95
101
|
run_instances_overrides,
|
|
96
102
|
create_fleet_overrides,
|
|
103
|
+
instance_info_retrieval_timeout=INSTANCE_INFO_RETRIEVAL_TIMEOUT_DEFAULT,
|
|
97
104
|
):
|
|
98
105
|
try:
|
|
99
106
|
queue_config = fleet_config[queue]
|
|
@@ -120,6 +127,7 @@ class FleetManagerFactory:
|
|
|
120
127
|
compute_resource_config,
|
|
121
128
|
all_or_nothing,
|
|
122
129
|
create_fleet_overrides.get(queue, {}).get(compute_resource, {}),
|
|
130
|
+
instance_info_retrieval_timeout=instance_info_retrieval_timeout,
|
|
123
131
|
)
|
|
124
132
|
elif api == "run-instances":
|
|
125
133
|
return Ec2RunInstancesManager(
|
|
@@ -272,6 +280,7 @@ class Ec2CreateFleetManager(FleetManager):
|
|
|
272
280
|
compute_resource_config,
|
|
273
281
|
all_or_nothing,
|
|
274
282
|
launch_overrides,
|
|
283
|
+
instance_info_retrieval_timeout=INSTANCE_INFO_RETRIEVAL_TIMEOUT_DEFAULT,
|
|
275
284
|
):
|
|
276
285
|
super().__init__(
|
|
277
286
|
cluster_name,
|
|
@@ -283,6 +292,7 @@ class Ec2CreateFleetManager(FleetManager):
|
|
|
283
292
|
all_or_nothing,
|
|
284
293
|
launch_overrides,
|
|
285
294
|
)
|
|
295
|
+
self._instance_info_retrieval_timeout = instance_info_retrieval_timeout
|
|
286
296
|
|
|
287
297
|
def _evaluate_template_overrides(self) -> list:
|
|
288
298
|
"""Build and return the list of Launch Template Overrides to be applied in the CreateFleet request.
|
|
@@ -436,22 +446,39 @@ class Ec2CreateFleetManager(FleetManager):
|
|
|
436
446
|
"""
|
|
437
447
|
Describe instances to retrieve info not available from create-fleet response.
|
|
438
448
|
|
|
449
|
+
Right after a CreateFleet launch, DescribeInstances may return instances with missing info
|
|
450
|
+
(e.g. PrivateIpAddress) or even InvalidInstanceID.NotFound due to EC2 API eventual consistency.
|
|
451
|
+
Retry with exponential backoff (capped per attempt) until the configured total timeout is reached,
|
|
452
|
+
as recommended at https://docs.aws.amazon.com/ec2/latest/devguide/eventual-consistency.html
|
|
453
|
+
|
|
439
454
|
:raises ClientError in case of boto3 failure
|
|
440
455
|
:return list of instances with complete information and list of IDs for instances with incomplete information
|
|
441
456
|
"""
|
|
442
457
|
instances = []
|
|
443
458
|
partial_instance_ids = instance_ids
|
|
444
459
|
|
|
445
|
-
retries = 5
|
|
446
460
|
attempt_count = 0
|
|
461
|
+
# Budget is tracked against the un-jittered backoff; jitter is added only to the actual sleep.
|
|
462
|
+
elapsed_backoff = 0
|
|
447
463
|
# Wait for instances to be available in EC2
|
|
448
464
|
time.sleep(0.1)
|
|
449
|
-
while
|
|
465
|
+
while partial_instance_ids:
|
|
450
466
|
complete_instances, partial_instance_ids = self._retrieve_instances_info_from_ec2(partial_instance_ids)
|
|
451
467
|
instances.extend(complete_instances)
|
|
468
|
+
if not partial_instance_ids:
|
|
469
|
+
break
|
|
470
|
+
base_backoff = min(0.3 * 2 ** (attempt_count + 1), INSTANCE_INFO_RETRIEVAL_MAX_BACKOFF)
|
|
471
|
+
if elapsed_backoff + base_backoff > self._instance_info_retrieval_timeout:
|
|
472
|
+
logger.warning(
|
|
473
|
+
"Unable to retrieve complete info for instances %s within %s seconds, giving up after %s attempts.",
|
|
474
|
+
print_with_count(partial_instance_ids),
|
|
475
|
+
self._instance_info_retrieval_timeout,
|
|
476
|
+
attempt_count + 1,
|
|
477
|
+
)
|
|
478
|
+
break
|
|
479
|
+
elapsed_backoff += base_backoff
|
|
452
480
|
attempt_count += 1
|
|
453
|
-
|
|
454
|
-
time.sleep(0.3 * 2**attempt_count + (secrets.randbelow(500) / 1000))
|
|
481
|
+
time.sleep(base_backoff + (secrets.randbelow(500) / 1000))
|
|
455
482
|
|
|
456
483
|
return instances, partial_instance_ids
|
|
457
484
|
|
|
@@ -19,7 +19,11 @@ from configparser import ConfigParser
|
|
|
19
19
|
from logging.config import fileConfig
|
|
20
20
|
|
|
21
21
|
from botocore.config import Config
|
|
22
|
-
from common.schedulers.slurm_commands import
|
|
22
|
+
from common.schedulers.slurm_commands import (
|
|
23
|
+
reset_nodes_in_inactive_partitions,
|
|
24
|
+
resume_powering_down_nodes,
|
|
25
|
+
update_all_partitions,
|
|
26
|
+
)
|
|
23
27
|
from slurm_plugin.clustermgtd import ComputeFleetStatus, ComputeFleetStatusManager
|
|
24
28
|
from slurm_plugin.common import log_exception
|
|
25
29
|
from slurm_plugin.instance_manager import InstanceManager
|
|
@@ -91,6 +95,7 @@ def _manage_fleet_status_transition(config, computefleet_status_data_path):
|
|
|
91
95
|
|
|
92
96
|
def _start_partitions():
|
|
93
97
|
log.info("Setting slurm partitions to UP and resuming nodes...")
|
|
98
|
+
reset_nodes_in_inactive_partitions()
|
|
94
99
|
update_all_partitions(PartitionStatus.UP, reset_node_addrs_hostname=False)
|
|
95
100
|
# TODO: This function was added due to Slurm ticket 12915. The bug is not reproducible and the ticket was then
|
|
96
101
|
# closed. This operation may now be useless: we need to check this.
|
|
@@ -25,7 +25,11 @@ from common.ec2_utils import get_private_ip_address_and_dns_name
|
|
|
25
25
|
from common.schedulers.slurm_commands import get_nodes_info, update_nodes
|
|
26
26
|
from common.utils import grouper, setup_logging_filter
|
|
27
27
|
from slurm_plugin.common import ComputeInstanceDescriptor, ScalingStrategy, log_exception, print_with_count
|
|
28
|
-
from slurm_plugin.fleet_manager import
|
|
28
|
+
from slurm_plugin.fleet_manager import (
|
|
29
|
+
INSTANCE_INFO_RETRIEVAL_TIMEOUT_DEFAULT,
|
|
30
|
+
EC2Instance,
|
|
31
|
+
FleetManagerFactory,
|
|
32
|
+
)
|
|
29
33
|
from slurm_plugin.slurm_resources import (
|
|
30
34
|
EC2_HEALTH_STATUS_UNHEALTHY_STATES,
|
|
31
35
|
EC2_INSTANCE_ALIVE_STATES,
|
|
@@ -85,6 +89,7 @@ class InstanceManager:
|
|
|
85
89
|
run_instances_overrides: dict = None,
|
|
86
90
|
create_fleet_overrides: dict = None,
|
|
87
91
|
job_level_scaling: bool = False,
|
|
92
|
+
instance_info_retrieval_timeout: int = INSTANCE_INFO_RETRIEVAL_TIMEOUT_DEFAULT,
|
|
88
93
|
):
|
|
89
94
|
"""Initialize InstanceLauncher with required attributes."""
|
|
90
95
|
self._region = region
|
|
@@ -101,6 +106,7 @@ class InstanceManager:
|
|
|
101
106
|
self._fleet_config = fleet_config
|
|
102
107
|
self._run_instances_overrides = run_instances_overrides or {}
|
|
103
108
|
self._create_fleet_overrides = create_fleet_overrides or {}
|
|
109
|
+
self._instance_info_retrieval_timeout = instance_info_retrieval_timeout
|
|
104
110
|
self._boto3_resource_factory = lambda resource_name: boto3.session.Session().resource(
|
|
105
111
|
resource_name, region_name=region, config=boto3_config
|
|
106
112
|
)
|
|
@@ -262,7 +268,8 @@ class InstanceManager:
|
|
|
262
268
|
"""
|
|
263
269
|
Get instances that are associated with the cluster.
|
|
264
270
|
|
|
265
|
-
Instances
|
|
271
|
+
Instances with missing info (e.g. PrivateIpAddress due to EC2 eventual consistency) are kept with
|
|
272
|
+
empty IP fields so that clustermgtd can still match them to Slurm nodes by instance ID.
|
|
266
273
|
"""
|
|
267
274
|
ec2_client = boto3.client("ec2", region_name=self._region, config=self._boto3_config)
|
|
268
275
|
paginator = ec2_client.get_paginator("describe_instances")
|
|
@@ -290,12 +297,17 @@ class InstanceManager:
|
|
|
290
297
|
)
|
|
291
298
|
)
|
|
292
299
|
except Exception as e:
|
|
300
|
+
# Keep the instance with empty IP info so it can still be matched by instance ID in clustermgtd.
|
|
293
301
|
logger.warning(
|
|
294
|
-
"
|
|
302
|
+
"Incomplete EC2 info for instance %s, keeping it for instance-ID matching, "
|
|
303
|
+
"exception: %s, message: %s",
|
|
295
304
|
instance_info["InstanceId"],
|
|
296
305
|
type(e).__name__,
|
|
297
306
|
e,
|
|
298
307
|
)
|
|
308
|
+
instances.append(
|
|
309
|
+
EC2Instance(instance_info["InstanceId"], "", "", set(), instance_info.get("LaunchTime"))
|
|
310
|
+
)
|
|
299
311
|
|
|
300
312
|
return instances
|
|
301
313
|
|
|
@@ -1008,6 +1020,7 @@ class InstanceManager:
|
|
|
1008
1020
|
all_or_nothing=all_or_nothing_batch,
|
|
1009
1021
|
run_instances_overrides=self._run_instances_overrides,
|
|
1010
1022
|
create_fleet_overrides=self._create_fleet_overrides,
|
|
1023
|
+
instance_info_retrieval_timeout=self._instance_info_retrieval_timeout,
|
|
1011
1024
|
)
|
|
1012
1025
|
return fleet_manager
|
|
1013
1026
|
|
|
@@ -1077,6 +1090,7 @@ class InstanceManager:
|
|
|
1077
1090
|
slurm_nodes,
|
|
1078
1091
|
nodeaddrs=[instance.private_ip for instance in launched_instances],
|
|
1079
1092
|
nodehostnames=node_hostnames,
|
|
1093
|
+
instance_ids=[instance.id for instance in launched_instances],
|
|
1080
1094
|
)
|
|
1081
1095
|
logger.info(
|
|
1082
1096
|
"Nodes are now configured with instances %s",
|
{aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/slurm_plugin/resume.py
RENAMED
|
@@ -22,6 +22,7 @@ from common.schedulers.slurm_commands import get_nodes_info, set_nodes_down
|
|
|
22
22
|
from common.utils import read_json
|
|
23
23
|
from slurm_plugin.cluster_event_publisher import ClusterEventPublisher
|
|
24
24
|
from slurm_plugin.common import ScalingStrategy, is_clustermgtd_heartbeat_valid, print_with_count
|
|
25
|
+
from slurm_plugin.fleet_manager import INSTANCE_INFO_RETRIEVAL_TIMEOUT_DEFAULT
|
|
25
26
|
from slurm_plugin.instance_manager import InstanceManager
|
|
26
27
|
from slurm_plugin.slurm_resources import CONFIG_FILE_DIR
|
|
27
28
|
|
|
@@ -47,6 +48,7 @@ class SlurmResumeConfig:
|
|
|
47
48
|
"fleet_config_file": "/etc/parallelcluster/slurm_plugin/fleet-config.json",
|
|
48
49
|
"job_level_scaling": True,
|
|
49
50
|
"scaling_strategy": "all-or-nothing",
|
|
51
|
+
"instance_info_retrieval_timeout": INSTANCE_INFO_RETRIEVAL_TIMEOUT_DEFAULT,
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
def __init__(self, config_file_path):
|
|
@@ -96,6 +98,11 @@ class SlurmResumeConfig:
|
|
|
96
98
|
self.job_level_scaling = config.getboolean(
|
|
97
99
|
"slurm_resume", "job_level_scaling", fallback=self.DEFAULTS.get("job_level_scaling")
|
|
98
100
|
)
|
|
101
|
+
self.instance_info_retrieval_timeout = config.getint(
|
|
102
|
+
"slurm_resume",
|
|
103
|
+
"instance_info_retrieval_timeout",
|
|
104
|
+
fallback=self.DEFAULTS.get("instance_info_retrieval_timeout"),
|
|
105
|
+
)
|
|
99
106
|
fleet_config_file = config.get(
|
|
100
107
|
"slurm_resume", "fleet_config_file", fallback=self.DEFAULTS.get("fleet_config_file")
|
|
101
108
|
)
|
|
@@ -206,6 +213,7 @@ def _resume(arg_nodes, resume_config, slurm_resume):
|
|
|
206
213
|
run_instances_overrides=resume_config.run_instances_overrides,
|
|
207
214
|
create_fleet_overrides=resume_config.create_fleet_overrides,
|
|
208
215
|
job_level_scaling=resume_config.job_level_scaling,
|
|
216
|
+
instance_info_retrieval_timeout=resume_config.instance_info_retrieval_timeout,
|
|
209
217
|
)
|
|
210
218
|
instance_manager.add_instances(
|
|
211
219
|
slurm_resume=slurm_resume,
|
|
@@ -203,7 +203,11 @@ class SlurmNode(metaclass=ABCMeta):
|
|
|
203
203
|
SLURM_SCONTROL_POWER_UP_STATE = "POWERING_UP"
|
|
204
204
|
SLURM_SCONTROL_ONLINE_STATES = {"IDLE+CLOUD", "MIXED+CLOUD", "ALLOCATED+CLOUD", "COMPLETING+CLOUD"}
|
|
205
205
|
SLURM_SCONTROL_POWER_WITH_JOB_STATE = {"MIXED", "CLOUD", "POWERED_DOWN"}
|
|
206
|
-
|
|
206
|
+
# In Slurm 25.11:
|
|
207
|
+
# When ResumeTimeout fires on a cloud node, Slurm explicitly clears NODE_STATE_NO_RESPOND (along with DRAIN,
|
|
208
|
+
# POWER_DOWN, POWERING_UP), adds POWERED_DOWN, and then set node to DOWN with reason "ResumeTimeout reached".
|
|
209
|
+
# The resulting state is therefore DOWN+CLOUD+POWERED_DOWN and does NOT include NOT_RESPONDING.
|
|
210
|
+
SLURM_SCONTROL_RESUME_FAILED_STATE = {"DOWN", "CLOUD", "POWERED_DOWN"}
|
|
207
211
|
SLURM_SCONTROL_NODE_DOWN_NOT_RESPONDING_STATE = {"DOWN", "CLOUD", "NOT_RESPONDING"}
|
|
208
212
|
# Due to a bug in Slurm a powered down node can enter IDLE+CLOUD+POWER_DOWN+POWERED_DOWN state
|
|
209
213
|
SLURM_SCONTROL_POWER_STATES = [{"IDLE", "CLOUD", "POWERED_DOWN"}, {"IDLE", "CLOUD", "POWERED_DOWN", "POWER_DOWN"}]
|
|
@@ -239,6 +243,7 @@ class SlurmNode(metaclass=ABCMeta):
|
|
|
239
243
|
slurmdstarttime: datetime = None,
|
|
240
244
|
lastbusytime: datetime = None,
|
|
241
245
|
reservation_name: str = None,
|
|
246
|
+
instance_id: str = None,
|
|
242
247
|
):
|
|
243
248
|
"""Initialize slurm node with attributes."""
|
|
244
249
|
self.name = name
|
|
@@ -249,6 +254,7 @@ class SlurmNode(metaclass=ABCMeta):
|
|
|
249
254
|
self.partitions = partitions.strip().split(",") if partitions else None
|
|
250
255
|
self.reason = reason
|
|
251
256
|
self.instance = instance
|
|
257
|
+
self.instance_id = instance_id
|
|
252
258
|
self.slurmdstarttime = slurmdstarttime
|
|
253
259
|
self.lastbusytime = lastbusytime
|
|
254
260
|
self.reservation_name = reservation_name
|
|
@@ -529,6 +535,7 @@ class StaticNode(SlurmNode):
|
|
|
529
535
|
slurmdstarttime=None,
|
|
530
536
|
lastbusytime=None,
|
|
531
537
|
reservation_name=None,
|
|
538
|
+
instance_id=None,
|
|
532
539
|
):
|
|
533
540
|
"""Initialize slurm node with attributes."""
|
|
534
541
|
super().__init__(
|
|
@@ -542,6 +549,7 @@ class StaticNode(SlurmNode):
|
|
|
542
549
|
slurmdstarttime,
|
|
543
550
|
lastbusytime=lastbusytime,
|
|
544
551
|
reservation_name=reservation_name,
|
|
552
|
+
instance_id=instance_id,
|
|
545
553
|
)
|
|
546
554
|
|
|
547
555
|
def is_healthy(
|
|
@@ -581,20 +589,30 @@ class StaticNode(SlurmNode):
|
|
|
581
589
|
return True
|
|
582
590
|
else:
|
|
583
591
|
if log_warn_if_unhealthy:
|
|
584
|
-
logger.warning(
|
|
592
|
+
logger.warning(
|
|
593
|
+
"Node state check: node %s in DRAINED and not within the replacement protection window, "
|
|
594
|
+
"it is considered unhealthy and will be replaced, node state: %s",
|
|
595
|
+
self,
|
|
596
|
+
self.state_string,
|
|
597
|
+
)
|
|
585
598
|
return False
|
|
586
599
|
# Check to see if node is in DOWN, ignoring any node currently being replaced
|
|
587
600
|
elif self.is_down() and consider_down_as_unhealthy:
|
|
588
601
|
if self.is_being_replaced:
|
|
589
602
|
logger.debug(
|
|
590
|
-
"Node state check: node %s in DOWN but is currently being replaced, ignoring
|
|
603
|
+
"Node state check: node %s in DOWN but is currently being replaced, ignoring, node state: %s",
|
|
591
604
|
self,
|
|
592
605
|
self.state_string,
|
|
593
606
|
)
|
|
594
607
|
return True
|
|
595
608
|
else:
|
|
596
609
|
if log_warn_if_unhealthy:
|
|
597
|
-
logger.warning(
|
|
610
|
+
logger.warning(
|
|
611
|
+
"Node state check: node %s in DOWN and not within the replacement protection window, "
|
|
612
|
+
"it is considered unhealthy and will be replaced, node state: %s",
|
|
613
|
+
self,
|
|
614
|
+
self.state_string,
|
|
615
|
+
)
|
|
598
616
|
return False
|
|
599
617
|
return True
|
|
600
618
|
|
|
@@ -663,6 +681,7 @@ class DynamicNode(SlurmNode):
|
|
|
663
681
|
slurmdstarttime=None,
|
|
664
682
|
lastbusytime=None,
|
|
665
683
|
reservation_name=None,
|
|
684
|
+
instance_id=None,
|
|
666
685
|
):
|
|
667
686
|
"""Initialize slurm node with attributes."""
|
|
668
687
|
super().__init__(
|
|
@@ -676,6 +695,7 @@ class DynamicNode(SlurmNode):
|
|
|
676
695
|
slurmdstarttime,
|
|
677
696
|
lastbusytime=lastbusytime,
|
|
678
697
|
reservation_name=reservation_name,
|
|
698
|
+
instance_id=instance_id,
|
|
679
699
|
)
|
|
680
700
|
|
|
681
701
|
def is_state_healthy(self, consider_drain_as_unhealthy, consider_down_as_unhealthy, log_warn_if_unhealthy=True):
|
|
@@ -730,7 +750,7 @@ class DynamicNode(SlurmNode):
|
|
|
730
750
|
self.state_string,
|
|
731
751
|
)
|
|
732
752
|
return True
|
|
733
|
-
# Dynamic node in
|
|
753
|
+
# Dynamic node in SLURM_SCONTROL_RESUME_FAILED_STATE.
|
|
734
754
|
elif self.is_bootstrap_timeout():
|
|
735
755
|
# We need to check if nodeaddr is set to avoid counting powering up nodes as bootstrap failure nodes during
|
|
736
756
|
# cluster start/stop.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/common/time_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
{aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/slurm_plugin/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/slurm_plugin/common.py
RENAMED
|
File without changes
|
{aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/slurm_plugin/computemgtd.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aws_parallelcluster_node-3.15.1 → aws_parallelcluster_node-3.16.1}/src/slurm_plugin/suspend.py
RENAMED
|
File without changes
|
|
File without changes
|