-
Notifications
You must be signed in to change notification settings - Fork 0
Commands and Environment
This page documents how a job's command is invoked (shell vs. direct exec), how
its environment is constructed from environment, env_file, and inherited
defaults, and how privilege switching with user/group works. For schedule
syntax see Schedules and Timezones; for how defaults
merge into jobs see Includes, Defaults, and Multi-File Config.
These options are members of each job (and may also be set in a defaults block).
Types and defaults are taken from the strictyaml schema and DEFAULT_CONFIG.
| Option | Type | Default | Description |
|---|---|---|---|
command |
Str or Seq(Str)
|
required | The program to run. A string is run through a shell (on Windows, with the default empty shell, through the native command processor cmd.exe via %ComSpec%); a list is executed directly with no shell on every platform. |
shell |
Str |
/bin/sh (POSIX) / empty (Windows) |
Shell used when command is a string. The default is platform-specific: /bin/sh on POSIX, empty on Windows (an empty default routes a string command through the native command processor %ComSpec% / cmd.exe). To use PowerShell or another interpreter, set shell: explicitly, or pass command as a list (which bypasses the shell on every platform). See Running on Windows. |
environment |
Seq(Map({key, value})) |
[] |
Environment variables (each an item with key and value, both Str) added to the subprocess environment. |
env_file |
Str |
None |
Path to a KEY=VALUE file whose variables are merged into environment. |
user |
Str or Int
|
unset | User (login name or numeric uid) to run the subprocess as. POSIX-only; a job setting it raises a configuration error on Windows (see Running on Windows). |
group |
Str or Int
|
unset | Group (group name or numeric gid) to run the subprocess as. POSIX-only; a job setting it raises a configuration error on Windows (see Running on Windows). |
command is required on every job. shell has a platform-specific
schema/DEFAULT_CONFIG default: /bin/sh on POSIX and an empty string on
Windows (DEFAULT_SHELL in yacron2/platform.py), which makes a string command
run via cmd.exe. environment defaults to an empty list. env_file,
user, and group are optional (Opt(...) in the schema) and unset by default;
environment and env_file are also inheritable via defaults, but user and
group are job-only fields in the schema (they appear in _job_defaults_common,
so they are technically accepted in defaults, but resolution and the
root-required check happen per job).
command may be either a string or a list of strings, and the form determines
how the process is launched (RunningJob.start in yacron2/job.py):
-
String: run through a shell.
- If
shellis set, yacron2 launchesasyncio.create_subprocess_execwith argv[shell, "-c", command]. With the defaultshellon POSIX, that is["/bin/sh", "-c", command]. - If
shellis falsy, yacron2 instead usesasyncio.create_subprocess_shellwith the bare command string. On POSIX the default/bin/shmakes theexec-with--cpath the one that runs; on Windows the defaultshellis empty (DEFAULT_SHELLinyacron2/platform.py), so thecreate_subprocess_shellpath is the default: the command is handed to the native command processorcmd.exevia%ComSpec%. Settingshell:explicitly on Windows takes theexec-with--cpath with that interpreter. See Running on Windows.
- If
-
List: executed directly with
asyncio.create_subprocess_exec, with no shell involved. The argv is taken verbatim from the list; no word splitting, globbing, quoting, or$VARexpansion is performed.
In all cases the argv elements are encoded to bytes (c.encode()) before the
subprocess is created.
jobs:
# string form: run via /bin/bash -c "..."
- name: via-shell
command: echo "$HOME" && date
shell: /bin/bash
schedule: "*/5 * * * *"
# list form: executed directly, no shell, no variable expansion
- name: direct-exec
command:
- echo
- foobar
schedule: "*/5 * * * *"If the process cannot be launched at all (for example, a list-form command
whose executable does not exist, raising FileNotFoundError, or a
SubprocessError/UnicodeEncodeError), the launch error is logged,
start_failed is set, and the run is treated as a normal job failure with exit
code 127 rather than crashing the scheduler. See
Failure Detection and Retries.
environment is a list of {key, value} maps; both key and value are
strings in the schema. When environment is non-empty, the subprocess
environment is built from the full current process environment
(dict(os.environ)), the PyInstaller fixup is applied (see below), and then each
configured variable is set/overwritten by key:
env = dict(os.environ)
fixup_pyinstaller_env(env)
for envvar in self.config.environment:
env[envvar["key"]] = envvar["value"]If environment is empty (the default) and there is no env_file, no env
is passed to the subprocess, so it inherits yacron2's environment unchanged.
jobs:
- name: with-env
command: printenv PATH
schedule: "*/5 * * * *"
environment:
- key: PATH
value: /bin:/usr/binWhen the yacron2.job module is imported, if HOSTNAME is not already present
in os.environ, it is set to socket.gethostname():
if "HOSTNAME" not in os.environ:
os.environ["HOSTNAME"] = gethostname()Because this mutates the process environment, HOSTNAME is therefore present in
the inherited environment of every job. Note that the reporting templates'
environment variable is the constructed subprocess env dict (self.env),
which is only populated when the job has a non-empty environment or an
env_file; for a job with neither, environment is None in templates and
{{ environment.HOSTNAME }} renders empty. See
Reporting (Mail, Sentry, Shell, Webhook).
env_file names a file of KEY=VALUE lines. Parsing is done by
parse_environment_file in yacron2/config.py:
- The file is opened as UTF-8.
- Each line is stripped of surrounding spaces and a trailing newline.
- Lines beginning with
#and blank lines are ignored. - A line without an
=raisesConfigError("Invalid line in env_file: ..."). - Each remaining line is split on the first
=into key and value; both key and value are then space-stripped. There is no quote handling and no#inline-comment handling beyond whole-line comments. -
parse_environment_fileitself raises a bareOSErrorif the file cannot be opened; the caller_merge_env_filewraps that asConfigError("Could not load env_file: ...").
When env_file is set, it is merged at config-parse time (_merge_env_file):
the file is parsed into a dict, then the job's own environment entries are
applied on top, so environment overrides env_file per key. The merged
result replaces self.environment as a list of {key, value} items, which is
then applied to the subprocess as described in environment
above. (This means that when env_file is set, an env is always passed to the
subprocess even if environment was originally empty.)
jobs:
- name: with-env-file
command: printenv
schedule: "*/5 * * * *"
env_file: /etc/yacron2/job.env
environment:
- key: LOG_LEVEL
value: debug # overrides LOG_LEVEL from job.env, if presentExample env_file contents:
# comment lines and blank lines are ignored
PATH=/bin:/usr/bin
LOG_LEVEL=info
environment set in a defaults block merges into each job by key, not by list
concatenation. In mergedicts (yacron2/config.py), the environment list is
special-cased: the default's entries and the job's entries are folded into a
single key-to-value mapping, with the job's value winning on conflict, then
re-expanded to a {key, value} list. The result has no duplicate keys: a
job variable overrides the same-named default rather than appearing twice.
defaults:
environment:
- key: PATH
value: /bin:/usr/bin
- key: LANG
value: C
jobs:
- name: job-a
command: printenv
schedule: "*/5 * * * *"
environment:
- key: PATH
value: /usr/local/bin:/bin # overrides the default PATH
# LANG=C is inherited from defaultsThe precedence chain for a variable is therefore: inherited
process environment (including injected HOSTNAME) < env_file < merged
environment (defaults then job, job winning). See
Includes, Defaults, and Multi-File Config for how
defaults and includes are merged overall.
user and group request that the subprocess run under a different identity.
Resolution happens in JobConfig._resolve_user_group (yacron2/config.py):
Windows: this whole feature is POSIX-only. Windows has no setuid/setgid model, so a job with
userorgroupset raises the configuration errorJob <name>: changing user/group is not supported on Windows(config.py_resolve_user_group). The Root requirement, Resolution rules, and Demotion ordering below all apply to POSIX only. See Running on Windows.
-
useras a name (Str): looked up withgetpwnam. Setsuidfrompw_uid,gidfrompw_gid(the user's primary group), and the resolved login name (pw_name). A missing user raisesConfigError("User not found: ..."). -
useras a number (Int):uidis set to the number directly. yacron2 additionally looks the uid up withgetpwuidto derive the user's primary gid and login name; ifgroupwas not given, the derived primary gid is used (so a numericuserwithoutgroupdoes not silently keep yacron2's gid 0). If the uid is not in the passwd database, no login name or derived gid is available (and that is not an error here). -
groupas a name (Str): looked up withgetgrnam;gidset fromgr_gid. A missing group raisesConfigError("Group not found: ..."). -
groupas a number (Int):gidis set to the number directly. - If only
useris given, the group defaults to the main group of that user. An explicitgroupoverrides any gid derived fromuser.
The resolved login name (username) matters for supplementary-group handling in
_demote (below); it is None when the user is unknown.
If, after resolution, either uid or gid is set and the yacron2 process is not
running as root (os.geteuid() != 0), config parsing fails with:
Job <name> wants to change user or group, but yacron2 is not running as superuser
On POSIX, any use of user or group therefore requires yacron2 to run as
root. yacron2 needs no special privileges otherwise; user/group switching is
the only feature that requires root. (On Windows user/group are rejected
outright with a configuration error, so this root requirement is a POSIX-only
statement; see the Windows note above.)
jobs:
- name: as-www-data
command: id
schedule:
minute: "*"
captureStderr: true
user: www-data # group defaults to www-data's primary groupWhen uid or gid is set, start passes preexec_fn=self._demote, which runs
in the child process after fork while still privileged. The order is
deliberate and is required for safety:
-
Supplementary groups first. If both a login name and a gid are known,
os.initgroups(username, gid)gives the child exactly the target user's supplementary groups. Otherwiseos.setgroups([])drops all supplementary groups. A failure raisesRuntimeError("setgroups/initgroups: ..."). -
Primary gid next. If
gidis set,os.setgid(gid). A failure raisesRuntimeError("setgid: ..."). -
uid last. If
uidis set,os.setuid(uid). A failure raisesRuntimeError("setuid: ...").
Supplementary groups and the gid must be changed before setuid, because
once the process drops root via setuid it can no longer call
setgroups/setgid. Performing them in the other order would leave the child
holding root's supplementary group memberships (the classic
"forgot setgroups() before setuid()" privilege-escalation bug).
fixup_pyinstaller_env is applied to the subprocess environment (only when an
env is being constructed, i.e. when environment/env_file produced
variables). It only does anything when running as a frozen PyInstaller binary
(getattr(sys, "frozen", False)):
for env_var in "LD_LIBRARY_PATH", "LIBPATH":
env[env_var] = env.get(f"{env_var}_ORIG", "")PyInstaller's bootloader overwrites LD_LIBRARY_PATH and LIBPATH so the
bundled binary can find its own libraries, saving the caller's original values in
LD_LIBRARY_PATH_ORIG/LIBPATH_ORIG. This fixup restores those originals (or
empties the variable if there was no _ORIG) for the subprocess, so a child
process does not inherit the frozen interpreter's library paths. See
Production and Container Deployment for the frozen-binary
build. (Because the fixup is only applied when an env is constructed, jobs with
no environment and no env_file inherit the process environment as-is,
including any PyInstaller-clobbered values.)
This wiki documents yacron2. See the README and the changelog.
yacron2 is a fork of gjcarneiro/yacron.
- Getting Started
- Configuration
- Job Behavior
- Integrations
- Reference and Development