posixcube
Usage
usage: posixcube.sh -h HOST... [-l] [-c CUBE_DIR...] [OPTION]... COMMAND...
A POSIX compliant, shell script-based server automation framework.
-? Help.
-a If using Bash, asynchronously execute remote CUBEs/COMMANDs.
-b If using Bash, install programmable tab completion for SSH hosts.
-c CUBE Execute a cube. Option may be specified multiple times. If COMMANDS
are also specified, cubes are run first.
-d Print debugging information.
-D CMD Use `CMD` as the superuser command instead of `sudo`.
-e ENVAR Shell script with environment variable assignments which is
uploaded and sourced on each HOST. Option may be specified
multiple times. Files ending with .enc will be decrypted
temporarily. If not specified, defaults to envars*sh envars*sh.enc
-F FILE SSH `-F` option.
-h HOST Target host. Option may be specified multiple times. The HOST may
be preceded with USER@ to specify the remote user. If a host has
a wildcard ('*'), then HOST is interpeted as a regular expression,
with '*' replaced with '.*' and any matching hosts in the following
files are added to the HOST list: /etc/ssh_config,
/etc/ssh/ssh_config, ~/.ssh/config, /etc/ssh_known_hosts,
/etc/ssh/ssh_known_hosts, ~/.ssh/known_hosts, and /etc/hosts.
-i FILE SSH `-i` option for identity file.
-k Keep the cube_exec.sh generated script.
-l Local execution. Instead of (or in addition to) `-h HOST`s or a
sub-COMMAND, run the `-c` cubes or `COMMAND`s locally.
-o K=V SSH `-o` option. Option may be specified multiple times. Defaults
to `-o ConnectTimeout=5`.
-O P=V Set the specified variable P with the value V. Option may be
specified multiple times. Do not put double quotes around V. If
V contains *, replace with matching hosts per the -h algorithm.
-p PORT SSH `-p` option.
-P PWD Password for decrypting .enc ENVAR files.
-q Quiet; minimize output.
-r ROLE Role name. Option may be specified multiple times.
-R Use `rsync` instead of scp.
-s Skip remote host initialization (making ~/.posixcube, uploading
posixcube.sh, etc.). Assumes at least one run completed without -s.
Does not support encrypted ENVAR files.
-S Run cube_package and cube_service APIs as superuser.
-t SSH `-t` option.
-u USER SSH user. Defaults to ${USER}. This may also be specified in HOST.
-U CUBE Upload a CUBE but do not execute it. This is needed when one CUBE
includes this CUBE using cube_include.
-v Show version information.
-w PWDF File that contains the password for decrypting .enc ENVAR files.
Defaults to ~/.posixcube.pwd
-z SPEC Use the SPEC set of options from the ./cubespecs.ini file
-y If a HOST returns a non-zero code, continue processing other HOSTs.
COMMAND Remote command to run on each HOST. Option may be specified
multiple times. If no HOSTs are specified, available sub-commands:
edit: Decrypt, edit, and re-encrypt ENVAR file with $EDITOR.
show: Decrypt and print ENVAR file.
source: Source all ENVAR files. Must be run with
POSIXCUBE_SOURCED (see Public Variables section below).
Description:
posixcube.sh is used to execute CUBEs and/or COMMANDs on one or more HOSTs.
A CUBE is a shell script or directory containing shell scripts. The CUBE
is transferred to each HOST. If CUBE is a shell script, it's executed. If
CUBE is a directory, a shell script of the same name in that directory
is executed. In both cases, the directory is changed to the directory
containing the script before execution so that you may reference files
such as templates using relative paths.
An ENVAR script is encouraged to use environment variable names of the form
cubevar_${uniquecontext}_envar="value". If a CUBE directory contains the
file `envars.sh`, it's sourced before anything else (including `-e ENVARs`).
Both CUBEs and COMMANDs may execute any of the functions defined in the
"Public APIs" in the posixcube.sh script. Short descriptions of the
functions are in the APIs section below. See the source comments above each
function for details.
Examples (assuming posixcube.sh is on ${PATH}, or executed absolutely):
posixcube.sh -h socrates uptime
Run the `uptime` command on host `socrates`. This is not very different
from ssh ${USER}@socrates uptime, except that COMMANDs (`uptime`) have
access to the cube_* public functions.
posixcube.sh -h socrates -c test.sh
Run the `test.sh` script (CUBE) on host `socrates`. The script has
access to the cube_* public functions.
posixcube.sh -h socrates -c test
Upload the entire `test` directory (CUBE) to the host `socrates` and
then execute the `test.sh` script within that directory (the name
of the script is expected to be the same as the name of the CUBE). This
allows for easily packaging other scripts and resources needed by
`test.sh`.
posixcube.sh -S -h plato@socrates cube_package install atop
As the remote user `plato` on the host `socrates`, install the package
`atop`. The `-S` option is required to run the commands within
cube_package as the superuser (see the Philosophy section, #3).
posixcube.sh -h root@socrates -h seneca uptime
Run the `uptime` command on hosts `socrates` and `seneca`
as the user `root`.
posixcube.sh -h web*.test.com uptime
Run the `uptime` command on all hosts matching the regular expression
web.*.test.com in the SSH configuration files.
sudo posixcube.sh -b && . /etc/bash_completion.d/posixcube_completion.sh
For Bash users, install a programmable completion script to support tab
auto-completion of hosts from SSH configuration files.
posixcube.sh -e production.sh.enc show
Decrypt and show the contents of production.sh
posixcube.sh -e production.sh.enc edit
Decrypt, edit, and re-encrypt the contents of production.sh with $EDITOR
posixcube.sh -i ~/.ssh/plato -o LogLevel=VERBOSE -o ConnectTimeout=30 \
-h plato@socrates uptime
Run the `uptime` command on host `socrates` as the remote user `plato`
using the SSH identity file ~/.ssh/plato and specifying the SSH options
LogLevel=VERBOSE and ConnectTimeout=30
Philosophy:
1. Fail hard and fast. In principle, a well written script would check ${?}
after each command and either gracefully handle it, or report an error.
Few people write scripts this well, so we enforce this check (using
`cube_check_return` within all APIs) and we encourage you to do the same;
for example, `touch /etc/fstab || cube_check_return`. All cube_*
APIs are guaranteed to do their own checks, so you don't have to do this
for those calls; however, note that if you're executing a cube_* API in a
sub-shell, although any failures will be reported by cube_check_return,
the script will continue unless you also check the return of the sub-shell.
For example: $(cube_readlink /etc/localtime) || cube_check_return
With this strategy, unfortunately, piping becomes more difficult. There are
non-standard mechanisms like pipefail and PIPESTATUS, but the standardized
approach is to run each command separately and check the status. For example:
cubevar_app_x="$(cmd1)" || cube_check_return
cubevar_app_y="$(printf '%s' "${cubevar_app_x}" | cmd2)" || cube_check_return
2. We don't use `set -e` because some functions may handle all errors
internally (with `cube_check_return`) and use a positive return code as a
"benign" result (e.g. `cube_set_file_contents`). We don't use `set -u`
because we source in the user's scripts and they may not want this behavior.
3. Recent versions of many distributions encourage running most commands
as a non-superuser, and then using `sudo` if needed, with some distributions
disallowing remote SSH using the `root` account by default. First, the `sudo`
command is not standardized (see https://unix.stackexchange.com/a/48553).
Moreoever, it is not enough to prefix posixcube APIs with `sudo` because
`sudo` doesn't pass along functions (even if they're exported and `sudo` is
executed with --preserve-env). `su -c` may be used but it requires
password input. For the most common use case of requiring `sudo` for
cube_package and cube_service, if `-S` is specified, the commands within
those APIs are executed using `sudo`. If you need to run something else as a
superuser and you need access to the posixcube APIs, see the `cube_sudo` API.
Frequently Asked Questions:
* Why is there a long delay between "Preparing hosts" and the first remote
execution?
You can see details of what's happening with the `-d` flag. By default,
the script first loops through every host and ensures that ~/.posixcube/
exists, then it transfers itself to the remote host. These two actions
may be skipped with the `-s` parameter if you've already run the script
at least once and your version of this script hasn't been updated. Next,
the script loops through every host and transfers any CUBEs and a script
containing the CUBEs and COMMANDs to run (`cube_exec.sh`). If the shell
is detected to be `bash`, then the above occurs asynchronously across the
HOSTs. Finally, you'll see the "Executing on HOST..." line and the real
execution starts.
Cube Development:
Shell scripts don't have scoping, so to reduce the chances of function name
conflicts, name functions cube_${cubename}_${function} and name variables
cubevar_${cubename}_${var}.
Public APIs:
* cube_echo
Print ${@} to stdout prefixed with ([$(date)] [$(hostname)]) and
suffixed with a newline.
Example: cube_echo "Hello World"
* cube_printf
Print $1 to stdout prefixed with ([$(date)] [$(hostname)]) and
suffixed with a newline (with optional printf arguments in $@).
Example: cube_printf "Hello World from PID %5s" $$
* cube_error_echo
Same as cube_echo except output to stderr and include a red "Error: "
message prefix.
Example: cube_error_echo "Goodbye World"
* cube_error_printf
Same as cube_printf except output to stderr and include a red "Error: "
message prefix.
Example: cube_error_printf "Goodbye World from PID %5s" $$
* cube_warning_echo
Same as cube_echo except output to stderr and include a yellow
"Warning: " message prefix.
Example: cube_warning_echo "Watch out, World"
* cube_warning_printf
Same as cube_printf except output to stderr and include a yellow
"Warning: " message prefix.
Example: cube_warning_printf "Watch out, World from PID %5s" $$
* cube_throw
Same as cube_error_echo but also print a stack of functions and processes
(if available) and then call `exit 1`.
Example: cube_throw "Expected some_file."
* cube_check_return
Check if $? is non-zero and call cube_throw if so.
Example: some_command || cube_check_return
* cube_include
Include the ${1} cube
Example: cube_include core_cube
* cube_check_numargs
Call cube_throw if there are less than $1 arguments in $@
Example: cube_check_numargs 2 "${@}"
* cube_service
Run the $1 action on the $2 service.
Example: cube_service start crond
* cube_package
Pass $@ to the package manager. Implicitly passes the parameter
to say yes to questions. On Debian-based systems, uses --force-confold
Example: cube_package install python
* cube_package_uninstall
Same as cube_package, except that you only specify the packages
to uninstall, and this will figure out the right operation to
pass to the package manager (e.g. dnf=remove, apt=purge).
Example: cube_package_uninstall python
* cube_append_str
Print $1 to stdout with $2 appended after a space if $1 was not blank.
Example: cubevar_app_str=$(cube_append_str "${cubevar_app_str}" "Test")
* cube_element_exists
Check if $1 contains the $2 element with the $3 delimiter (default space).
Example: cube_element_exists "${cubevar_str}" "X"
* cube_elements_count
Print to stdout the number of elements in the string $1 as separated by
the delimeter $2 (default space).
Example: cubevar_app_count=$(cube_elements_count "${cubevar_str}")
* cube_command_exists
Check if $1 command or function exists in the current context.
Example: cube_command_exists systemctl
* cube_dir_exists
Check if $1 exists as a directory.
Example: cube_dir_exists /etc/cron.d/
* cube_file_exists
Check if $1 exists as a file with read access.
Example: cube_file_exists /etc/cron.d/0hourly
* cube_operating_system
Detect operating system and return one of the POSIXCUBE_OS_* values.
Example: [ $(cube_operating_system) -eq ${POSIXCUBE_OS_LINUX} ] && ...
* cube_operating_system_version_major
Returns the major version of the operating system distribution.
Example: [ $(cube_operating_system_version_major) -gt 10 ] && ...
* cube_operating_system_version_minor
Returns the minor version of the operating system distribution.
Example: [ $(cube_operating_system_version_minor) -gt 10 ] && ...
* cube_operating_system_has_flavor
Check if the operating system flavor includes the flavor specified in $1
by one of the POSIXCUBE_OS_FLAVOR_* values.
Example: cube_operating_system_has_flavor ${POSIXCUBE_OS_FLAVOR_FEDORA} \
&& ...
* cube_shell
Detect running shell and return one of the CUBE_SHELL_* values.
Example: [ $(cube_shell) -eq ${POSIXCUBE_SHELL_BASH} ] && ...
* cube_current_script_name
Print to stdout the basename of the currently executing script.
Example: script_name=$(cube_current_script_name)
* cube_current_script_abs_path
Print to stdout the absolute path the currently executing script.
Example: script_name=$(cube_current_script_abs_path)
* cube_file_size
Print to stdout the size of a file $1 in bytes
Example: cube_file_size some_file
* cube_set_file_contents
Copy the contents of $2 on top of $1 if $1 doesn't exist or the contents
are different than $2. If $2 ends with ".template", first evaluate all
${VARIABLE} expressions (except for \${VARIABLE}).
Example: cube_set_file_contents "/etc/npt.conf" "templates/ntp.conf"
* cube_set_file_contents_string
Set the contents of $1 to the string $@. Create file if it doesn't exist.
Example: cube_set_file_contents_string ~/.info "Hello World"
* cube_expand_parameters
Print stdin to stdout with all ${VAR}'s evaluated (except for \${VAR})
Example: cube_expand_parameters < template > output
* cube_readlink
Print to stdout the absolute path of $1 without any symbolic links.
Example: cube_readlink /etc/localtime
* cube_random_number
Print to stdout a random number between 1 and $1
Example: cube_random_number 10
* cube_tmpdir
Print to stdout a temporary directory
Example: cube_tmpdir
* cube_total_memory
Print to stdout total system memory in bytes
Example: cube_total_memory
* cube_ensure_directory
Ensure directory $1, and parent directories, exist. Return true if the
directory is created; otherwise, false.
Example: cube_ensure_directory ~/.ssh/
* cube_ensure_file
Ensure file $1, and parent directories, exist. Return true if the file is
created; otherwise, false.
Example: cube_ensure_file ~/.ssh/authorized_keys
* cube_pushd
Add the current directory to a stack of directories and change directory
to ${1}
Example: cube_pushd ~/.ssh/
* cube_popd
Pop the top of the stack of directories from `cube_pushd` and change
directory to that directory.
Example: cube_popd
* cube_has_role
Return true if the role $1 is set.
Example: cube_has_role "database_backup"
* cube_file_contains
Check if the file $1 contains $2. Uses grep with default arguments
(e.g. regex).
Example: cube_file_contains /etc/fstab nfsmount
* cube_stdin_contains
Check if stdin contains $1
Example: echo "Hello World" | cube_stdin_contains "Hello"
* cube_interface_ipv4_address
Print to stdout the IPv4 address of interface $1
Example: cube_interface_ipv4_address eth0
* cube_interface_ipv6_address
Print to stdout the IPv6 address of interface $1
Example: cube_interface_ipv6_address eth0
* cube_prompt
Prompt the question $1 followed by " (y/N)" and prompt for an answer.
A blank string answer is equivalent to No. Return true if yes, false
otherwise.
Example: cube_prompt "Are you sure?"
* cube_hostname
Print to stdout the full hostname.
Example: cube_hostname
* cube_string_contains
Return true if $1 contains $2; otherwise, false.
Example: cube_string_contains "${cubevar_app_str}" "@" && ...
* cube_string_substring_before
Print to stdout a substring of $1 strictly before the first match of the
regular expression $2.
Example: cubevar_app_x="$(cube_string_substring_before "${somevar}" "@")"
* cube_string_substring_after
Print to stdout a substring of $1 strictly after the first match of the
regular expression $2.
Example: cubevar_app_x="$(cube_string_substring_after "${somevar}" "@")"
* cube_sudo
Execute $* as superuser with all posixcube APIs available
(see Philosophy #3).
Example: cube_sudo cube_ensure_file /etc/app.txt
* cube_read_stdin
Read stdin (e.g. HEREDOC) into the variable named by the first argument.
* cube_ensure_user
Create the user $1 if it doesn't already exist (with group name $1)
Example: cube_ensure_user nginx
* cube_user_exists
Check if the $1 user exists
Example: cube_user_exists nginx
* cube_create_user
Create the user $1
Example: cube_create_user nginx
* cube_group_exists
Check if the $1 group exists
Example: cube_group_exists nginx
* cube_create_group
Create the group $1
Example: cube_create_group nginx
* cube_group_contains_user
Check if the $1 group contains the user $2
Example: cube_group_contains_user nginx nginx
* cube_add_group_user
Add the user $2 to group $1
Example: cube_add_group_user nginx nginx
* cube_current_user
Print to stdout the current user
Example: cube_current_user
* cube_user_home_dir
Print to stdout the home directory of user $1 (defaults to
`cube_current_user`)
Example: cube_user_home_dir root
* cube_user_ensure_private_key
Ensure the SSH private key contents $1 are placed in a file named $2
(defaults to `id_rsa`) for the user $3 (defaults to `cube_current_user`)
Example: cube_user_ensure_private_key "${cubevar_app_key1_private}"
* cube_user_ensure_authorized_public_key
Ensure the SSH public key contents $1 exist in the `.ssh/authorized_keys`
file in the home directory of user $2 (defaults to `cube_current_user`)
Example: cube_user_ensure_authorized_public_key "${cubevar_app_key1_pub}"
* cube_user_authorize_known_host
Ensure the public key(s) of the host $1 are in the `.ssh/known_hosts` file
in the home directory of user $2 (defaults to `cube_current_user`). Note:
the fingerprints are currently not checked, but you may review STDOUT.
To reduce the risks of MITM attacks, you can call this only after a
successful call to `cube_user_ensure_private_key` so that the scan is
only done once on private key setup.
Example: cube_user_ensure_private_key "${cubevar_app_host_privkey}" && \
cube_user_authorize_known_host some_host
Public Variables:
* POSIXCUBE_APIS_ONLY
Set this to any value to only source the public APIs in posixcube.sh.
Example: POSIXCUBE_APIS_ONLY=true . posixcube.sh && cube_echo \
$(cube_random_number 10)
* POSIXCUBE_SOURCED
Set this to any value to only run a sub-COMMAND, most commonly `source`,
to source in all ENVAR files, but skip actual execution of posixcube.
Example: POSIXCUBE_SOURCED=true . posixcube.sh source; \
POSIXCUBE_SOURCED= ; cube_echo Test
Source: https://github.com/myplaceonline/posixcube
Examples