How To
Summary
There is no bstop command function implementation in LSF python API examples. Use below sample python script to implement bstop command.
Steps
#!/usr/bin/env python3
"""
bstop.py - a Python re-implementation of LSF's bstop built on the
IBM Spectrum LSF Python API (https://github.com/IBMSpectrumComputing/lsf-python-api).
bstop suspends unfinished jobs by sending SIGSTOP (sequential jobs) or
SIGTSTP (parallel jobs). This script finds the matching jobs with
lsb_openjobinfo()/lsb_readjobinfo() and suspends them with lsb_signaljob().
Usage (same shape as the real command):
bstop.py [-a] [-J job_name] [-m host] [-q queue] [-u user|all]
[-C reason] [0] [job_ID ...]
bstop.py 314 suspend job 314
bstop.py -u all suspend the last submitted job in the system
bstop.py -u jsmith 0 suspend all jobs of user jsmith
bstop.py -q night -J "sim*" 0
bstop.py -a suspend all jobs
Extra switches for this script only:
--dry-run show what would be suspended, send nothing
--mock run against a fake in-memory job list (no LSF needed)
Requires: LSF environment sourced (profile.lsf) and pythonlsf installed.
"""
import argparse
import getpass
import signal
import sys
---- LSF constants (values from lsf/lsbatch.h; the SWIG module normally
---- exports them, so we prefer lsf. and fall back to the numbers).
JOB_STAT_PEND = 0x01
JOB_STAT_PSUSP = 0x02
JOB_STAT_RUN = 0x04
JOB_STAT_SSUSP = 0x08
JOB_STAT_USUSP = 0x10
JOB_STAT_EXIT = 0x20
JOB_STAT_DONE = 0x40
SUSPENDED_MASK = JOB_STAT_PSUSP | JOB_STAT_SSUSP | JOB_STAT_USUSP
FINISHED_MASK = JOB_STAT_EXIT | JOB_STAT_DONE
lsb_openjobinfo option bits
_OPEN_FLAGS = {
"ALL_JOB": 0x0001,
"PEND_JOB": 0x0004,
"SUSP_JOB": 0x0008,
"CUR_JOB": 0x0010, # unfinished jobs only
"LAST_JOB": 0x0020, # only the most recently submitted job
"RUN_JOB": 0x0040,
}
def _flag(lsf, name):
return getattr(lsf, name, _OPEN_FLAGS[name]) if lsf else _OPEN_FLAGS[name]
--------------------------------------------------------------------------
Backends
--------------------------------------------------------------------------
class Job:
"""Minimal view of a jobInfoEnt."""
def __init__(self, job_id, status, user, name="", queue="", hosts=(), nproc=1):
self.job_id = job_id
self.status = status
self.user = user
self.name = name
self.queue = queue
self.hosts = list(hosts)
self.nproc = nproc
@property
def parallel(self):
return self.nproc > 1
class LsfBackend:
"""Talks to a real LSF cluster through pythonlsf."""
def __init__(self):
try:
from pythonlsf import lsf
except ImportError:
sys.exit("bstop.py: cannot import pythonlsf. Source profile.lsf and "
"install lsf-python-api, or use --mock.")
self.lsf = lsf
if lsf.lsb_init("bstop.py") != 0:
sys.exit("bstop.py: failed to initialise LSF batch library: "
+ self._err())
def _err(self):
sysmsg = getattr(self.lsf, "lsb_sysmsg", None)
return sysmsg() if sysmsg else "unknown LSF error"
def find_jobs(self, job_id, job_name, user, queue, host, last_only):
lsf = self.lsf
options = _flag(lsf, "CUR_JOB") # unfinished jobs only
if last_only:
options |= _flag(lsf, "LAST_JOB")
count = lsf.lsb_openjobinfo(job_id, job_name or "", user or "",
queue or "", host or "", options)
if count < 0:
sys.exit("bstop.py: " + self._err())
jobs = []
more = lsf.new_intp()
lsf.intp_assign(more, count)
try:
for _ in range(count):
info = lsf.lsb_readjobinfo(more)
if info is None:
break
submit = getattr(info, "submit", None)
nproc = getattr(submit, "numProcessors", 1) or 1
# exHosts is a raw char** in the SWIG wrapper and is not
# indexable from Python; host filtering is done by LSF itself
# (hostName arg of lsb_openjobinfo), so this list is optional.
hosts = []
try:
n_ex = getattr(info, "numExHosts", 0)
ex_hosts = getattr(info, "exHosts", None)
if ex_hosts is not None and n_ex:
hosts = [ex_hosts[i] for i in range(n_ex)]
except (TypeError, IndexError):
hosts = []
jobs.append(Job(
job_id=info.jobId,
status=info.status,
user=info.user,
name=getattr(submit, "jobName", "") or getattr(info, "jName", ""),
queue=getattr(submit, "queue", ""),
hosts=hosts,
nproc=nproc,
))
finally:
lsf.lsb_closejobinfo() # always close to avoid leaking the connection
return jobs
def signal_job(self, job_id, sig):
"""Returns (ok, error_message)."""
if self.lsf.lsb_signaljob(job_id, int(sig)) == 0:
return True, ""
return False, self._err()
class MockBackend:
"""In-memory fake cluster so the script can be tried without LSF."""
def __init__(self):
me = getpass.getuser()
self.jobs = [
Job(101, JOB_STAT_RUN, me, "sim_a", "normal", ["hostA"], 1),
Job(102, JOB_STAT_RUN, me, "sim_b", "night", ["hostB"], 8),
Job(103, JOB_STAT_PEND, me, "post", "normal", [], 1),
Job(104, JOB_STAT_USUSP, me, "held", "normal", ["hostA"], 1),
Job(105, JOB_STAT_RUN, "jsmith", "other", "night", ["hostC"], 1),
Job(106, JOB_STAT_DONE, me, "old", "normal", ["hostA"], 1),
]
def find_jobs(self, job_id, job_name, user, queue, host, last_only):
import fnmatch
me = getpass.getuser()
out = []
for j in self.jobs:
if j.status & FINISHED_MASK:
continue
if job_id and j.job_id != job_id:
continue
if job_name and not fnmatch.fnmatch(j.name, job_name):
continue
if user != "all" and j.user != (user or me):
if not job_id: # explicit job IDs may belong to anyone
continue
if queue and j.queue != queue:
continue
if host and host not in j.hosts:
continue
out.append(j)
if last_only and out:
out = [max(out, key=lambda j: j.job_id)]
return out
def signal_job(self, job_id, sig):
for j in self.jobs:
if j.job_id == job_id:
if j.status & (JOB_STAT_RUN | JOB_STAT_PEND):
j.status = JOB_STAT_USUSP if j.status & JOB_STAT_RUN else JOB_STAT_PSUSP
return True, ""
return False, "No matching job found"
--------------------------------------------------------------------------
bstop logic
--------------------------------------------------------------------------
def parse_args(argv):
p = argparse.ArgumentParser(
prog="bstop.py",
description="Suspend unfinished LSF jobs (SIGSTOP for sequential, "
"SIGTSTP for parallel jobs).",
)
p.add_argument("-a", action="store_true", help="suspend all jobs")
p.add_argument("-J", dest="job_name", metavar="job_name",
help="only jobs with this name (wildcard * allowed)")
p.add_argument("-m", dest="host", metavar="host_name",
help="only jobs dispatched to this host")
p.add_argument("-q", dest="queue", metavar="queue_name",
help="only jobs in this queue")
p.add_argument("-u", dest="user", metavar="user_name|all",
help="only jobs of this user, or 'all'")
p.add_argument("-C", dest="reason", metavar="suspend_reason",
help="suspend reason (max 4095 chars)")
p.add_argument("-V", action="store_true", help="print version and exit")
p.add_argument("--dry-run", action="store_true",
help="list the jobs that would be suspended, send nothing")
p.add_argument("--mock", action="store_true",
help="use a fake in-memory cluster instead of real LSF")
p.add_argument("job_ids", nargs="*", metavar="job_ID",
help="job IDs to suspend; 0 means every job matching the filters")
return p.parse_args(argv)
def suspend_signal(job):
return signal.SIGTSTP if job.parallel else signal.SIGSTOP
def main(argv=None):
args = parse_args(argv)
if args.V:
print("bstop.py - Python simulation of LSF bstop (lsf-python-api)")
return 0
if args.reason and len(args.reason) > 4095:
print("bstop.py: suspend reason is longer than 4095 characters", file=sys.stderr)
return 1
ids = []
for token in args.job_ids:
try:
ids.append(int(token))
except ValueError:
print(f"bstop.py: Illegal job ID: {token}", file=sys.stderr)
return 1
has_filter = any([args.job_name, args.host, args.queue, args.user])
if not (ids or args.a or has_filter):
print("bstop.py: specify a job ID or one of -a, -J, -m, -q, -u",
file=sys.stderr)
return 1
backend = MockBackend() if args.mock else LsfBackend()
if args.reason and not args.mock:
print("bstop.py: warning: -C reason is not passed through "
"lsb_signaljob and will be ignored", file=sys.stderr)
# Work out the list of (jobId) queries.
# explicit IDs (not 0) -> one query per ID
# 0 or -a -> every job that matches the filters
# filters only, no 0/-a -> only the last submitted matching job
all_matching = args.a or 0 in ids
explicit = [i for i in ids if i != 0]
user = "all" if args.a else args.user
jobs = []
if explicit:
for jid in explicit:
found = backend.find_jobs(jid, args.job_name, "all", args.queue,
args.host, last_only=False)
if not found:
print(f"Job <{jid}>: No matching job found", file=sys.stderr)
jobs.extend(found)
if all_matching or not explicit:
jobs.extend(backend.find_jobs(0, args.job_name, user, args.queue,
args.host, last_only=not all_matching))
if not jobs and not explicit:
print("No matching job found", file=sys.stderr)
return 1
seen, rc = set(), 0
for job in jobs:
if job.job_id in seen:
continue
seen.add(job.job_id)
if job.status & FINISHED_MASK:
print(f"Job <{job.job_id}>: Job has already finished", file=sys.stderr)
rc = 1
continue
if job.status & JOB_STAT_USUSP:
print(f"Job <{job.job_id}>: Job has already been stopped", file=sys.stderr)
rc = 1
continue
sig = suspend_signal(job)
if args.dry_run:
print(f"Job <{job.job_id}> would be stopped ({sig.name})")
continue
ok, err = backend.signal_job(job.job_id, sig)
if ok:
print(f"Job <{job.job_id}> is being stopped")
else:
print(f"Job <{job.job_id}>: {err}", file=sys.stderr)
rc = 1
return rc
if name == "main":
sys.exit(main())
Test result
[egoadmin@gen2 ~]$ bsub sleep 30
Job <418> is submitted to default queue <normal>.
[egoadmin@gen2 examples]$ python3 bstop.py 418
Job <418> is being stopped
[egoadmin@gen2 ~]$ bjobs
JOBID USER STAT QUEUE FROM_HOST EXEC_HOST JOB_NAME SUBMIT_TIME
418 egoadmi USUSP normal gen2.fyre.i gen3.fyre.i sleep 30 Nov 11 15:41
Document Location
Worldwide
[{"Type":"MASTER","Line of Business":{"code":"LOB77","label":"Automation Platform"},"Business Unit":{"code":"BU048","label":"IBM Software"},"Product":{"code":"SSWRJV","label":"IBM Spectrum LSF"},"ARM Category":[{"code":"a8m0z000000cw9WAAQ","label":"Python"}],"ARM Case Number":"","Platform":[{"code":"PF025","label":"Platform Independent"}],"Version":"10.1.0"}]
Was this topic helpful?
Document Information
Modified date:
11 November 2026
UID
ibm17296284