blob: 89af779e9d02fb1f9e65ee3de8ef45877deec3fa [file] [edit]
--[[
-- Example switch.lua for the switch/lua plugin.
--
-- Install as <SlurmConfDir>/switch.lua and set SwitchType=switch/lua.
--
-- This is a manual-test harness: it exercises every hook the plugin calls
-- and the main return conventions, and logs each call so the ordering is
-- visible in the slurmctld and slurmd logs. Grep the logs for "switchlua:".
--
-- job_start and job_complete only run for a job whose allocation requests
-- --network with a non-empty value. The slurmstepd hooks do not run for the
-- steps of any other job, nor for a job that job_start declined.
--
-- Behavior is driven from the job's --network so each path can be triggered
-- without editing this file, once ALLOW_DIRECTIVES below is set to true.
-- Leave it false anywhere but a test cluster: the directives let any user
-- fail setups and drain nodes.
--
-- sbatch --network=switchlua:skip ...
-- srun --network=switchlua:preinit_warn+init_fail ...
--
-- A directive that fails the setup (defer, start_crash) requeues a batch job
-- that can be requeued, and the setup then fails again on every run, with no
-- limit. Submit those with sbatch --no-requeue, or run them with srun.
--
-- Directives (combine with '+'):
--
-- slurmctld side
-- skip job_start returns no jobinfo; the plugin declines the
-- job, switch_jobinfo stays NULL, no stepinfo is built,
-- job_complete is not called for it, and the
-- slurmstepd hooks do not run for its steps
-- defer job_start returns failure; the job is requeued if it
-- can be (a batch job that allows requeue), otherwise
-- cancelled with the reason "switch setup failed". The
-- nodes are not drained.
-- start_crash raise a Lua error inside job_start; this is a failed
-- setup, handled as for "defer"
-- complete_again job_complete returns slurm.EAGAIN every time. With
-- BLOCK_JOB_COMPLETE true the controller retries up to
-- SlurmctldParameters=switch_retries times and then
-- drains the nodes; with it false the first EAGAIN is
-- already a failure and the nodes are drained.
-- complete_crash raise a Lua error inside job_complete; this is a
-- failed teardown, so the job's nodes are DRAINED
--
-- slurmstepd side (per step, per node)
-- preinit_warn preinit returns 1 -> logged, step still runs
-- preinit_fail preinit returns -1 -> step fails, srun reports
-- "Error configuring interconnect"
-- preinit_crash raise a Lua error inside preinit
-- init_warn init returns 1 -> logged, step still runs
-- init_fail init returns -1 -> step fails
-- init_crash raise a Lua error inside init
-- noenv init returns no environment table
-- postfini_warn postfini returns 1 -> logged, node NOT drained
-- postfini_drain postfini returns -1 -> node IS DRAINED. Deliberate.
-- postfini_crash raise a Lua error inside postfini
--
-- The slurmstepd "*_crash" directives verify fail-open: a Lua error is
-- logged and the step proceeds. That is deliberately weaker than an explicit
-- negative return, which is enforced. The slurmctld hooks are not fail-open:
-- a Lua error in job_start fails the setup and a Lua error in job_complete
-- fails the teardown.
--
-- Every call of job_start and job_complete runs in its own slurmscriptd
-- child, on a Lua state loaded from this file for that call alone.
-- Module-level variables are therefore not shared between those calls, nor
-- with the other hooks: keep anything that must survive in the fabric
-- itself. The jobinfo that job_start returns reaches the slurmstepd hooks as
-- their stepinfo, and job_complete as its jobinfo.
--
-- The child is killed at SlurmctldParameters=switch_timeout, and whatever
-- it started that is still running in its process group is killed when it
-- ends. Its stdin, stdout and stderr are /dev/null, so log with
-- slurm.log_*() or print(). Those lines go to SlurmctldLogFile, or to
-- syslog if that is not set, never to the terminal of slurmctld -D. With
-- SlurmctldLogFile=/dev/stdout or /dev/stderr they never reach slurmctld's
-- output.
--]]
--[[
-- Flip to true to also call slurm_switch_step_init() for the batch and
-- interactive steps, which then carry the stepinfo as well. preinit and
-- postfini only ever run for normal job steps, and the extern step runs no
-- step hook (like every slurmstepd, it still loads this file and asks
-- slurm_switch_block_job_complete() and slurm_switch_setup_special_steps()
-- when it starts). Asked once, when slurmctld loads the plugin and when each
-- slurmstepd does, so a change takes effect when slurmctld is next restarted
-- or reconfigured and on the nodes that have the new file.
--]]
local SETUP_SPECIAL_STEPS = false
--[[
-- Flip to true to keep a job's nodes in COMPLETING until job_complete has
-- reported, instead of returning them to service while it runs. It is what
-- lets job_complete return slurm.EAGAIN. Asked once, when slurmctld loads
-- the plugin, so a change takes effect when slurmctld is next restarted or
-- reconfigured.
--]]
local BLOCK_JOB_COMPLETE = false
--[[
-- Flip to true, on a test cluster only, to act on the directives in --network
-- described above. While false every job with --network takes the default
-- path.
--]]
local ALLOW_DIRECTIVES = false
local TAG = "switchlua:"
local function log(fmt, ...)
slurm.log_info(TAG .. " " .. fmt, ...)
end
local function logerr(fmt, ...)
slurm.log_error(TAG .. " " .. fmt, ...)
end
--[[ Pull "switchlua:a+b+c" out of a --network string. ]]
local function parse_directives(args)
local set = {}
if not ALLOW_DIRECTIVES or args == nil then
return set
end
local spec = string.match(args, "switchlua:([%w_+]+)")
if spec == nil then
return set
end
for word in string.gmatch(spec, "[%w_]+") do
set[word] = true
end
return set
end
--[[ Rebuild the directive set on the stepd side from the stepinfo dict. ]]
local function directives_from_stepinfo(stepinfo)
if not ALLOW_DIRECTIVES or stepinfo == nil or
stepinfo.directives == nil then
return {}
end
local set = {}
for word in string.gmatch(stepinfo.directives, "[%w_]+") do
set[word] = true
end
return set
end
local function describe_step(step)
if step == nil then
return "<no step>"
end
--[[
-- step.partition is nil on a batch step: the launch request that
-- carries it only exists for normal steps. Only reachable when
-- SETUP_SPECIAL_STEPS is true.
--]]
return string.format("JobId=%s SLUID=%s Partition=%s",
tostring(step.job_id),
tostring(step.sluid),
tostring(step.partition))
end
--[[ ================= slurmctld: whether to hold nodes ================== ]]
function slurm_switch_block_job_complete()
log("block_job_complete(): returning %s", tostring(BLOCK_JOB_COMPLETE))
return BLOCK_JOB_COMPLETE
end
--[[ ================= slurmctld: whether to run everywhere ================ ]]
function slurm_switch_setup_special_steps()
log("setup_special_steps(): returning %s",
tostring(SETUP_SPECIAL_STEPS))
return SETUP_SPECIAL_STEPS
end
--[[ ======================= slurmctld: job lifecycle ====================== ]]
--[[
-- Returns two values: a return code and the jobinfo dictionary. The job does
-- not start until this returns.
--
-- rc == slurm.SUCCESS proceed
-- rc ~= slurm.SUCCESS the setup failed (so does an error, or a return
-- code that is not a number); the job is requeued
-- if it can be, otherwise cancelled with the reason
-- "switch setup failed". The nodes are not drained
-- and job_complete is not called, so undo whatever
-- the setup got through before returning it.
--
-- jobinfo == nil the plugin is not managing this job;
-- switch_jobinfo is cleared
-- jobinfo == table stored on the job record, packed into the job
-- state file, copied into every step's stepinfo,
-- and passed to job_complete
--
-- A setup that takes longer than SlurmctldParameters=switch_timeout seconds
-- fails the job in the same way (with the reason "switch setup timed out")
-- and is killed. As the controller cannot say what it left behind,
-- job_complete is called for the job. In the rare case that the setup still
-- returns slurm.SUCCESS with a jobinfo table, or exits without returning,
-- before it is killed, job_complete is called once more for it, with the
-- job as it was when the setup started and that jobinfo table, or nil if
-- the table could not be saved or the setup exited without returning. Its
-- result is only logged.
--
-- A call that exits without returning (os.exit(), or a function that stops
-- the process) fails the job as a returned failure does, but job_complete
-- is called for it, as for a timed-out setup. A call that dies on a signal
-- is treated as timed out.
--
-- This is called again for a job whose setup was still outstanding when
-- slurmctld restarted, and for a job that is requeued. The controller does
-- not say that it is a repeat, so judge from the fabric what already
-- exists, and make this function safe to repeat.
--]]
function slurm_switch_job_start(job)
local d = parse_directives(job.network)
log("job_start(): JobId=%s User=%s Nodes=%s Network=%s",
tostring(job.job_id), tostring(job.user_name),
tostring(job.nodes), tostring(job.network))
if d.start_crash then
--[[ Deliberate fault: the setup fails, as if rc were not 0. ]]
error("switchlua: deliberate error in job_start")
end
if d.skip then
log("job_start(): declining JobId=%s", tostring(job.job_id))
return slurm.SUCCESS, nil
end
if d.defer then
logerr("job_start(): deferring JobId=%s", tostring(job.job_id))
return slurm.ERROR, nil
end
--[[
-- Everything here round-trips through pack_data()/unpack_data() into
-- the job state file, and is copied into each step's stepinfo. Keep
-- it to strings, numbers, booleans and nested tables.
--]]
local jobinfo = {
directives = tostring(job.network or ""),
job_id = job.job_id,
user_name = tostring(job.user_name),
nodes = tostring(job.nodes or ""),
num_nodes = job.num_nodes,
partition = tostring(job.partition),
--[[ nested table, to prove structure survives the round trip ]]
fabric = {
endpoint = "fabric-mgr.example.invalid:8080",
token = string.format("tok-%s", tostring(job.job_id)),
vlans = { 101, 102, 103 },
},
}
log("job_start(): accepted JobId=%s, jobinfo=%s",
tostring(job.job_id), slurm.to_json(jobinfo))
return slurm.SUCCESS, jobinfo
end
--[[
-- Teardown counterpart. Returns a single numeric rc. It is given the job
-- table and a copy of the jobinfo table that job_start returned, or nil for
-- an abandoned setup (see below), whose leftovers have to be worked out from
-- the fabric.
--
-- rc == slurm.SUCCESS torn down; the job's nodes go back to service
-- rc == slurm.EAGAIN not yet. The nodes stay held and nothing is
-- drained; the controller calls this again for the
-- same job, with the same jobinfo, at its next
-- periodic check, up to
-- SlurmctldParameters=switch_retries times, after
-- which the nodes are drained like any failure. Only
-- honored when slurm_switch_block_job_complete()
-- returns true, because a retry needs the nodes held,
-- or for a job that ended while its setup was still
-- running or whose teardown is run again after a
-- restart, whose nodes are held whatever that
-- returns; otherwise it is a failure. Be ready to be
-- called again after a partial attempt.
-- rc < 0, or an error failure: the job's nodes are drained, unless
-- SlurmctldParameters=switch_drain_on_failure=no
--
-- Any other positive value is only logged, as for the slurmstepd hooks. (It
-- is not so for job_start, where any value but slurm.SUCCESS fails the
-- setup.) Return values deliberately rather than by accident.
--
-- This is called when a job ends if job_start returned a jobinfo table for
-- it. It is also called, with a nil jobinfo, for a job whose setup did not
-- report within SlurmctldParameters=switch_timeout seconds, or exited
-- without returning, because the controller cannot say what such a setup
-- left behind: work out from the fabric whether anything needs undoing. In
-- rare cases a timed-out setup still reports before it is killed; if it
-- returns slurm.SUCCESS with a jobinfo table, or exits without returning,
-- this is called once more for it, with that table (nil if it could not be
-- saved, or if the setup exited without returning), possibly while the
-- first call is still running.
-- Cleaning up after an abandoned setup (timed out, or exited without
-- returning) is up to this hook; slurmctld calls it and acts on its result
-- as for any other teardown. A setup that returns a failure is not torn down
-- here: job_start has to undo its own work before it returns one.
--
-- This is called again for a job whose teardown was holding its nodes (see
-- slurm_switch_block_job_complete() above) when slurmctld restarted, with
-- the nodes still held and the same jobinfo. The controller does not say
-- that it is a repeat, so judge from the fabric what is left to undo, and
-- make this function safe to repeat.
--]]
function slurm_switch_job_complete(job, jobinfo)
local d = parse_directives(job.network)
log("job_complete(): JobId=%s Nodes=%s jobinfo=%s",
tostring(job.job_id), tostring(job.nodes),
jobinfo and slurm.to_json(jobinfo) or "nil")
if d.complete_crash then
error("switchlua: deliberate error in job_complete")
end
if d.complete_again then
logerr("job_complete(): asking to be called again")
return slurm.EAGAIN
end
return slurm.SUCCESS
end
--[[ ======================== slurmstepd: per step ========================= ]]
--[[
-- Runs as root in the stepd job manager, before the proctrack container
-- exists. Paired with postfini: anything started here is outside the
-- container and will not be cleaned up for you.
--
-- 0 success
-- negative step fails with ESLURM_INTERCONNECT_FAILURE
-- positive logged only, step proceeds
--]]
function slurm_switch_step_preinit(step, stepinfo)
local d = directives_from_stepinfo(stepinfo)
log("step_preinit(): %s stepinfo=%s", describe_step(step),
stepinfo and slurm.to_json(stepinfo) or "nil")
if d.preinit_crash then
error("switchlua: deliberate error in step_preinit")
end
if d.preinit_fail then
logerr("step_preinit(): failing the step on purpose")
return -1
end
if d.preinit_warn then
logerr("step_preinit(): reporting non-fatal trouble on purpose")
return 1
end
return slurm.SUCCESS
end
--[[
-- Runs as root in the stepd job manager, inside the container, after
-- spank_init() and before the MPI prefork and the task fork. This is the
-- only hook that can set step-wide environment.
--
-- Returns rc plus a flat table of environment variables:
--
-- FOO = "bar" set FOO=bar
-- FOO = 42 set FOO=42 (numbers are stringified)
-- FOO = true set FOO=1
-- FOO = false unset FOO
--
-- Note that setup_env() runs after this and will overwrite any name Slurm
-- owns, so SLURM_* cannot be overridden here.
--]]
function slurm_switch_step_init(step, stepinfo)
local d = directives_from_stepinfo(stepinfo)
log("step_init(): %s", describe_step(step))
if d.init_crash then
error("switchlua: deliberate error in step_init")
end
if d.init_fail then
logerr("step_init(): failing the step on purpose")
return -1, nil
end
if d.noenv then
log("step_init(): setting no environment")
return slurm.SUCCESS, nil
end
local env = {
SWITCHLUA_JOB_ID = tostring(step.job_id),
SWITCHLUA_SLUID = tostring(step.sluid),
--[[ number, to prove coercion ]]
SWITCHLUA_RANK_HINT = 7,
--[[ boolean true becomes "1" ]]
SWITCHLUA_ENABLED = true,
--[[ boolean false removes the variable ]]
SWITCHLUA_SHOULD_BE_UNSET = false,
}
if stepinfo ~= nil and stepinfo.fabric ~= nil then
env.SWITCHLUA_ENDPOINT = tostring(stepinfo.fabric.endpoint)
env.SWITCHLUA_TOKEN = tostring(stepinfo.fabric.token)
end
if d.init_warn then
logerr("step_init(): reporting non-fatal trouble on purpose")
return 1, env
end
return slurm.SUCCESS, env
end
--[[
-- Runs as root in the stepd job manager after every task is dead and the
-- container has been SIGKILLed. Last chance to release whatever preinit
-- took. Not called for batch, interactive or extern steps.
--
-- 0 success
-- negative the node is DRAINED
-- positive logged only
--]]
function slurm_switch_step_postfini(step, stepinfo)
local d = directives_from_stepinfo(stepinfo)
log("step_postfini(): %s", describe_step(step))
if d.postfini_crash then
error("switchlua: deliberate error in step_postfini")
end
if d.postfini_drain then
logerr("step_postfini(): draining this node on purpose")
return -1
end
if d.postfini_warn then
logerr("step_postfini(): reporting non-fatal trouble on purpose")
return 1
end
return slurm.SUCCESS
end
--[[
-- The chunk's own return value is checked at load time: a number other
-- than slurm.SUCCESS makes the plugin refuse to load the script. Returning
-- nothing counts as slurm.SUCCESS.
--]]
slurm.log_info("switchlua: script loaded")
return slurm.SUCCESS