Add the ability to gate access to a reservation by QOS
Changelog: Add the ability to gate access to a reservation by QOS.
Issue: 50466
Ticket: 23173
diff --git a/contribs/slurm_completion_help/slurm_completion.sh b/contribs/slurm_completion_help/slurm_completion.sh
index 073760e..f66617d 100644
--- a/contribs/slurm_completion_help/slurm_completion.sh
+++ b/contribs/slurm_completion_help/slurm_completion.sh
@@ -4321,6 +4321,7 @@
"nodecnt="
"nodes="
"partition="
+ "qos="
"reservation" # meta
"reservationname=" # meta
"skip"
@@ -4364,6 +4365,7 @@
license?(s)) __slurm_compreply_list "$(__slurm_licenses)" ;;
node?(s)) __slurm_compreply_list "$(__slurm_nodes)" "ALL" "true" ;;
partition?(s)) __slurm_compreply "$(__slurm_partitions)" ;;
+ qos?(s)) __slurm_compreply "$(__slurm_qos)" ;;
reservationname?(s)) __slurm_compreply "$(__slurm_reservations)" ;;
user?(s)) __slurm_compreply_list "$(__slurm_users)" ;;
*)
diff --git a/doc/html/reservations.shtml b/doc/html/reservations.shtml
index b7a29e4..3d84a99 100644
--- a/doc/html/reservations.shtml
+++ b/doc/html/reservations.shtml
@@ -3,7 +3,7 @@
<h1>Advanced Resource Reservation Guide</h1>
<p>Slurm has the ability to reserve resources for jobs
-being executed by select users and/or select accounts.
+being executed by select users and/or QOS and/or select accounts.
A resource reservation identifies the resources in that reservation
and a time period during which the reservation is available.
The resources which can be reserved include cores, nodes, licenses and/or
@@ -173,8 +173,9 @@
Nodes=tux8,tux9 CoreCnt=2,4 User=brenda
</pre>
-<p>Reservations can not only be created for the use of specific accounts and
-users, but specific accounts and/or users can be prevented from using them.
+<p>Reservations can not only be created for the use of specific accounts and/or
+QOS and/or users, but specific accounts and/or QOS and/or users can be prevented
+from using them.
In the following example, a reservation is created for account "foo", but user
"alan" is prevented from using the reservation even when using the account
"foo".</p>
@@ -529,6 +530,6 @@
This will prevent the initiation of some jobs which would complete execution
before a reservation given fewer jobs to time-slice with.</p>
-<p style="text-align: center;">Last modified 02 August 2024</p>
+<p style="text-align: center;">Last modified 13 June 2025</p>
<!--#include virtual="footer.txt"-->
diff --git a/doc/man/man1/scontrol.1 b/doc/man/man1/scontrol.1
index a3d463f..d07a15d 100644
--- a/doc/man/man1/scontrol.1
+++ b/doc/man/man1/scontrol.1
@@ -2448,7 +2448,8 @@
List of accounts permitted to use the reserved nodes, for example
"Accounts=physcode1,physcode2".
A user in any of the specified accounts or subaccounts may use the reserved
-nodes. A new reservation must specify Users or Groups and/or Accounts.
+nodes. A new reservation must specify Users or Groups and/or QOS and/or
+Accounts.
If both Users/Groups and Accounts are specified, a job must match both in order
to use the reservation.
Accounts can also be denied access to reservations by preceding all of the
@@ -2796,7 +2797,7 @@
\fBGroups\fR=<\fIgroup list\fR>
List of groups permitted to use the reserved nodes, for example
"Group=bio,chem".
-A new reservation must specify Users or Groups and/or Accounts.
+A new reservation must specify Users or Groups and/or QOS and/or Accounts.
If both Users/Groups and Accounts are specified, a job must match both in order
to use the reservation.
Unlike users groups do not allow denied access to reservations.
@@ -2816,10 +2817,32 @@
.IP
.TP
+\fBQOS\fR=<\fIqos list\fR>
+List of QOS permitted to use the reserved resources, for example
+"QOS=normal,standby".
+A user using any of the specified QOS may use the reserved
+resources. A new reservation must specify Users or Groups and/or QOS and/or
+Accounts.
+If both Users/Groups and QOS are specified, a job must match both in order
+to use the reservation.
+QOS can also be denied access to reservations by preceding all of the
+QOS names with '\-'. Alternately precede the equal sign with '\-'.
+For example, "QOS=\-normal,\-standby" or "QOS\-=normal,standby"
+will permit any QOS except normal and standby to use the reservation.
+You can add or remove individual QOS from an existing reservation by
+using the update command and adding a '+' or '\-' sign before the '=' sign.
+If QOS are denied access to a reservation (QOS name preceded by a '\-'),
+then all other QOS are implicitly allowed to use the reservation and it is
+not possible to also explicitly specify allowed QOS.
+Root and the SlurmUser are given access to all reservations, regardless of the
+QOS set here.
+.IP
+
+.TP
\fBUsers\fR=<\fIuser list\fR>
List of users permitted to use the reserved nodes, for example
"User=jones1,smith2".
-A new reservation must specify Users or Groups and/or Accounts.
+A new reservation must specify Users or Groups and/or QOS and/or Accounts.
If both Users/Groups and Accounts are specified, a job must match both in order
to use the reservation.
Users can also be denied access to reservations by preceding all of the
diff --git a/src/api/reservation_info.c b/src/api/reservation_info.c
index a1e33d9..7f1ad0d 100644
--- a/src/api/reservation_info.c
+++ b/src/api/reservation_info.c
@@ -148,6 +148,11 @@
xstrcat(out, line_end);
/****** Line ******/
+ xstrfmtcat(out,
+ "QOS=%s", resv_ptr->qos);
+ xstrcat(out, line_end);
+
+ /****** Line ******/
if ((resv_ptr->start_time <= now) && (resv_ptr->end_time >= now))
state = "ACTIVE";
xstrfmtcat(out,
diff --git a/src/common/slurm_protocol_defs.c b/src/common/slurm_protocol_defs.c
index c7632fc..06d255e 100644
--- a/src/common/slurm_protocol_defs.c
+++ b/src/common/slurm_protocol_defs.c
@@ -6490,8 +6490,9 @@
if (((resv_msg->users == NULL) || (resv_msg->users[0] == '\0')) &&
((resv_msg->groups == NULL) || (resv_msg->groups[0] == '\0')) &&
+ (!resv_msg->qos || (resv_msg->qos[0] == '\0')) &&
((resv_msg->accounts == NULL) || (resv_msg->accounts[0] == '\0'))) {
- *err_msg = "Either Users/Groups and/or Accounts must be specified. No reservation created.";
+ *err_msg = "Either Users/Groups, QOS and/or Accounts must be specified. No reservation created.";
return SLURM_ERROR;
} else if (resv_msg->users && resv_msg->groups) {
*err_msg = "Users and Groups are mutually exclusive. You can have one or the other, but not both. No reservation created.";
diff --git a/src/scontrol/create_res.c b/src/scontrol/create_res.c
index d7db29c..044a180 100644
--- a/src/scontrol/create_res.c
+++ b/src/scontrol/create_res.c
@@ -167,6 +167,22 @@
resv_msg_ptr->users = val;
}
+ } else if (!xstrncasecmp(tag, "QOS", MAX(taglen, 1))) {
+ if (resv_msg_ptr->qos) {
+ exit_code = 1;
+ error("Parameter %s specified more than once",
+ argv[i]);
+ return SLURM_ERROR;
+ }
+ if (plus_minus) {
+ resv_msg_ptr->qos =
+ scontrol_process_plus_minus(plus_minus,
+ val, false);
+ *res_free_flags |= RESV_FREE_STR_QOS;
+ plus_minus = '\0';
+ } else {
+ resv_msg_ptr->qos = val;
+ }
} else if (!xstrncasecmp(tag, "ReservationName",
MAX(taglen, 1))) {
resv_msg_ptr->name = val;
diff --git a/src/slurmctld/job_scheduler.c b/src/slurmctld/job_scheduler.c
index 342e63e..a6002d5 100644
--- a/src/slurmctld/job_scheduler.c
+++ b/src/slurmctld/job_scheduler.c
@@ -4318,6 +4318,8 @@
FREE_NULL_BITMAP(avail_bitmap);
reservation_delete_resv_exc_parts(&resv_exc);
job_start_data->rc = rc2;
+ if (rc2 == ESLURM_INVALID_QOS)
+ return 0;
return -1;
}
diff --git a/src/slurmctld/reservation.c b/src/slurmctld/reservation.c
index a6e6125..a91a937 100644
--- a/src/slurmctld/reservation.c
+++ b/src/slurmctld/reservation.c
@@ -1000,13 +1000,13 @@
else
duration = resv_ptr->duration;
- info("%s: Name=%s StartTime=%s EndTime=%s Duration=%d Flags=%s NodeCnt=%u CoreCnt=%u NodeList=%s Features=%s PartitionName=%s Users=%s Groups=%s Accounts=%s Licenses=%s BurstBuffer=%s TRES=%s Comment=%s",
+ info("%s: Name=%s StartTime=%s EndTime=%s Duration=%d Flags=%s NodeCnt=%u CoreCnt=%u NodeList=%s Features=%s PartitionName=%s Users=%s Groups=%s Accounts=%s Licenses=%s QOS=%s BurstBuffer=%s TRES=%s Comment=%s",
mode, resv_ptr->name, start_str, end_str, duration,
flag_str, resv_ptr->node_cnt, resv_ptr->core_cnt,
resv_ptr->node_list,
resv_ptr->features, resv_ptr->partition,
resv_ptr->users, resv_ptr->groups, resv_ptr->accounts,
- resv_ptr->licenses,
+ resv_ptr->licenses, resv_ptr->qos,
resv_ptr->burst_buffer, resv_ptr->tres_str,
resv_ptr->comment);
@@ -1260,7 +1260,8 @@
goto end_it;
}
}
- } else if (accounting_enforce & ACCOUNTING_ENFORCE_ASSOCS) {
+ } else if (!resv_ptr->qos &&
+ (accounting_enforce & ACCOUNTING_ENFORCE_ASSOCS)) {
error("We need at least 1 user or 1 account to "
"create a reservtion.");
rc = SLURM_ERROR;
@@ -1293,6 +1294,266 @@
return rc;
}
+static void _addto_name_str(foreach_set_allow_str_t *set_allow_str,
+ char *name)
+{
+ xstrfmtcatat(*set_allow_str->str, &set_allow_str->str_pos, "%s%s%s",
+ *set_allow_str->str ? "," : "",
+ set_allow_str->prefix,
+ name);
+}
+
+static int _foreach_set_qos_name_str(void *x, void *arg)
+{
+ slurmdb_qos_rec_t *qos_ptr = x;
+ foreach_set_allow_str_t *set_allow_str = arg;
+
+ _addto_name_str(set_allow_str, qos_ptr->name);
+ return 0;
+}
+
+extern int _sort_qos_list_asc(void *v1, void *v2)
+{
+ slurmdb_qos_rec_t *qos_a = *(slurmdb_qos_rec_t **) v1;
+ slurmdb_qos_rec_t *qos_b = *(slurmdb_qos_rec_t **) v2;
+
+ return slurm_sort_char_list_asc(&qos_a->name, &qos_b->name);
+}
+
+/*
+ * Since the returned qos_list is full of pointers from the
+ * assoc_mgr_qos_list assoc_mgr_lock_t READ_LOCK on
+ * qos must be set before calling this function and while
+ * handling it after a return.
+ */
+static int _append_to_qos_list(list_t *qos_list, char *qos_name)
+{
+ int rc = ESLURM_INVALID_QOS;
+ slurmdb_qos_rec_t *qos_ptr = NULL;
+ slurmdb_qos_rec_t qos = {
+ .name = qos_name,
+ };
+
+ xassert(qos_list);
+ xassert(qos.name);
+ xassert(verify_assoc_lock(QOS_LOCK, READ_LOCK));
+
+ if (assoc_mgr_fill_in_qos(acct_db_conn, &qos, accounting_enforce,
+ &qos_ptr, true)) {
+ if (accounting_enforce & ACCOUNTING_ENFORCE_QOS) {
+ error("No QOS by name %s", qos.name);
+ } else {
+ verbose("No QOS by name %s", qos.name);
+ rc = SLURM_SUCCESS;
+ }
+ }
+
+ if (qos_ptr) {
+ if (!list_find_first(qos_list, slurm_find_ptr_in_list, qos_ptr))
+ list_append(qos_list, qos_ptr);
+ rc = SLURM_SUCCESS;
+ }
+
+ return rc;
+}
+
+/*
+ * Since the returned qos_list is full of pointers from the
+ * assoc_mgr_qos_list assoc_mgr_lock_t READ_LOCK on
+ * qos must be set before calling this function and while
+ * handling it after a return.
+ */
+static int _remove_from_qos_list(list_t *qos_list, char *qos_name)
+{
+ int rc = ESLURM_INVALID_QOS;
+ slurmdb_qos_rec_t *qos_ptr = NULL;
+ slurmdb_qos_rec_t qos = {
+ .name = qos_name,
+ };
+
+ xassert(qos_list);
+ xassert(qos.name);
+ xassert(verify_assoc_lock(QOS_LOCK, READ_LOCK));
+
+ if (assoc_mgr_fill_in_qos(acct_db_conn, &qos, accounting_enforce,
+ &qos_ptr, true)) {
+ if (accounting_enforce & ACCOUNTING_ENFORCE_QOS) {
+ error("No QOS by name %s", qos.name);
+ } else {
+ verbose("No QOS by name %s", qos.name);
+ rc = SLURM_SUCCESS;
+ }
+ }
+
+ if (qos_ptr) {
+ (void) list_delete_first(qos_list, slurm_find_ptr_in_list,
+ qos_ptr);
+ rc = SLURM_SUCCESS;
+ }
+
+ return rc;
+}
+
+/*
+ * Validate a comma delimited list of account names and build an array of
+ * them
+ * IN account - a list of account names
+ * OUT account_cnt - number of accounts in the list
+ * OUT account_list - list of the account names,
+ * CALLER MUST XFREE this plus each individual record
+ * OUT account_not - true of account_list is that of accounts to be blocked
+ * from reservation access
+ * RETURN 0 on success
+ */
+static int _build_qos_list(char *qos, list_t **qos_list, bool *qos_not,
+ bool break_on_failure)
+{
+ char *last = NULL, *tmp, *tok;
+ int rc = SLURM_SUCCESS;
+ assoc_mgr_lock_t locks = {
+ .qos = READ_LOCK,
+ };
+
+ xassert(qos_list);
+
+ *qos_not = false;
+
+ if (!qos)
+ return ESLURM_INVALID_QOS;
+
+ assoc_mgr_lock(&locks);
+
+ tmp = xstrdup(qos);
+ tok = strtok_r(tmp, ",", &last);
+ while (tok) {
+ if (tok[0] == '-') {
+ if (!*qos_list) {
+ *qos_not = true;
+ } else if (*qos_not != true) {
+ info("Reservation request has some not/qos");
+ rc = ESLURM_INVALID_QOS;
+ break;
+ }
+ tok++;
+ } else if (*qos_not != false) {
+ info("Reservation request has some not/qos");
+ rc = ESLURM_INVALID_QOS;
+ break;
+ }
+
+ if (!*qos_list)
+ *qos_list = list_create(NULL);
+ if (((rc = _append_to_qos_list(*qos_list, tok)) !=
+ SLURM_SUCCESS) && break_on_failure)
+ break;
+
+ tok = strtok_r(NULL, ",", &last);
+ }
+
+ if (rc != SLURM_SUCCESS)
+ FREE_NULL_LIST(*qos_list);
+
+ if (*qos_list) {
+ list_sort(*qos_list, _sort_qos_list_asc);
+ }
+
+ assoc_mgr_unlock(&locks);
+
+ xfree(tmp);
+
+ return rc;
+}
+
+/*
+ * Update a qos list for an existing reservation based upon an
+ * update comma delimited specification of qos to add (+name),
+ * remove (-name), or set value of
+ * IN/OUT resv_ptr - pointer to reservation structure being updated
+ * IN qos - a list of qos names, to set, add, or remove
+ * RETURN 0 on success
+ */
+static int _update_qos_list(slurmctld_resv_t *resv_ptr, char *qos)
+{
+ int rc = SLURM_SUCCESS;
+ char *last = NULL, *tmp, *tok;
+ bool minus_qos = false, cleared = false;
+ list_t *qos_list = NULL;
+ assoc_mgr_lock_t locks = {
+ .qos = READ_LOCK,
+ };
+
+ if (!qos)
+ return ESLURM_INVALID_QOS;
+
+ if (!resv_ptr->qos_list)
+ resv_ptr->qos_list = list_create(NULL);
+ qos_list = list_shallow_copy(resv_ptr->qos_list);
+
+ assoc_mgr_lock(&locks);
+
+ tmp = xstrdup(qos);
+ tok = strtok_r(tmp, ",", &last);
+ while (tok) {
+ if (tok[0] == '-') {
+ minus_qos = 1;
+ tok++;
+ } else if (tok[0] == '+') {
+ minus_qos = 0;
+ tok++;
+ } else if (tok[0] == '\0') {
+ continue;
+ } else {
+ /* The request is a completely new list */
+ if (!cleared) {
+ list_flush(qos_list);
+ resv_ptr->ctld_flags &= (~RESV_CTLD_QOS_NOT);
+ cleared = true;
+ }
+ }
+
+ if (resv_ptr->ctld_flags & RESV_CTLD_QOS_NOT) {
+ if (minus_qos)
+ rc = _append_to_qos_list(qos_list, tok);
+ else
+ rc = _remove_from_qos_list(qos_list, tok);
+ } else {
+ if (minus_qos)
+ rc = _remove_from_qos_list(qos_list, tok);
+ else
+ rc = _append_to_qos_list(qos_list, tok);
+ }
+
+ if (rc != SLURM_SUCCESS)
+ break;
+
+ tok = strtok_r(NULL, ",", &last);
+ }
+
+ if ((rc == SLURM_SUCCESS) && list_count(qos_list)) {
+ foreach_set_allow_str_t set_allow_str = {
+ .prefix = (resv_ptr->ctld_flags & RESV_CTLD_QOS_NOT) ?
+ "-" : "",
+ .str = &resv_ptr->qos,
+ };
+
+ list_sort(qos_list, _sort_qos_list_asc);
+ xfree(resv_ptr->qos);
+ (void) list_for_each(qos_list, _foreach_set_qos_name_str,
+ &set_allow_str);
+ FREE_NULL_LIST(resv_ptr->qos_list);
+ resv_ptr->qos_list = qos_list;
+ qos_list = NULL;
+ } else if (rc == SLURM_SUCCESS)
+ rc = ESLURM_INVALID_QOS;
+
+ assoc_mgr_unlock(&locks);
+
+ FREE_NULL_LIST(qos_list);
+ xfree(tmp);
+
+ return rc;
+}
+
static int _set_access(slurmctld_resv_t *resv_ptr)
{
int rc = SLURM_SUCCESS;
@@ -2807,6 +3068,9 @@
if (resv_ptr->node_list)
xstrfmtcatat(tmp_str, &tmp_str_pos, " nodes=%s",
resv_ptr->node_list);
+ if (resv_ptr->qos)
+ xstrfmtcatat(tmp_str, &tmp_str_pos, " qos=%s",
+ resv_ptr->qos);
if (resv_ptr->tres_fmt_str)
xstrfmtcatat(tmp_str, &tmp_str_pos, " tres=%s",
resv_ptr->tres_fmt_str);
@@ -3006,8 +3270,9 @@
uid_t *user_list = NULL;
list_t *license_list = NULL;
uint32_t total_node_cnt = 0;
- bool account_not = false, user_not = false;
+ bool account_not = false, user_not = false, qos_not = false;
resv_select_t resv_select = { 0 };
+ list_t *qos_list = NULL;
_create_resv_lists(false);
@@ -3161,9 +3426,10 @@
goto bad_parse;
} else if (!resv_desc_ptr->accounts &&
- !resv_desc_ptr->users &&
- !resv_desc_ptr->groups) {
- info("Reservation request lacks users, accounts or groups");
+ !resv_desc_ptr->users &&
+ !resv_desc_ptr->qos &&
+ !resv_desc_ptr->groups) {
+ info("Reservation request lacks users, accounts, QOS, or groups");
rc = ESLURM_RESERVATION_EMPTY;
goto bad_parse;
}
@@ -3192,6 +3458,22 @@
}
}
+ if (resv_desc_ptr->qos) {
+ foreach_set_allow_str_t set_allow_str = {
+ .str = &resv_desc_ptr->qos,
+ };
+
+ rc = _build_qos_list(resv_desc_ptr->qos, &qos_list,
+ &qos_not, true);
+ if (rc != SLURM_SUCCESS)
+ goto bad_parse;
+
+ set_allow_str.prefix = qos_not ? "-" : "";
+ xfree(resv_desc_ptr->qos);
+ (void) list_for_each(qos_list, _foreach_set_qos_name_str,
+ &set_allow_str);
+ }
+
if (resv_desc_ptr->licenses) {
bool valid = true;
license_list = _license_validate2(resv_desc_ptr, &valid);
@@ -3400,6 +3682,8 @@
resv_ptr->ctld_flags |= RESV_CTLD_USER_NOT;
if (account_not)
resv_ptr->ctld_flags |= RESV_CTLD_ACCT_NOT;
+ if (qos_not)
+ resv_ptr->ctld_flags |= RESV_CTLD_QOS_NOT;
resv_ptr->duration = resv_desc_ptr->duration;
if (resv_desc_ptr->purge_comp_time != NO_VAL)
@@ -3447,6 +3731,10 @@
resv_ptr->user_cnt = user_cnt;
resv_ptr->user_list = user_list;
user_list = NULL;
+ resv_ptr->qos = resv_desc_ptr->qos;
+ resv_desc_ptr->qos = NULL;
+ resv_ptr->qos_list = qos_list;
+ qos_list = NULL;
if (!(resv_desc_ptr->flags & RESERVE_FLAG_GRES_REQ) &&
(resv_desc_ptr->core_cnt == NO_VAL)) {
@@ -3482,6 +3770,7 @@
xfree(account_list[i]);
xfree(account_list);
FREE_NULL_LIST(license_list);
+ FREE_NULL_LIST(qos_list);
_free_resv_select_members(&resv_select);
xfree(user_list);
return rc;
@@ -3895,10 +4184,19 @@
goto update_failure;
}
+ if (resv_desc_ptr->qos) {
+ rc = _update_qos_list(resv_ptr, resv_desc_ptr->qos);
+ if (rc) {
+ error_code = rc;
+ goto update_failure;
+ }
+ }
+
if (!resv_ptr->users &&
!resv_ptr->accounts &&
+ !resv_ptr->qos &&
!resv_ptr->groups) {
- info("Reservation %s request lacks users, accounts or groups",
+ info("Reservation %s request lacks users, accounts, QOS or groups",
resv_desc_ptr->name);
error_code = ESLURM_RESERVATION_EMPTY;
goto update_failure;
@@ -4490,6 +4788,19 @@
}
}
+ if (resv_ptr->qos) {
+ bool qos_not; /* we don't care about this */
+ (void) _build_qos_list(resv_ptr->qos,
+ &resv_ptr->qos_list,
+ &qos_not, false);
+
+ if (!resv_ptr->qos_list || !list_count(resv_ptr->qos_list)) {
+ error("Reservation %s has invalid QOS (%s)",
+ resv_ptr->name, resv_ptr->qos);
+ return false;
+ }
+ }
+
if ((resv_ptr->flags & RESERVE_FLAG_PART_NODES) &&
resv_ptr->part_ptr && resv_ptr->part_ptr->node_bitmap) {
memset(&old_resv_ptr, 0, sizeof(slurmctld_resv_t));
@@ -6413,6 +6724,9 @@
char tmp_char[30];
slurmdb_assoc_rec_t *assoc;
if (!resv_ptr->assoc_list) {
+ if (resv_ptr->qos_list)
+ return SLURM_SUCCESS;
+
error("Reservation %s has no association list. "
"Checking user/account lists",
resv_ptr->name);
@@ -6513,6 +6827,67 @@
}
/*
+ * Check if user is requesting a QOS that isn't
+ * allowed in the reservation.
+ * RET SLURM_SUCCESS if true, some error code otherwise
+ */
+static int _valid_job_access_resv_at_sched(job_record_t *job_ptr,
+ slurmctld_resv_t *resv_ptr)
+{
+ int rc = SLURM_SUCCESS;
+
+ if (validate_slurm_user(job_ptr->user_id))
+ return SLURM_SUCCESS;
+
+ /* Check QOS */
+ if (resv_ptr->qos_list) {
+ slurmdb_qos_rec_t *qos_ptr = NULL;
+ if (!job_ptr->qos_ptr) {
+ slurmdb_qos_rec_t qos_rec = {
+ .id = job_ptr->qos_id,
+ };
+ /*
+ * This should never be called, but just to be
+ * safe we will try to fill it in.
+ */
+ if (assoc_mgr_fill_in_qos(
+ acct_db_conn, &qos_rec,
+ accounting_enforce,
+ &job_ptr->qos_ptr, false) ||
+ !job_ptr->qos_ptr) {
+ return ESLURM_INVALID_QOS;
+ }
+ }
+
+ /*
+ * Since we do not allow mixed state check the list's pointers.
+ */
+ qos_ptr = list_find_first(resv_ptr->qos_list,
+ slurm_find_ptr_in_list,
+ job_ptr->qos_ptr);
+
+ if (resv_ptr->ctld_flags & RESV_CTLD_QOS_NOT) {
+ if (qos_ptr) { /* explicitly denied */
+ rc = ESLURM_INVALID_QOS;
+ }
+ } else if (!qos_ptr) { /* not allowed */
+ rc = ESLURM_INVALID_QOS;
+ }
+
+ if (rc != SLURM_SUCCESS) {
+ debug2("%pJ attempted to use reservation '%s' with QOS '%s' not allowed in reservation (%s)",
+ job_ptr,
+ resv_ptr->name,
+ job_ptr->qos_ptr->name,
+ resv_ptr->qos);
+ return rc;
+ }
+ }
+
+ return rc;
+}
+
+/*
* Determine if a job can start now based only upon reservations
*
* IN job_ptr - job to test
@@ -6927,6 +7302,11 @@
rc2 = _valid_job_access_resv(job_ptr, resv_ptr, true);
if (rc2 != SLURM_SUCCESS)
return rc2;
+
+ rc2 = _valid_job_access_resv_at_sched(job_ptr, resv_ptr);
+ if (rc2 != SLURM_SUCCESS)
+ return rc2;
+
/*
* Just in case the reservation was altered since last looking
* we want to make sure things are good in the database.
diff --git a/testsuite/README b/testsuite/README
index 113a79a..414611d 100644
--- a/testsuite/README
+++ b/testsuite/README
@@ -955,6 +955,7 @@
test_123_2 Test RESV_DEL_HOLD
test_123_3 Test overlapping reservations.
test_123_4 Test reservation access
+test_123_5 Test reservation access based on QOS
test_124_# Testing of AccountingStorageEnforce.
=================================================
diff --git a/testsuite/python/tests/test_123_5.py b/testsuite/python/tests/test_123_5.py
new file mode 100644
index 0000000..7f85081
--- /dev/null
+++ b/testsuite/python/tests/test_123_5.py
@@ -0,0 +1,103 @@
+############################################################################
+# Copyright (C) SchedMD LLC.
+############################################################################
+import os
+import pytest
+import atf
+
+test_name = os.path.splitext(os.path.basename(__file__))[0]
+qos1 = f"{test_name}_qos"
+acct1 = f"{test_name}_acct"
+res_name = f"{test_name}_resv"
+testuser = atf.properties["test-user"]
+
+
+@pytest.fixture(scope="module", autouse=True)
+def setup():
+ global local_cluster_name
+
+ atf.require_accounting()
+ atf.require_slurm_running()
+ atf.require_config_parameter_includes("AccountingStorageEnforce", "qos")
+ local_cluster_name = atf.get_config_parameter("ClusterName")
+
+ # Create test QOS and User that can use it
+ atf.run_command(
+ f"sacctmgr -i add qos {qos1}",
+ user=atf.properties["slurm-user"],
+ )
+ atf.run_command(
+ f"sacctmgr -i add account {acct1} cluster={local_cluster_name}",
+ user=atf.properties["slurm-user"],
+ )
+ atf.run_command(
+ f"sacctmgr -i add user {testuser} cluster={local_cluster_name} account={acct1} qos=normal,{qos1}",
+ user=atf.properties["slurm-user"],
+ )
+
+ atf.run_command(
+ f"sacctmgr -i add user {testuser} cluster={local_cluster_name} wckey={acct1}",
+ user=atf.properties["slurm-user"],
+ )
+ # Create the reservation for QOS test
+ result = atf.run_command(
+ f"scontrol create reservationname={res_name} qos={qos1} start=now duration=1 nodecnt=1",
+ user=atf.properties["slurm-user"],
+ )
+ assert result["exit_code"] == 0, "Couldn't create the reservation!"
+
+ yield
+
+ atf.run_command(
+ f"scontrol delete reservation {res_name}",
+ user=atf.properties["slurm-user"],
+ quiet=True,
+ )
+ atf.run_command(
+ f"sacctmgr -i remove user {testuser} wckey={acct1}",
+ user=atf.properties["slurm-user"],
+ quiet=True,
+ )
+ atf.run_command(
+ f"sacctmgr -i remove user {testuser} account={acct1}",
+ user=atf.properties["slurm-user"],
+ quiet=True,
+ )
+ atf.run_command(
+ f"sacctmgr -i remove account {acct1}",
+ user=atf.properties["slurm-user"],
+ quiet=True,
+ )
+ atf.run_command(
+ f"sacctmgr -i remove qos {qos1}",
+ user=atf.properties["slurm-user"],
+ quiet=True,
+ )
+
+
+def test_reservation_qos():
+ """Test that a reservation created for QOS {qos1} can't be used by atf"""
+
+ # Try to run a job as in the wrong QOS
+ result = atf.run_command(
+ f"srun -N1 --reservation={res_name} --account={acct1} --qos=normal true",
+ user=atf.properties["test-user"],
+ )
+ assert (
+ result["exit_code"] != 0
+ ), "The job should have been denied! {result[exit_code]}"
+ assert (
+ "Problem using reservation" in result["stderr"]
+ ), "The job should have been denied!"
+
+ # Try to run a job as in the correct QOS
+ result = atf.run_command(
+ f"srun -N1 --reservation={res_name} --account={acct1} --qos=normal,{qos1} true",
+ user=atf.properties["test-user"],
+ )
+ assert (
+ result["exit_code"] == 0
+ ), "ExitCode wasn't 0. The job should not have been denied!"
+ assert (
+ "Problem using reservation" not in result["stderr"]
+ ), "The job should not have been denied for user!"