Reference


>Table of Contents<


Why

I built my own Lua-based configuration management software. A little more than thousand commits in, I realized the oneshot nature of shell scripts more convenient.

CFEngine, Puppet, Chef and others did not offer advantages over a combination of rerun/bashing + drist. Cons mostly outweight the pros. Slow and complicated are the common complaints. If you’re just templating shell commands then plain shell scripts should suffice.

According to the Lindy effect; shell scripts, openssh and tar will outlive these mentioned CM software.

Huge shell scripts can be unwieldy. rr will help you manage your scripts.


Multicall Modes

You can use a symlink to activate the modes.

Example for sudo mode:

ln -s rr rrs

Console

Verbose mode.

When called as rrv or a console is detected it runs in verbose mode.

Thu, 22 Jul 2021 14:01:43 +0800 rr 0.10.0 "Kilowatt Triceps"
Thu, 22 Jul 2021 14:01:43 +0800 Running tmp:test via local...
Thu, 22 Jul 2021 14:01:43 +0800 Running test...
Thu, 22 Jul 2021 14:01:53 +0800 Done. Output:
 local ┌─ stdout
 local │
 local │ 3
 local │ 
 local └─
Thu, 22 Jul 2021 14:01:53 +0800 Total run time: 10.059915355s. All OK.

In this mode it also logs to the file rr.json in the current working directory.

NOTE: logs STDOUT and STDERR as a debug entry when no errors are detected

TIP: Example command line to extract STDOUT from the log file

grep 5DB2F900F1F1AF2B rr.json | grep stdout | cut -f12 -d\" | base64 -d

Silent

When called as rr and a console is not detected it only shows errors as structured JSON.

{"level":"error","stdout":"ss\n","stderr":"ee\n","time":"2021-07-20T20:16:04+08:00","message":"Output"}
{"level":"error","elapsed":"1.798478ms","time":"2021-07-20T20:16:04+08:00","message":"Something went wrong."}

Dump

When called as rrd, dumps the generated script. This is mainly for debugging and allows running scripts generated from a managed namespace without the rr executable.

Sudo

When called as rrs, asks for a sudo password for the remote user. Similar to the ansible options --become -K. Also turns on console mode since a TTY is expected anyway.


Audit Trail

The contents of the TASK file logged as the message field and task field. Without the task file the default is UNDEFINED. It can be overriden by the environment variable TASK.

This can be useful for audit trails or for changelogs. Before running a script you should fill up TASK.

echo 'Upgrade a thing for bugfix' > TASK
rr program:upgrade

READMEs

Any case insensitive file named readme* in the namespace and script directories can be shown by invoking rr by the following ways:

Prints namespace/readme*

rr namespace
rr namespace/

Prints namespace/script/readme*

rr namespace/script
rr namespace/script/

Environment Variables

Any environment variable prefixed with rr__ are passed to the script.

For example, the rr__USERNAME=test environment variable is passed to the script as USERNAME=test

$ cat tmp/env/script
echo $USERNAME

$ env rr__USERNAME=test rr tmp:env
Thu, 22 Jul 2021 18:13:32 +0800 rr 0.10.0 "Kilowatt Triceps"
Thu, 22 Jul 2021 18:13:32 +0800 Running tmp:env via local...
Thu, 22 Jul 2021 18:13:32 +0800 Running env...
Thu, 22 Jul 2021 18:13:32 +0800 Done. Output:
 local ┌─ stdout
 local │
 local │ test
 local │ 
 local └─
Thu, 22 Jul 2021 18:13:32 +0800 Total run time: 101.648007ms. All OK.

Idempotence

The default behavior of scripts is to indicate two results: no errors(ok) and failure(failed). To emulate idempotence your script can print the following string to STDOUT in it’s own line:

+++++repaired+++++

When writing your script you have to check if an operation is needed to apply a change or not. Such a precondition can be written like this:

if test -d /some_dir
then
  exit 0
else
  {
    mkdir /some_dir
  } >/dev/null 2>&1 || exit 1
  printf "+++++repaired+++++\\n"
  exit 0
fi

In the JSON log and report mode the result value will say repaired.

No changes, script exited –> ok
Script succesfully executed changes –> repaired
Repair needed but unable to apply changes –> failed

To borrow terminology from *CFEngine*. There are no changes when the **desired state** is already the expected outcome.

Any state –> Desired state
Desired state –> Desired state


Hierarchy

TOPLEVEL                            <--- Directory of namespaces or a project
├── rr.hosts                        <--- custom SSH config
├── .files                          <--- synced to any host
├── .files-avocado                  <--- synced to the avocado host
├── .lib                            <--- sourced by all scripts
└── namespace
    ├── .files                      <--- synced to any host when namespace:* is called
    ├── .files-avocado              <--- synced to the avocado host when namespace:* is called
    ├── .lib                        <--- sourced along with namespace:* scripts
    ├── readme                      <--- Documentation for namespace
    └── script directory
        ├── .files                  <--- synced to any host when namespace:script is called
        ├── .files-avocado          <--- synced to the avocado host when namespace:script is called
        ├── .lib                    <--- sourced along with namespace:script scripts
        ├── readme                  <--- Documentation for script
        ├── task                    <--- Companion to readme, used to log current task
        └── script                  <--- the actual shell script

Notes

Tested on Linux and macOS.

Remote host only requires OpenSSH server and tar(1).

Scripts should reference $@ if it wants to use arguments passed.


Decision Record