blob: 84aded1535b5783cfea2671f7dc7aa116c6c200b [file] [edit]
############################################################################
# Copyright (C) SchedMD LLC.
############################################################################
import atf
import pytest
import csv
import io
import json
import re
def get_cluster_names(remote_license):
"""Find all cluster names"""
cluster_names = [
k
for item in remote_license["actions"]
if "clusters" in item
for k in item["clusters"]
]
return sorted(cluster_names)
# Setup
def setup(remote_license):
cluster_names = get_cluster_names(remote_license)
for cluster in cluster_names:
atf.set_config_parameter("ClusterName", cluster)
# TODO: Set cluster name for all clusters when multi-cluster ability is
# available.
break
if "preserve_case" in remote_license and remote_license["preserve_case"]:
atf.require_config_parameter(
"Parameters", "PreserveCaseResource", source="slurmdbd"
)
atf.require_accounting(modify=True)
atf.require_slurm_running()
for cluster in cluster_names:
print(f"cluster: {cluster}")
atf.run_command(
f"sacctmgr -i add cluster {cluster}",
user=atf.properties["slurm-user"],
)
@pytest.fixture
def do_licenses():
remote_licenses = []
def _processor(remote_license):
setup(remote_license)
remote_licenses.append(remote_license)
_remote_licenses(remote_license)
return "Added"
yield _processor
print(f"Cleaning up resources: {remote_licenses}")
for remote_license in remote_licenses:
atf.run_command(
f"sacctmgr -i delete resource {remote_license['name']}",
user=atf.properties["slurm-user"],
fatal=True,
)
cluster_names = get_cluster_names(remote_license)
for cluster in cluster_names:
print(f"cluster: {cluster}")
atf.run_command(
f"sacctmgr -i delete cluster {cluster}",
user=atf.properties["slurm-user"],
)
def resources_to_list():
output = atf.run_command_output(
"sacctmgr -P show resource", user=atf.properties["slurm-user"], fatal=True
)
resources = []
f = io.StringIO(output.strip())
reader = csv.DictReader(f, delimiter="|")
for row in reader:
for key, value in row.items():
if value.isdigit():
row[key] = int(value)
resources.append(row)
return resources
def cluster_resources_to_dict():
output = atf.run_command_output(
"sacctmgr -P show resource withclusters",
user=atf.properties["slurm-user"],
fatal=True,
)
resources = {}
f = io.StringIO(output.strip())
reader = csv.DictReader(f, delimiter="|")
for row in reader:
for key, value in row.items():
if value.isdigit():
row[key] = int(value)
cluster_name = row.pop("Cluster")
resources[cluster_name] = row
return resources
def _remote_licenses(remote_license):
# Add license
command = f"sacctmgr -i create resource {remote_license['name']} count={remote_license['count']}"
if remote_license["flags"]:
command += f" flags={remote_license["flags"]}"
output = atf.run_command_output(
command, user=atf.properties["slurm-user"], fatal=True
)
# Verify
resource = resources_to_list()[0]
assert resource["Name"] == remote_license["name"]
assert resource["Server"] == "slurmdb"
assert resource["Type"] == "License"
assert resource["Count"] == remote_license["count"]
assert resource["Flags"].lower() == remote_license["flags"].lower()
# Add/modify cluster licenses
for action in remote_license["actions"]:
if action["type"] == "validate":
pass # no mods
elif "clusters" in action:
for cname, res in action["clusters"].items():
mod_cmd = ""
if action["type"] == "modify":
mod_cmd = " set"
if "allowed" in res:
mod_cmd += f" allowed={res['allowed']}"
if "flags" in res:
mod_cmd += f" flags{res['flags']}"
proc = atf.run_command(
f"sacctmgr -i {action['type']} resource {remote_license['name']} cluster={cname} {mod_cmd}",
user=atf.properties["slurm-user"],
)
if "error" in action:
assert re.match(action["error"], proc["stderr"])
assert proc["exit_code"] == 1
else:
assert proc["exit_code"] == 0
else:
mod_cmd = ""
if action["type"] == "modify":
mod_cmd = " set"
if "flags" in action:
mod_cmd += f" flags{action['flags']}"
if "lastconsumed" in action:
mod_cmd += f" lastconsumed={action['lastconsumed']}"
proc = atf.run_command(
f"sacctmgr -i {action['type']} resource {remote_license['name']} {mod_cmd}",
user=atf.properties["slurm-user"],
)
if "error" in action:
assert re.match(action["error"], proc["stderr"])
assert proc["exit_code"] == 1
else:
assert proc["exit_code"] == 0
# Verify
resources = cluster_resources_to_dict()
if "clusters" in action:
for cname, res in action["clusters"].items():
assert resources[cname]["Count"] == remote_license["count"]
assert resources[cname]["Allocated"] == action["allocated"]
if "expected_allowed" in res:
assert resources[cname]["Allowed"] == res["expected_allowed"]
else:
assert resources[cname]["Allowed"] == res["allowed"]
for cname, res in action["clusters"].items():
output = atf.run_command_output(
"scontrol show lic --json",
user=atf.properties["slurm-user"],
fatal=True,
)
resources = json.loads(output)
print(resources)
resource = next(
(
item
for item in resources["licenses"]
if item["LicenseName"] == f"{remote_license['name']}@slurmdb"
),
None,
)
print(resource)
assert resource["Total"] == res["actual_total"]
assert resource["Free"] == res["actual_free"]
assert resource["Remote"]
assert resource["Used"] == 0
# TODO: Check other clusters
break
@pytest.mark.parametrize(
"preserve_case", [False, True], ids=["PreserveCase=No", "PreserveCase=yes"]
)
def test_remote_license_percent(request, do_licenses, preserve_case):
"""Remote licenses - Percent"""
remote_license = {
"name": "lic1",
"count": 200,
"flags": "",
"preserve_case": preserve_case,
"actions": [
{
"type": "add",
"allocated": 100,
"clusters": {
"c1": {"allowed": 25, "actual_total": 50, "actual_free": 50},
"c2": {"allowed": 75, "actual_total": 150, "actual_free": 150},
},
},
{
"type": "modify",
"allocated": 60,
"clusters": {
"c1": {"allowed": 10, "actual_total": 20, "actual_free": 20},
"c2": {"allowed": 50, "actual_total": 100, "actual_free": 100},
},
},
{
"type": "modify",
"allocated": 100,
"clusters": {
"c1": {"allowed": 50, "actual_total": 100, "actual_free": 100},
"c2": {"allowed": 50, "actual_total": 100, "actual_free": 100},
},
},
{
"type": "modify",
"lastconsumed": 100,
},
{
"type": "validate",
"allocated": 100,
"clusters": {
"c1": {"allowed": 50, "actual_total": 100, "actual_free": 100},
"c2": {"allowed": 50, "actual_total": 100, "actual_free": 100},
},
},
{
"type": "modify",
"lastconsumed": 200,
},
{
"type": "validate",
"allocated": 100,
"clusters": {
"c1": {"allowed": 50, "actual_total": 100, "actual_free": 0},
"c2": {"allowed": 50, "actual_total": 100, "actual_free": 0},
},
},
{
"type": "modify",
"lastconsumed": 180,
},
{
"type": "validate",
"allocated": 100,
"clusters": {
"c1": {"allowed": 50, "actual_total": 100, "actual_free": 20},
"c2": {"allowed": 50, "actual_total": 100, "actual_free": 20},
},
},
{
"type": "modify",
"lastconsumed": 101,
},
{
"type": "validate",
"allocated": 100,
"clusters": {
"c1": {"allowed": 50, "actual_total": 100, "actual_free": 99},
"c2": {"allowed": 50, "actual_total": 100, "actual_free": 99},
},
},
],
}
if preserve_case:
if atf.get_version("bin/sacctmgr") < (26, 5):
request.applymarker(
pytest.mark.xfail(
reason="Issue 50782: PreserveCaseResource added in 26.05"
)
)
remote_license["name"] = "LiC1"
do_licenses(remote_license)
def test_remote_license_absolute(do_licenses):
"""Remote licenses - Absolute"""
remote_license = {
"name": "lic1",
"count": 200,
"flags": "absolute",
"actions": [
{
"type": "add",
"allocated": 100,
"clusters": {
"c1": {"allowed": 25, "actual_total": 25, "actual_free": 25},
"c2": {"allowed": 75, "actual_total": 75, "actual_free": 75},
},
},
{
"type": "modify",
"allocated": 60,
"clusters": {
"c1": {"allowed": 10, "actual_total": 10, "actual_free": 10},
"c2": {"allowed": 50, "actual_total": 50, "actual_free": 50},
},
},
{
"type": "modify",
"allocated": 200,
"clusters": {
"c1": {"allowed": 100, "actual_total": 100, "actual_free": 100},
"c2": {"allowed": 100, "actual_total": 100, "actual_free": 100},
},
},
{
"type": "modify",
"lastconsumed": 100,
},
{
"type": "validate",
"allocated": 200,
"clusters": {
"c1": {"allowed": 100, "actual_total": 100, "actual_free": 100},
"c2": {"allowed": 100, "actual_total": 100, "actual_free": 100},
},
},
{
"type": "modify",
"lastconsumed": 200,
},
{
"type": "validate",
"allocated": 200,
"clusters": {
"c1": {"allowed": 100, "actual_total": 100, "actual_free": 0},
"c2": {"allowed": 100, "actual_total": 100, "actual_free": 0},
},
},
{
"type": "modify",
"lastconsumed": 180,
},
{
"type": "validate",
"allocated": 200,
"clusters": {
"c1": {"allowed": 100, "actual_total": 100, "actual_free": 20},
"c2": {"allowed": 100, "actual_total": 100, "actual_free": 20},
},
},
{
"type": "modify",
"lastconsumed": 101,
},
{
"type": "validate",
"allocated": 200,
"clusters": {
"c1": {"allowed": 100, "actual_total": 100, "actual_free": 99},
"c2": {"allowed": 100, "actual_total": 100, "actual_free": 99},
},
},
],
}
do_licenses(remote_license)
@pytest.mark.skipif(
atf.get_version("bin/sacctmgr") < (26, 5),
reason="Issue 50784: SharedPool added in 26.05.",
)
def test_remote_license_sharedpool(do_licenses):
"""Remote licenses - Percent,SharedPool"""
remote_license = {
"name": "lic1",
"count": 200,
"flags": "sharedpool",
"actions": [
{
"type": "add",
"allocated": 200,
"clusters": {
"c1": {"allowed": 100, "actual_total": 200, "actual_free": 200},
"c2": {"allowed": 100, "actual_total": 200, "actual_free": 200},
},
},
{"type": "modify", "lastconsumed": 150},
{
"type": "validate",
"allocated": 200,
"clusters": {
"c1": {"allowed": 100, "actual_total": 200, "actual_free": 50},
"c2": {"allowed": 100, "actual_total": 200, "actual_free": 50},
},
},
{"type": "modify", "lastconsumed": 0},
# Remove flag. Should only let you do it once the license usage is less
# than 100%.
{
"type": "modify",
"flags": "-=sharedpool",
"error": r"sacctmgr: error: You can not allocate more than 100% of a resource.*",
},
{
"type": "modify",
"allocated": 200,
"clusters": {
"c1": {
"allowed": 5,
"actual_total": 200,
"actual_free": 200,
"expected_allowed": 100,
},
},
"error": r"sacctmgr: error: Invalid value for Allowed field when SharedPool enabled, must be 0 or 100% \(without Absolute\) or Count \(with Absolute\)\..*",
},
{
"type": "modify",
"allocated": 0,
"clusters": {
"c1": {"allowed": 0, "actual_total": 0, "actual_free": 0},
"c2": {"allowed": 0, "actual_total": 0, "actual_free": 0},
},
},
{
"type": "modify",
"flags": "-=sharedpool",
},
],
}
do_licenses(remote_license)
@pytest.mark.skipif(
atf.get_version("bin/sacctmgr") < (26, 5),
reason="Issue 50784: SharedPool added in 26.05.",
)
def test_remote_license_add_sharedpool(do_licenses):
"""Test adding SharedPool flag to existing external license"""
remote_license = {
"name": "lic1",
"count": 200,
"flags": "",
"actions": [
{
"type": "add",
"allocated": 100,
"clusters": {
"c1": {"allowed": 50, "actual_total": 100, "actual_free": 100},
"c2": {"allowed": 50, "actual_total": 100, "actual_free": 100},
},
},
{
"type": "modify",
"flags": "=sharedpool",
"allocated": 100,
"error": r"sacctmgr: error: Invalid value for Allowed field when SharedPool enabled, must be 0 or 100% \(without Absolute\) or Count \(with Absolute\)\..*",
},
{
"type": "modify",
"allocated": 0,
"clusters": {
"c1": {"allowed": 0, "actual_total": 0, "actual_free": 0},
"c2": {"allowed": 0, "actual_total": 0, "actual_free": 0},
},
},
{
"type": "modify",
"allocated": 0,
"flags": "=sharedpool",
},
{
"type": "modify",
"allocated": 200,
"clusters": {
"c1": {"allowed": 100, "actual_total": 200, "actual_free": 200},
"c2": {"allowed": 100, "actual_total": 200, "actual_free": 200},
},
},
],
}
do_licenses(remote_license)
@pytest.mark.skipif(
atf.get_version("bin/sacctmgr") < (26, 5),
reason="Issue 50784: SharedPool added in 26.05.",
)
def test_remote_license_abs_sharedpool(do_licenses):
"""Remote licenses - Absolute,SharedPool"""
remote_license = {
"name": "lic1",
"count": 200,
"flags": "absolute,sharedpool",
"actions": [
{
"type": "add",
"allocated": 400,
"clusters": {
"c1": {"allowed": 200, "actual_total": 200, "actual_free": 200},
"c2": {"allowed": 200, "actual_total": 200, "actual_free": 200},
},
},
# Remove flag. Should only let you do it once the license usage is less
# than absolute value.
{
"type": "modify",
"flags": "-=sharedpool",
"error": r"sacctmgr: error: You can not allocate more than 100% of a resource.*",
},
# Setting flags. Should only let you do it once the license usage
# is less than absolute value.
{
"type": "modify",
"flags": "=absolute",
"error": r"sacctmgr: error: You can not allocate more than 100% of a resource.*",
},
{
"type": "modify",
"allocated": 200,
"clusters": {
"c1": {"allowed": 0, "actual_total": 0, "actual_free": 0},
"c2": {"allowed": 200, "actual_total": 200, "actual_free": 200},
},
},
{
"type": "modify",
"flags": "-=sharedpool",
},
],
}
do_licenses(remote_license)