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

1# Copyright 2026 CNRS 

2# This software is distributed under the terms of the CeCILL-C free software license. 

3 

4"""Implement a mock of the scancel command of slurm. 

5 

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""" 

10 

11import argparse 

12import getpass 

13import os 

14import signal 

15 

16import numpy as np 

17 

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 

27 

28 

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) 

109 

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) 

115 

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() 

118 

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) 

127 

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) 

130 

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) 

135 

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 

141 

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) 

151 

152 

153if __name__ == "__main__": 153 ↛ 154line 153 didn't jump to line 154 because the condition on line 153 was never true

154 main()