How to specify dynamic "Environment" variables in a systemd unit file?

This $(ls -d...) does not work in a systemd unit file:

[Service]
Type=forking
Environment="ORACLE_HOME=$(ls -d /usr/lib/oracle/*/client64 | sort -rV | head -n1)"
Environment="TNS_ADMIN=$(ls -d /usr/lib/oracle/*/client64/lib/network/admin | sort -rV | head -n1)"

I want to avoid hard-coding the Oracle client version (at the moment 19.19), to simplify updates. When I install a new Oracle client, I don’t want to have to modify the systemd unit file.

How can I achieve that? I use RHEL9 if that matters.

Asked By: Sybil

||

Most common methods:

  • Use an EnvironmentFile= that is generated on the fly, e.g. via ExecStartPre= that calls a simple script. Current systemd versions will re-read EnvironmentFile before each exec to allow this to work. (/run is a good location for the temporary file.)

    This is the simplest method, as the script only needs to write out the KEY="value" lines.

  • Use a systemd generator which dynamically writes unit files at /run/systemd every time configuration is (re)loaded. The generator could be a shell script, as long as it’s limited to local filesystem access.

    Generators can be placed in /etc/systemd/system-generators/; they will be run during every boot and during every "systemctl daemon-reload", getting the output path as $1. They will run literally before any units have started, so they must not expect network or anything else to be up.

    This is the most flexible method, as any unit option can be specified dynamically – not just environment but other things like WorkingDirectory=. (The generator doesn’t need to create a whole unit; it can extend existing units in the usual way, by creating $1/oracle.service.d/environ.conf or similar.)

Other methods:

  • Use an instanced service unit oracle@.service which uses %i to fill in the version. You will still need to disable "oracle@old" and enable "oracle@new", but it saves you opening the text editor. (And it also makes it easy to quickly roll back to the old version just by starting the correct unit.) I think you can have an Alias=oracle.service so that enabling an instance will automatically map it to the shorter name.

  • Use a wrapper shell-script that sets the variables and execs the actual program. Yes, the exec is important. (Also, use SyslogIdentifier= to prevent the script’s name from showing your journalctl output.) Wrapper scripts are usually discouraged, but that’s typically because they do things that can trivially be done from the .service, which is not 100% the case here.

Avoid systemctl set-environment as it is global – the variables will become available to all services started after that point, whether they want them or not.

Answered By: u1686_grawity
Categories: Answers Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.