Run a remote command from a template *file*

I need to run a few lines of shell on a remote system, and I find YAML more than awkward to use for this and would prefer to put this into a foo.sh.j2 file instead.

Now I could do the dance:

  • stat to test if a remote directory exists
  • create a remote temporary path if said directory does not exist
  • run the template module with foo.sh.j2 into the remote temporary path if said directory does not exist
  • run sh path/foo.sh with the command module if said directory does not exist, creating said directory
  • remove the remote temporary path if said directory did not exist

This is quite the overhead for something that ideally does get executed only once.

What I am really looking for is something like:

ansible.builtin.shell:
  cmd_from_template: foo.sh.j2
  creates: /path/to/said/directory

The foo.sh.j2 file needs to be evaluated locally, as is normal for templates; this is not a request to do that remotely.

You can use the template lookup to build a string from a template using the template lookup:

- name: run shell command from template
  ansible.builtin.shell:
    cmd: '{{ lookup("template", "foo.sh.j2") }}'
    creates: /path/to/said/directory

Please keep in mind this could be dangerous if the template is from an untrusted source, that may be a moot point if it’s stored in the same way as the playbook but if sourced elsewhere this could lead to an injection attack.

Ah, like that; yes, that should work, I’ll play with it around a bit before I check it as solution, thanks.

The template module evaluates on the control node but using inventory context of the target node, generates the result and transfers it to the remote node for storage which also lets you specify permissions and file attributes. You can then call the “shell” or “command” module as necessary to execute your generated script. It’s two tasks in this fashion, but it depends on what level of detail you want to achieve.

Regardless of which method, you still need the additional “file” module task to delete the file on the target node.

I also assume you’re using the ssh connection type for all this. If you’re using something else, then this may not apply.

No, you can run a script from stdin or from command line, you do not need any file.

I was just missing the building blocks to template either. I’ve not yet come to it, but what jborean wrote may just be it.

I’m really curious about why you want to use a template rather than in-line shell text.
You do you; it’s great you’ve got a solution that you like. I’ve done a lot (too much?) in-line shell, and maybe there’s something I’ve not considered. So I’m curious.

Because I want to write it as separate file, in my text editor, with syntax highlighting for shell, and avoiding escape hassles, instead of as one huge line for YAML with tricky escapes.

(I loathe YAML.)

I can’t speak to what @mirabilos is doing, but I use a J2 template when I’m running the CLI scripts on Check Point firewalls and management servers.

In the J2 template, I add pre-/post- type of boilerplate things. Such things include “is this running on a singe gateway or cluster member? if it’s a cluster member, is this the ACTIVE or STANDBY cluster member?”; sometimes I may want this script to run only on the ACTIVE gateway (for some of current-state info-fetching command which is only relevant on the ACTIVE gateway), and sometimes I want the script to run on both gateways (for some general configuration command). Having the J2 template do the “ugly” and highly-repetitive parts is nice, so that my playbook task (and the kickoff script I use) only sends the command itself. My kickoff script allows me to provide a runtime flag (-a) which passes an extra-vars variable to the playbook, and read by the J2 template, to choose “one gateway or all gateways of the cluster”. Plus, I the human don’t have to do any thinking beyond that one J2 template. :slight_smile:

So you can extend the same mnemonic to any system that’s a member of group (K8S pods, load-balanced web heads, replicated databases, whatever).

If you ever have a problem or need a revision, you can edit the J2 template, as a changeable artifact, rather than edit the playbook, which should be a fixed and replicable structure. Think “separation of concerns”: let the variable data input be the driver, not the system itself. Much like an automobile: the driver is the variant; the vehicle is the fixed-frame device.

I’m also a very very lazy person, so I work hard now (once) to be lazy later. :smiley:

Thanks, this indeed worked. I wrote a roles/schroot/templates/init.j2 with something along the lines of…

#!/bin/sh
 
# just something to test this procedure
 
