Coverage for src/mockslurm/mock_scancel.py: 63%
53 statements
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-07 13:34 +0000
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-07 13:34 +0000
1# Copyright 2026 CNRS
2# This software is distributed under the terms of the CeCILL-C free software license.
4"""Implement a mock of the scancel command of slurm.
6The jobs to cancel are found by querying the database for job IDs or job names,
7filtering the jobs that are already completed (return code != default value),
8then the signal to transmit (default SIGKILL) is send.
9"""
11import argparse
12import getpass
13import os
14import signal
16import numpy as np
18from mockslurm.process_db import (
19 JobState,
20 find_db_file,
21 get_db,
22 get_db_file_handle,
23 get_filtered_DB_mask,
24 update_db_value,
25)
26from mockslurm.utils import filter_dict_from_args
29def main():
30 parser = argparse.ArgumentParser(
31 description="Slurm scancel mock", formatter_class=argparse.ArgumentDefaultsHelpFormatter
32 )
33 user_group = parser.add_mutually_exclusive_group()
34 parser.add_argument(
35 "--account",
36 "-A",
37 type=str,
38 dest="ACCOUNT",
39 help="Restrict the scancel operation to jobs under this charge account",
40 )
41 parser.add_argument(
42 "--jobname", "-n", type=str, dest="NAME", help="Restrict the scancel operation to jobs with this job name"
43 )
44 user_group.add_argument(
45 "--me",
46 action="store_true",
47 dest="me",
48 help="Restrict the scancel operation to jobs owned by the current account",
49 )
50 parser.add_argument(
51 "--nodelist",
52 "-w",
53 type=str,
54 dest="NODELIST",
55 help="Cancel any jobs using any of the given hosts. "
56 "The list may be specified as a comma-separated list of hosts, a range of hosts "
57 "(host[1-5,7,...] for example)",
58 )
59 parser.add_argument(
60 "--partition",
61 "-P",
62 type=str,
63 dest="PARTITION",
64 help="Restrict the scancel operation to jobs in this partition",
65 )
66 parser.add_argument(
67 "--quiet",
68 "-Q",
69 type=str,
70 dest="quiet",
71 help="Do not report an error if the specified job is already completed",
72 )
73 parser.add_argument(
74 "--reservation",
75 "-R",
76 type=str,
77 dest="RESERVATION",
78 help="Restrict the scancel operation to jobs with this reservation name",
79 )
80 parser.add_argument(
81 "--signal",
82 "-s",
83 type=str,
84 dest="signal",
85 default="SIGKILL",
86 help="The name or number of the signal to send. If this option is not used the specified job or step will be terminated",
87 )
88 parser.add_argument(
89 "--user",
90 "-u",
91 type=str,
92 dest="USER",
93 help="Restrict the scancel operation to jobs owned by the given user",
94 )
95 parser.add_argument("jobids", type=int, nargs="*", help="The Slurm job ID to be signaled")
96 args = parser.parse_args()
97 if ( 97 ↛ 107line 97 didn't jump to line 107 because the condition on line 97 was never true
98 not args.jobids
99 and args.ACCOUNT is None
100 and args.NAME is None
101 and args.me is False
102 and args.NODELIST is None
103 and args.PARTITION is None
104 and args.RESERVATION is None
105 and args.USER is None
106 ):
107 print("scancel: error: No job identification provided")
108 exit(1)
110 if args.jobids: 110 ↛ 111line 110 didn't jump to line 111 because the condition on line 110 was never true
111 for id in args.jobids:
112 if id < 0:
113 print(f"scancel: error: Invalid job id {id}")
114 exit(1)
116 if args.me is not False: 116 ↛ 117line 116 didn't jump to line 117 because the condition on line 116 was never true
117 args.ACCOUNT = getpass.getuser()
119 if args.signal in [signal.name for signal in signal.Signals]: 119 ↛ 122line 119 didn't jump to line 122 because the condition on line 119 was always true
120 args.signal = signal.Signals[args.signal]
121 else:
122 try:
123 args.signal = int(args.signal)
124 except:
125 print(f"Unknown job signal: {args.signal}")
126 exit(1)
128 # Transform the arguments values into a map {field: value, field2: [values], etc...} for filtering DB
129 field_filter_values = filter_dict_from_args(args)
131 with get_db_file_handle(find_db_file()) as db_file:
132 db = get_db(db_file)
133 if db.shape[0] == 0: # db is empty: no jobs to cancel 133 ↛ 134line 133 didn't jump to line 134 because the condition on line 133 was never true
134 exit(0)
136 # Get mask to select DB rows
137 mask = get_filtered_DB_mask(db_file, field_filter_values)
138 # filter job IDs if some were specified
139 if args.jobids: 139 ↛ 140line 139 didn't jump to line 140 because the condition on line 139 was never true
140 mask[args.jobids] = True
142 job_indices = np.nonzero(mask)[0]
143 # Send signal
144 for idx, job in zip(job_indices, db[mask]):
145 if job["STATE"] == JobState.RUNNING: # job is started, we can kill it 145 ↛ 146line 145 didn't jump to line 146 because the condition on line 145 was never true
146 os.kill(job["PID"], args.signal)
147 # set STATE to stopped immediately, actual exit STATE will be updated with exit code
148 # in mock of sbatch if job was running
149 # TODO: if signal wasn't meant to kill job, STATE is wrong ?
150 update_db_value(db_file, idx, key="STATE", value=JobState.CANCELLED)
153if __name__ == "__main__": 153 ↛ 154line 153 didn't jump to line 154 because the condition on line 153 was never true
154 main()