{
        printf '%s' $#
        printf ' <%s>' "$0" "$@"
        echo
        #cat -- "$0"
} | logger -t schroot-init-j2-debug
 
# need entropy for some init in the chroot
echo .{{ lookup('community.general.random_string', base64=true, length=500) }} >>/dev/urandom
 
# all tests for validity are in confd.j2
echo would create chroot {{ thechroot.name }} with {{ thechroot.dist }} in {{ thechroot.path }}
echo with extra packages {{ (['apt', 'debian-archive-keyring'] + schroot_role_pkgs) | join(',') }}
echo {{ thechroot.name }} \>{{ thechroot.path }}/etc/debian_chroot

… and a task…

- name: "{{ thechroot.name }} : INSTALL | set up chroot"
  ansible.builtin.command:
    argv: ["/bin/sh", "-s"]
    chdir: /
    creates: "{{ thechroot.path }}"
    stdin: "{{ lookup('template', 'init.j2') }}"
  register: initsh
 
- name: "{{ thechroot.name }} : debug"
  ansible.builtin.debug:
    var: initsh

… and it works as expected (the actual setting up of the chroot will come later…)

Nice work!

You may want to also consider explicitly using /bin/bash instead to ensure you get Bash-specific behavior (this assumes your target host has bash and a recent version). I know /bin/sh is a symlink to /bin/bash on many systems, but others actually link it to other shells ike /bin/dash, for example. Depending on where you are targeting this script, you may want to be more specific. Or, alternatively, specify whichever shell you’re expecting (ash, dash, zsh, …).

I’m also a fan of running the script under “set -Eeuo pipefail” for best safety and avoid unexpected weirdness. Warning: this radically changes the behavior of your script, tho! It’s just like running Perl scripts with “use strict;”.

Good luck with the rest of your script!

Ugh, no, why would I want GNU bash?

I write either for POSIX sh or for /bin/mksh, of course :cat_with_wry_smile:

But thanks for explaining set -euo pipefail (there’s no set -E…) to the developer of a Unix shell :grinning_cat:

I’m not a fan of set -e in general because errorlevel ≠ 0 does not always mean error (e.g. in grep(1)). Enacting these flags for the whole of a script is good training wheels for novice shell programmers, agreed.

(This scriptlet likely won’t need anything not in POSIX sh, it’s just long enough to be annoying to write it inline, and, this way, I can comment and structure better. It probably will need cleanup traps, though, or an atomic mv(1) at the end; we’ll see.)

POSIX compliance is good; nice target for portability! For your intended script, agreed, all this is probably overkill. I was just getting myself tripped long ago when I was using /bin/sh and expecting bash behaviors in particular. Not everyone likes Bash, tho (it’s definitely quirky!).

You might already know, but just in case, for set -e, you get an exemption when it’s used in things like:

# set -e gets bypassed
if git diff --cached --quiet; then
  echo "uncommitted changes exist. barf."
  exit 1
fi

set -e was breaking my scripts at first, too. If you have such a scenario where grep is ok to return non-zero, and you’d rather check the output, then you can head-fake it like:

OUTPUT=$(grep foo bar || /bin/true)  # allow nonzero for set -e
if [ -z "${OUTPUT}" ]; then
  echo "oops. null output."
  exit 1
fi

Anyhoo… I didn’t mean to hijack the direction; one thought started another. I’ll hush now. :slight_smile:
Good luck with the rest of your project!

As the developer of a shell, I might just know :wink:

It’s not helpful in that instance; for grep(1), you normally need to differentiate between 0 (matches found), 1 (no mathches found) and else (error).

(btw, look into grep -q)

Also fun: if you have a function that ends with such a construct, e.g…

foo() {
        # …
        if false; then
                echo meow
        fi
}

… then the function can still return 1 (it does not in this particular example, but I’ve seen more complex cases in which it did), which can cause set -e to trigger at the calling place, unless it also uses if, && or ||; this gets even more fun with set -o pipefail.

But, we digress.