diff options
Diffstat (limited to 'tools')
-rw-r--r-- | tools/README.md | 91 | ||||
-rwxr-xr-x | tools/setup-unit | 1497 | ||||
-rwxr-xr-x | tools/unitc | 235 |
3 files changed, 1823 insertions, 0 deletions
diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 00000000..f534aa1f --- /dev/null +++ b/tools/README.md @@ -0,0 +1,91 @@ +# Unit Tools + +This directory contains useful tools for installing, configuring, and +managing NGINX Unit. They may not be part of official packages and +should be considered experimental. + +* [`setup-unit`](#setup-unit) +* [`unitc`](#unitc) + +--- + +## setup-unit + +### A script that simplifies installing and configuring an NGINX Unit server for first-time users + +* `setup-unit repo-config` configures your package manager with the NGINX +Unit repository for later installation. +* `setup-unit welcome` creates an initial configuration to serve a welcome +web page with NGINX Unit. + +--- + +## unitc + +### A curl wrapper for managing NGINX Unit configuration + +```USAGE: unitc [options] URI``` + + * **URI** specifies the target in Unit's control API, e.g. `/config` . + * Configuration data is read from stdin. + * [jq](https://stedolan.github.io/jq/) is used to prettify JSON output, if + available. + +| Options | | +|---------|-| +| filename … | Read configuration data consequently from the specified files instead of stdin. +| _HTTP method_ | It is usually not required to specify a HTTP method. `GET` is used to read the configuration. `PUT` is used when making configuration changes unless a specific method is provided. +| `INSERT` | A _virtual_ HTTP method that prepends data when the URI specifies an existing array. The [jq](https://stedolan.github.io/jq/) tool is required for this option. +| `-q` \| `--quiet` | No output to stdout. + +Options are case insensitive and can appear in any order. For example, a +redundant part of the configuration can be identified by its URI, and +followed by `delete` in a subsequent command. + +### Local Configuration +For local instances of Unit, the control socket is automatically detected. +The error log is monitored; when changes occur, new log entries are shown. + +| Options | | +|---------|-| +| `-l` \| `--nolog` | Do not monitor the error log after configuration changes. + +#### Examples +```shell +unitc /config +unitc /control/applications/my_app/restart +unitc /config < unitconf.json +echo '{"*:8080": {"pass": "routes"}}' | unitc /config/listeners +unitc /config/applications/my_app DELETE +unitc /certificates/bundle cert.pem key.pem +``` + +### Remote Configuration +For remote instances of NGINX Unit, the control socket on the remote host can +be set with the `$UNIT_CTRL` environment variable. The remote control socket +can be accessed over TCP or SSH, depending on the type of control socket: + + * `ssh://[user@]remote_host[:ssh_port]/path/to/control.socket` + * `http://remote_host:unit_control_port` + +> **Note:** SSH is recommended for remote confguration. Consider the +> [security implications](https://unit.nginx.org/howto/security/#secure-socket-and-state) +> of managing remote configuration over plaintext HTTP. + +| Options | | +|---------|-| +| `ssh://…` | Specify the remote Unix control socket on the command line. +| `http://…`*URI* | For remote TCP control sockets, the URI may include the protocol, hostname, and port. + +#### Examples +```shell +unitc http://192.168.0.1:8080/status +UNIT_CTRL=http://192.168.0.1:8080 unitc /status + +export UNIT_CTRL=ssh://root@unithost/var/run/control.unit.sock +unitc /config/routes +cat catchall_route.json | unitc POST /config/routes +echo '{"match":{"uri":"/wp-admin/*"},"action":{"return":403}}' | unitc INSERT /config/routes +``` + +--- diff --git a/tools/setup-unit b/tools/setup-unit new file mode 100755 index 00000000..79dab850 --- /dev/null +++ b/tools/setup-unit @@ -0,0 +1,1497 @@ +#!/usr/bin/env bash + +##################################################################### +# +# Copyright (C) NGINX, Inc. +# Author: NGINX Unit Team, F5 Inc. +# +##################################################################### + + +if test -n ${BASH_VERSION} && test "${BASH_VERSINFO[0]}" -eq 3; then + >&2 echo 'Your version of bash(1) isn't supported by this script.'; + >&2 echo "You're probably running on macOS. We recommend that you either"; + >&2 echo 'install a newer version of bash(1) or run this script with'; + >&2 echo 'another shell, such as zsh(1):'; + >&2 echo " $ zsh ${SUDO_USER:+sudo }$0 ..."; + exit 1; +fi; + + +set -Eefuo pipefail; + +test -v BASH_VERSION \ +&& shopt -s lastpipe; + +export LC_ALL=C + +program_name="$0"; +prog_name="$(basename $program_name)"; + +dry_run='no'; + +help_unit() +{ + cat <<__EOF__ ; +SYNOPSIS + $program_name [-h] COMMAND [ARGS] + + Subcommands + +-- repo-config [-hn] [PKG-MANAGER OS-NAME OS-VERSION] + +-- welcome [-hn] + +DESCRIPTION + This script simplifies installing and configuring an NGINX Unit server + for first-time users. + + Run '$program_name COMMAND -h' for more information on a command. + +COMMANDS + repo-config + Configure your package manager with the NGINX Unit repository + for later installation. + + welcome + Create an initial configuration to serve a welcome web page + with NGINX Unit. + +OPTIONS + -h, --help + Print this help. + + --help-more + Print help for more commands. They are experimental. Using + these isn't recommended, unless you know what you're doing. + +__EOF__ +} + +help_more_unit() +{ + cat <<__EOF__ ; +SYNOPSIS + $program_name [-h] COMMAND [ARGS] + + Subcommands + +-- cmd [-h] + +-- ctl [-h] [-s SOCK] SUBCOMMAND [ARGS] + | +-- http [-h] [-c CURLOPT] METHOD PATH + | +-- insert [-h] PATH INDEX + +-- freeport [-h] + +-- json-ins [-hn] JSON INDEX + +-- os-probe [-h] + +-- ps [-h] [-t TYPE] + +-- repo-config [-hn] [PKG-MANAGER OS-NAME OS-VERSION] + +-- sock [-h] SUBCOMMAND [ARGS] + | +-- filter [-chs] + | +-- find [-h] + +-- welcome [-hn] + +DESCRIPTION + This script simplifies installing and configuring + an NGINX Unit server for first-time users. + + Run '$program_name COMMAND -h' for more information on a command. + +COMMANDS + cmd Print the invocation line of unitd(8). + + ctl Control a running unitd(8) instance via its control API socket. + + freeport + Print an available TCP port. + + json-ins + Insert a JSON element read from standard input into a JSON + array read from a file at a given INDEX. + + os-probe + Probe the OS and print details about its version. + + ps List unitd(8) processes. + + repo-config + Configure your package manager with the NGINX Unit + repository for later installation. + + sock Print the control API socket address. + + welcome + Create an initial configuration to serve a welcome web page + with NGINX Unit. + +OPTIONS + -h, --help + Print basic help (some commands are hidden). + + --help-more + Print the hidden help with more commands. + +__EOF__ +} + +warn() +{ + >&2 echo "$prog_name: error: $*"; +} + +err() +{ + >&2 echo "$prog_name: error: $*"; + exit 1; +} + +dry_run_echo() +{ + if test "$dry_run" = "yes"; then + echo "$*"; + fi; +} + +dry_run_eval() +{ + if test "$dry_run" = "yes"; then + echo " $*"; + else + eval "$*"; + fi; +} + + +help_unit_cmd() +{ + cat <<__EOF__ ; +SYNOPSIS + $program_name cmd [-h] + +DESCRIPTION + Print the invocation line of running unitd(8) instances. + +OPTIONS + -h, --help + Print this help. + +__EOF__ +} + + +unit_cmd() +{ + while test $# -ge 1; do + case "$1" in + -h | --help) + help_unit_cmd; + exit 0; + ;; + -*) + err "cmd: $1: Unknown option."; + ;; + *) + err "cmd: $1: Unknown argument."; + ;; + esac; + shift; + done; + + unit_ps -t m \ + | sed 's/.*\[\(.*\)].*/\1/'; +} + + +help_unit_ctl() +{ + cat <<__EOF__ ; +SYNOPSIS + $program_name ctl [-h] [-s SOCK] SUBCOMMAND [ARGS] + + Subcommands + +-- http [-h] [-c CURLOPT] METHOD PATH + +-- insert [-h] PATH INDEX + +DESCRIPTION + Control a running unitd(8) instance through its control API socket. + + Run '$program_name ctl SUBCOMMAND -h' for more information on a + subcommand. + +SUBCOMMANDS + http Send an HTTP request to the control API socket. + + insert Insert an element at the specified index into an array in the + JSON configuration. + +OPTIONS + -h, --help + Print this help. + + -s, --sock SOCK + Use SOCK as the control API socket address. If not specified, + the script tries to find it. This value is used by subcommands. + + The socket can be a tcp(7) socket or a unix(7) socket; in + the case of a unix(7) socket, it can exist locally or on + a remote machine, accessed through ssh(1). Accepted syntax + for SOCK: + + unix:/path/to/control.sock + ssh://[user@]host[:port]/path/to/control.sock + [http[s]://]host[:port] + + The last form is less secure than the first two; have a look: + <https://unit.nginx.org/howto/security/#secure-socket-and-stat> + +ENVIRONMENT + Options take precedence over their equivalent environment variables; + if both are specified, the command-line option is used. + + UNIT_CTL_SOCK + Equivalent to the option -s (--sock). + +__EOF__ +} + + +unit_ctl() +{ + + if test -v UNIT_CTL_SOCK; then + local sock="$UNIT_CTL_SOCK"; + fi; + + while test $# -ge 1; do + case "$1" in + -h | --help) + help_unit_ctl; + exit 0; + ;; + -s | --sock) + if ! test $# -ge 2; then + err "ctl: $1: Missing argument."; + fi; + local sock="$2"; + shift; + ;; + -*) + err "ctl: $1: Unknown option."; + ;; + *) + break; + ;; + esac; + shift; + done; + + if test ! $# -ge 1; then + err 'ctl: Missing subcommand.'; + fi; + + if test -v sock && echo $sock | grep '^ssh://' >/dev/null; then + local remote="$(echo $sock | sed 's,\(ssh://[^/]*\).*,\1,')"; + local sock="$(echo $sock | sed 's,ssh://[^/]*\(.*\),unix:\1,')"; + fi; + + case $1 in + http) + shift; + unit_ctl_http ${remote:+ ---r $remote} ${sock:+ ---s $sock} $@; + ;; + insert) + shift; + unit_ctl_insert ${remote:+ ---r $remote} ${sock:+ ---s $sock} $@; + ;; + *) + err "ctl: $1: Unknown argument."; + ;; + esac; +} + + +help_unit_ctl_http() +{ + cat <<__EOF__ ; +SYNOPSIS + $program_name ctl [CTL-OPTS] http [-h] [-c CURLOPT] METHOD PATH + +DESCRIPTION + Send an HTTP request to the unitd(8) control API socket. + + The payload is read from standard input. + +OPTIONS + -c, --curl CURLOPT + Pass CURLOPT as an option to curl. This script is implemented + in terms of curl(1), so it's useful to be able to tweak its + behavior. The option can be cumulatively used multiple times + (the result is also appended to UNIT_CTL_HTTP_CURLOPTS). + + -h, --help + Print this help. + +ENVIRONMENT + UNIT_CTL_HTTP_CURLOPTS + Equivalent to the option -c (--curl). + +EXAMPLES + $program_name ctl http -c --no-progress-meter GET /config >tmp; + +SEE ALSO + <https://unit.nginx.org/controlapi/#api-manipulation> + +__EOF__ +} + + +unit_ctl_http() +{ + local curl_options="${UNIT_CTL_HTTP_CURLOPTS:-}"; + + while test $# -ge 1; do + case "$1" in + -c | --curl) + if ! test $# -ge 2; then + err "ctl: http: $1: Missing argument."; + fi; + curl_options="$curl_options $2"; + shift; + ;; + -h | --help) + help_unit_ctl_http; + exit 0; + ;; + ---r | ----remote) + local remote="$2"; + shift; + ;; + ---s | ----sock) + local sock="$2"; + shift; + ;; + -*) + err "ctl: http: $1: Unknown option."; + ;; + *) + break; + ;; + esac; + shift; + done; + + if ! test $# -ge 1; then + err 'ctl: http: METHOD: Missing argument.'; + fi; + local method="$1"; + + if ! test $# -ge 2; then + err 'ctl: http: PATH: Missing argument.'; + fi; + local req_path="$2"; + + if test -v remote; then + local remote_sock="$(echo "$sock" | unit_sock_filter -s)"; + local local_sock="$(mktemp -u -p /var/run/unit/)"; + local ssh_ctrl="$(mktemp -u -p /var/run/unit/)"; + + mkdir -p /var/run/unit/; + + ssh -fMNnT -S "$ssh_ctrl" \ + -o 'ExitOnForwardFailure yes' \ + -L "$local_sock:$remote_sock" "$remote"; + + sock="unix:$local_sock"; + + elif ! test -v sock; then + local sock="$(unit_sock_find)"; + fi; + + curl $curl_options -X $method -d@- \ + $(echo "$sock" | unit_sock_filter -c)${req_path} \ + ||:; + + if test -v remote; then + ssh -S "$ssh_ctrl" -O exit "$remote" 2>/dev/null; + unlink "$local_sock"; + fi; +} + + +help_unit_ctl_insert() +{ + cat <<__EOF__ ; +SYNOPSIS + $program_name ctl [CTL-OPTS] insert [-h] PATH INDEX + +DESCRIPTION + Insert an element at the specified position (INDEX) into the JSON array + located at PATH in unitd(8) control API. + + The new element is read from standard input. + +OPTIONS + -h, --help + Print this help. + +SEE ALSO + $program_name ctl http -h; + +__EOF__ +} + + +unit_ctl_insert() +{ + while test $# -ge 1; do + case "$1" in + -h | --help) + help_unit_ctl_insert; + exit 0; + ;; + ---r | ----remote) + local remote="$2"; + shift; + ;; + ---s | ----sock) + local sock="$2"; + shift; + ;; + -*) + err "ctl: insert: $1: Unknown option."; + ;; + *) + break; + ;; + esac; + shift; + done; + + if ! test $# -ge 1; then + err 'ctl: insert: PATH: Missing argument.'; + fi; + local req_path="$1"; + + if ! test $# -ge 2; then + err 'ctl: insert: INDEX: Missing argument.'; + fi; + local idx="$2"; + + if test -v remote; then + local remote_sock="$(echo "$sock" | unit_sock_filter -s)"; + local local_sock="$(mktemp -u -p /var/run/unit/)"; + local ssh_ctrl="$(mktemp -u -p /var/run/unit/)"; + + mkdir -p /var/run/unit/; + + ssh -fMNnT -S "$ssh_ctrl" \ + -o 'ExitOnForwardFailure yes' \ + -L "$local_sock:$remote_sock" "$remote"; + + sock="unix:$local_sock"; + + elif ! test -v sock; then + local sock="$(unit_sock_find)"; + fi; + + local old="$(mktemp ||:)"; + + unit_ctl_http ---s "$sock" -c --no-progress-meter GET "$req_path" \ + </dev/null >"$old" \ + ||:; + + unit_json_ins "$old" "$idx" \ + | unit_ctl_http ---s "$sock" PUT "$req_path" \ + ||:; + + if test -v remote; then + ssh -S "$ssh_ctrl" -O exit "$remote" 2>/dev/null; + unlink "$local_sock"; + fi; +} + + +help_unit_ctl_welcome() +{ + cat <<__EOF__ ; +SYNOPSIS + $program_name welcome [-hn] + +DESCRIPTION + This script tests an NGINX Unit installation by creating an initial + configuration and serving a welcome web page. Recommended for + first-time users. + +OPTIONS + -h, --help + Print this help. + + -n, --dry-run + Dry run. Print the commands to be run instead of actually + running them. Each command is preceded by a line explaining + what it does. + +__EOF__ +} + + +unit_ctl_welcome() +{ + while test $# -ge 1; do + case "$1" in + -f | --force) + local force='yes'; + ;; + -h | --help) + help_unit_ctl_welcome; + exit 0; + ;; + -n | --dry-run) + dry_run='yes'; + ;; + -*) + err "welcome: $1: Unknown option."; + ;; + *) + err "welcome: $1: Unknown argument."; + ;; + esac; + shift; + done; + + id -u \ + | xargs test 0 -ne \ + && err 'welcome: This script requires root privileges to run.'; + + command -v curl >/dev/null \ + || err 'welcome: curl(1) not found in PATH. It must be installed to run this script.'; + + www='/srv/www/unit/index.html'; + if test -e "$www" && ! test -v force || ! test -w /srv; then + www="$(mktemp)"; + mv "$www" "$www.html"; + www="$www.html" + fi; + + unit_ps -t m \ + | wc -l \ + | read -r nprocs \ + ||: + + if test 0 -eq "$nprocs"; then + warn "welcome: NGINX Unit isn't running."; + warn 'For help with starting NGINX Unit, see:'; + err " <https://unit.nginx.org/installation/#startup-and-shutdown>"; + elif test 1 -ne "$nprocs"; then + err 'welcome: Only one NGINX Unit instance should be running.'; + fi; + + local sock="$(unit_sock_find)"; + local curl_opt="$(unit_sock_find | unit_sock_filter -c)"; + + curl $curl_opt/ >/dev/null 2>&1 \ + || err "welcome: Can't reach the control API socket."; + + if ! test -v force; then + unit_cmd \ + | read -r cmd; + + # Check unitd is not configured already. + echo "$cmd" \ + | if grep '\--state' >/dev/null; then + echo "$cmd" \ + | sed 's/ --/\n--/g' \ + | grep '\--state' \ + | cut -d' ' -f2; + else + $cmd --help \ + | sed -n '/\--state/,+1p' \ + | grep 'default:' \ + | sed 's/ *default: "\(.*\)"/\1/'; + fi \ + | sed 's,$,/conf.json,' \ + | read -r conffile \ + ||:; + + if test -e $conffile; then + if ! unit_ctl_http ---s "$sock" 'GET' '/config' </dev/null 2>/dev/null | grep -q '^{}.\?$'; # The '.\?' is for the possible carriage return. + then + warn 'welcome: NGINX Unit is already configured. To overwrite'; + err 'its current configuration, run the script again with --force.'; + fi; + fi; + fi; + + ( + unit_freeport \ + || err "welcome: Can't find an available port."; + ) \ + | read -r port; + + dry_run_echo 'Create a file to serve:'; + dry_run_eval "mkdir -p $(dirname $www);"; + dry_run_eval "cat >'$www'"' <<__EOF__; + <!DOCTYPE html> + <html> + <head> + <title>Welcome to NGINX Unit</title> + <style type="text/css"> + body { background: white; color: black; font-family: sans-serif; margin: 2em; line-height: 1.5; } + h1,h2 { color: #00974d; } + li { margin-bottom: 0.5em; } + pre { background-color: beige; padding: 0.4em; } + hr { margin-top: 2em; border: 1px solid #00974d; } + .indent { margin-left: 1.5em; } + </style> + </head> + <body> + <h1>Welcome to NGINX Unit</h1> + <p>Congratulations! NGINX Unit is installed and running.</p> + <h3>Useful Links</h3> + <ul> + <li><b><a href="https://unit.nginx.org/configuration/?referer=welcome">https://unit.nginx.org/configuration/</a></b><br> + To get started with Unit, see the <em>Configuration</em> docs, starting with + the <em>Quick Start</em> guide.</li> + <li><b><a href="https://github.com/nginx/unit">https://github.com/nginx/unit</a></b><br> + See our GitHub repo to browse the code, contribute, or seek help from the + <a href="https://github.com/nginx/unit#community">community</a>.</li> + </ul> + + <h2>Next steps</h2> + + <h3>Check Current Configuration</h3> + <div class="indent"> + <p>Unit'"'"'s control API is currently listening for configuration changes + on the '"$(unit_sock_find | grep -q '^unix:' && echo '<a href="https://en.wikipedia.org/wiki/Unix_domain_socket">Unix socket</a>' || echo 'socket')"' at + <b>'"$(unit_sock_find)"'</b><br> + To see the current configuration:</p> + <pre>'"${SUDO_USER:+sudo }"'curl '"$curl_opt"'/config</pre> + </div> + + <h3>Change Listener Port</h3> + <div class="indent"> + <p>This page is served over a random TCP high port. To choose the default HTTP port (80), + replace the <b>"listeners"</b> object:</p> + <pre>echo '"'"'{"*:80": {"pass": "routes"}}'"'"' | '"${SUDO_USER:+sudo }"'curl -X PUT -d@- '"$curl_opt"'/config/listeners</pre> + Then remove the port number from the address bar and reload the page. + </div> + + <hr> + <p><a href="https://unit.nginx.org/?referer=welcome">NGINX Unit — the universal web app server</a><br> + NGINX, Inc. © 2022</p> + </body> + </html> +__EOF__'; + dry_run_echo; + dry_run_echo 'Give it appropriate permissions:'; + dry_run_eval "chmod 644 '$www';"; + dry_run_echo; + + dry_run_echo 'Configure unitd:' + dry_run_eval "cat <<__EOF__ \\ + | sed 's/8080/$port/' \\ + | curl -X PUT -d@- $curl_opt/config; + { + \"listeners\": { + \"*:8080\": { + \"pass\": \"routes\" + } + }, + \"routes\": [{ + \"action\": { + \"share\": \"$www\" + } + }] + } +__EOF__"; + + dry_run_echo; + + echo; + echo 'You may want to try the following commands now:'; + echo; + echo 'Check out current unitd configuration:'; + echo " ${SUDO_USER:+sudo} curl $curl_opt/config"; + echo; + echo 'Browse the welcome page:'; + echo " curl http://localhost:$port/"; +} + + +help_unit_freeport() +{ + cat <<__EOF__ ; +SYNOPSIS + $program_name freeport [-h] + +DESCRIPTION + Print an available TCP port. + +OPTIONS + -h, --help + Print this help. + +__EOF__ +} + + +unit_freeport() +{ + while test $# -ge 1; do + case "$1" in + -h | --help) + help_unit_freeport; + exit 0; + ;; + -*) + err "freeport: $1: Unknown option."; + ;; + *) + err "freeport: $1: Unknown argument."; + ;; + esac; + shift; + done; + + freeport="$(mktemp -t freeport-XXXXXX)"; + + cat <<__EOF__ \ + | cc -x c -o $freeport -; + #include <netinet/in.h> + #include <stdio.h> + #include <stdlib.h> + #include <strings.h> + #include <sys/socket.h> + #include <unistd.h> + + + int32_t get_free_port(void); + + + int + main(void) + { + int32_t port; + + port = get_free_port(); + if (port == -1) + exit(EXIT_FAILURE); + + printf("%d\n", port); + exit(EXIT_SUCCESS); + } + + + int32_t + get_free_port(void) + { + int sfd; + int32_t port; + socklen_t len; + struct sockaddr_in addr; + + port = -1; + + sfd = socket(PF_INET, SOCK_STREAM, 0); + if (sfd == -1) { + perror("socket()"); + return -1; + } + + bzero(&addr, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = htons(0); // random port + + len = sizeof(addr); + if (bind(sfd, (struct sockaddr *) &addr, len)) { + perror("bind()"); + goto fail; + } + + if (getsockname(sfd, (struct sockaddr *) &addr, &len)) { + perror("getsockname()"); + goto fail; + } + + port = ntohs(addr.sin_port); + + fail: + close(sfd); + return port; + } +__EOF__ + + $freeport; +} + + +help_unit_json_ins() +{ +cat <<__EOF__ ; +SYNOPSIS + $program_name json-ins [-hn] JSON INDEX + +ARGUMENTS + JSON Path to a JSON file containing a top-level array. + + INDEX Position in the array to insert the element at. + +DESCRIPTION + Insert a JSON element read from standard input into a JSON array read + from a file at a given INDEX. + + The resulting array is printed to standard output. + +OPTIONS + -h, --help + Print this help. + + -n, --dry-run + Dry run. Print the command to be run instead of actually + running it. + +__EOF__ +} + + +unit_json_ins() +{ + while test $# -ge 1; do + case "$1" in + -h | --help) + help_unit_json_ins; + exit 0; + ;; + -n | --dry-run) + dry_run='yes'; + ;; + -*) + err "json-ins: $1: Unknown option."; + ;; + *) + break; + ;; + esac; + shift; + done; + + if ! test $# -ge 1; then + err 'json-ins: JSON: Missing argument.'; + fi; + local arr=$1; + + if ! test $# -ge 2; then + err 'json-ins: INDEX: Missing argument.'; + fi; + local idx=$2; + + dry_run_eval "( + jq '.[0:$idx]' <'$arr'; + echo '['; + jq .; + echo ']'; + jq '.[$idx:]' <'$arr'; + ) \\ + | sed '/^\[]$/d' \\ + | sed '/^]$/{N;s/^]\n\[$/,/}' \\ + | jq .;" +} + + +help_unit_os_probe() +{ + cat <<__EOF__ ; +SYNOPSIS + $program_name os-probe [-h] + +DESCRIPTION + This script probes the OS and prints three fields, delimited by ':'; + the first is the package manager, the second is the OS name, the third + is the OS version. + +OPTIONS + -h, --help + Print this help. + +__EOF__ +} + + +unit_os_probe() +{ + while test $# -ge 1; do + case "$1" in + -h | --help) + help_unit_os_probe; + exit 0; + ;; + -*) + err "os-probe: $1: Unknown option."; + ;; + *) + err "os-probe: $1: Unknown argument."; + ;; + esac; + shift; + done; + + local os=$(uname | tr '[:upper:]' '[:lower:]') + + if [ "$os" != 'linux' ] && [ "$os" != 'freebsd' ]; then + err "os-probe: The OS isn't Linux or FreeBSD; can't proceed." + fi + + if [ "$os" = 'linux' ]; then + if command -v apt-get >/dev/null; then + local pkgMngr='apt'; + elif command -v dnf >/dev/null; then + local pkgMngr='dnf'; + elif command -v yum >/dev/null; then + local pkgMngr='yum'; + else + local pkgMngr=''; + fi; + + local osRelease='/etc/os-release'; + + if [ -f "$osRelease" ]; then + # The value for the ID and VERSION_ID may or may not be in quotes + local osName=$(grep "^ID=" "$osRelease" | sed s/\"//g | awk -F= '{ print $2 }' ||:) + local osVersion=$(grep '^VERSION_ID=' "$osRelease" | sed s/\"//g | awk -F= '{ print $2 }' || lsb_release -cs) + else + err "os-probe: Unable to determine OS and version, or the OS isn't supported." + fi + else + local pkgMngr='pkg'; + local osName=$os + local osVersion=$(uname -rs | awk -F '[ -]' '{print $2}' ||:) + if [ -z "$osVersion" ]; then + err 'os-probe: Unable to get the FreeBSD version.' + fi + fi + + osName=$(echo "$osName" | tr '[:upper:]' '[:lower:]') + echo "$pkgMngr:$osName:$osVersion" +} + + +help_unit_ps() +{ + cat <<__EOF__ ; +SYNOPSIS + $program_name ps [-h] [-t TYPE] + +DESCRIPTION + List unitd(8) processes. + +OPTIONS + -h, --help + Print this help. + + -t, --type TYPE + List only processes of type TYPE. The available types are: + + - controller (c) + - main (m) + - router (r) + +__EOF__ +} + + +unit_ps() +{ + while test $# -ge 1; do + case "$1" in + -h | --help) + help_unit_ps; + exit 0; + ;; + -t | --type) + if ! test $# -ge 2; then + err "ps: $1: Missing argument."; + fi; + local type=; + case "$2" in + c | controller) + local type_c='c'; + ;; + m | main) + local type_m='m'; + ;; + r | router) + local type_r='r'; + ;; + esac; + shift; + ;; + -*) + err "ps: $1: Unknown option."; + ;; + *) + err "ps: $1: Unknown argument."; + ;; + esac; + shift; + done; + + ps ax \ + | if test -v type; then + grep ${type_c:+-e 'unit: controller'} \ + ${type_m:+-e 'unit: main'} \ + ${type_r:+-e 'unit: router'}; + else + grep 'unit: '; + fi \ + | grep -v grep \ + ||: +} + + +help_unit_repo_config() +{ + cat <<__EOF__ ; +SYNOPSIS + $program_name repo-config [-hn] [PKG-MANAGER OS-NAME OS-VERSION] + +DESCRIPTION + This script configures the NGINX Unit repository for the system + package manager. + + The script automatically detects the OS and proceeds accordingly. + However, if this automatic selection fails, you may specify the + package manager and the OS name and version. + +ARGUMENTS + PKG-MANAGER + Supported: 'apt', 'dnf', and 'yum'. + + OS-NAME + Supported: 'debian', 'ubuntu', 'fedora', 'rhel', and 'amzn2'. + + OS-VERSION + For most distributions, this should be a numeric value; for + Debian derivatives, use the codename instead. + +OPTIONS + -h, --help + Print this help. + + -n, --dry-run + Dry run. Print the commands to be run instead of actually + running them. Each command is preceded by a line explaining + what it does. + +EXAMPLES + $ $prog_name repo-config apt debian bullseye; + $ $prog_name repo-config apt ubuntu jammy; + $ $prog_name repo-config dnf fedora 36; + $ $prog_name repo-config dnf rhel 9; + $ $prog_name repo-config yum amzn2 2; + +__EOF__ +} + + +unit_repo_config() +{ + installAPT () + { + local os_name="$2"; + + dry_run_echo "Install on $os_name"; + dry_run_echo; + dry_run_eval 'curl --output /usr/share/keyrings/nginx-keyring.gpg https://unit.nginx.org/keys/nginx-keyring.gpg;'; + dry_run_echo; + dry_run_eval 'apt-get install -y apt-transport-https lsb-release ca-certificates;'; + + if test $# -ge 3; then + local os_version="$3"; + else + local os_version='$(lsb_release -cs)'; + fi; + + dry_run_echo; + dry_run_eval "printf 'deb [signed-by=/usr/share/keyrings/nginx-keyring.gpg] https://packages.nginx.org/unit/$os_name/ %s unit\n' \"$os_version\" | tee /etc/apt/sources.list.d/unit.list;"; + dry_run_eval "printf 'deb-src [signed-by=/usr/share/keyrings/nginx-keyring.gpg] https://packages.nginx.org/unit/$os_name/ %s unit\n' \"$os_version\" | tee -a /etc/apt/sources.list.d/unit.list;"; + dry_run_echo; + dry_run_eval 'apt-get update;'; + } + + installYumDnf () + { + local pkg_mngr="$1"; + local os_name="$2"; + + if test $# -ge 3; then + local os_version="$3"; + else + local os_version='\$releasever'; + fi; + + dry_run_echo "Install on $os_name"; + dry_run_echo; + + dry_run_eval "cat >/etc/yum.repos.d/unit.repo <<__EOF__ +[unit] +name=unit repo +baseurl=https://packages.nginx.org/unit/$os_name/$os_version/\\\$basearch/ +gpgcheck=0 +enabled=1 +__EOF__"; + + dry_run_echo; + dry_run_eval "$pkg_mngr makecache;"; + } + + while test $# -ge 1; do + case "$1" in + -h | --help) + help_unit_repo_config; + exit 0; + ;; + -n | --dry-run) + dry_run='yes'; + ;; + -*) + err "repo-config: $1: Unknown option."; + ;; + *) + break; + ;; + esac; + shift; + done; + + if test $# -ge 1; then + local pkg_mngr="$1"; + + if ! test $# -ge 2; then + err "repo-config: OS-NAME: Missing argument."; + fi; + local os_name="$2"; + + if ! test $# -ge 3; then + err "repo-config: OS-VERSION: Missing argument."; + fi; + local os_version="$3"; + fi; + + command -v curl >/dev/null \ + || err 'repo-config: curl(1) not found in PATH. It must be installed to run this script.'; + + id -u \ + | xargs test 0 -ne \ + && err 'repo-config: This script requires root privileges to run.'; + + echo 'This script sets up the NGINX Unit repository'; + + if ! test $# -ge 3; then + local os_pkg_name_version=$(unit_os_probe || warn "On macOS, try 'brew install nginx/unit/unit'.") + local pkg_mngr=$(echo "$os_pkg_name_version" | awk -F: '{print $1}') + local os_name=$(echo "$os_pkg_name_version" | awk -F: '{print $2}') + local os_version=$(echo "$os_pkg_name_version" | awk -F: '{print $3}') + fi; + + # Call the appropriate installation function + case "$pkg_mngr" in + apt) + case "$os_name" in + debian | ubuntu) + installAPT "$pkg_mngr" "$os_name" ${3:+$os_version}; + ;; + *) + err "repo-config: $os_name: The OS isn't supported."; + ;; + esac + ;; + yum | dnf) + case "$os_name" in + rhel | amzn | fedora) + installYumDnf "$pkg_mngr" "$os_name" "$os_version" ${3:+ovr}; + ;; + *) + err "repo-config: $os_name: The OS isn't supported."; + ;; + esac; + ;; + *) + err "repo-config: $pkg_mngr: The package manager isn't supported."; + ;; + esac; + + echo + echo 'All done; the NGINX Unit repository is set up.'; + echo "Configured with '$pkg_mngr' on '$os_name' '$os_version'."; + echo 'Further steps: <https://unit.nginx.org/installation/#official-packages>' +} + + +help_unit_sock() +{ + cat <<__EOF__ ; +SYNOPSIS + $program_name sock [-h] SUBCOMMAND [ARGS] + + Subcommands + +-- filter [-ch] + +-- find [-h] + +DESCRIPTION + Print the control API socket address of running unitd(8) + instances. + + Run '$program_name sock SUBCOMMAND -h' for more information on a + subcommand. + +SUBCOMMANDS + filter Filter the output of the 'find' subcommand and transform it + to something suitable for running other commands, such as + curl(1) or ssh(1). + + find Find and print the control API socket address of running + unitd(8) instances. + +OPTIONS + -h, --help + Print this help. + +__EOF__ +} + + +unit_sock() +{ + while test $# -ge 1; do + case "$1" in + -h | --help) + help_unit_sock; + exit 0; + ;; + -*) + err "sock: $1: Unknown option."; + ;; + *) + break; + ;; + esac; + shift; + done; + + if ! test $# -ge 1; then + err 'sock: Missing subcommand.'; + fi; + + case $1 in + filter) + shift; + unit_sock_filter $@; + ;; + find) + shift; + unit_sock_find $@; + ;; + *) + err "sock: $1: Unknown subcommand."; + ;; + esac; +} + + +help_unit_sock_filter() +{ + cat <<__EOF__ ; +SYNOPSIS + $program_name sock filter [-chs] + +DESCRIPTION + Filter the output of the 'sock find' command and transform it to + something suitable for running other commands, such as + curl(1) or ssh(1). + +OPTIONS + -c, --curl + Print an argument suitable for curl(1). + + -h, --help + Print this help. + + -s, --ssh + Print a socket address suitable for use in an ssh(1) tunnel. + +__EOF__ +} + + +unit_sock_filter() +{ + while test $# -ge 1; do + case "$1" in + -c | --curl) + if test -v ssh_flag; then + err "sock: filter: $1: Missing argument."; + fi; + local curl_flag='yes'; + ;; + -h | --help) + help_unit_sock_filter; + exit 0; + ;; + -s | --ssh) + if test -v curl_flag; then + err "sock: filter: $1: Missing argument."; + fi; + local ssh_flag='yes'; + ;; + -*) + err "sock: filter: $1: Unknown option."; + ;; + *) + err "sock: filter: $1: Unknown argument."; + ;; + esac; + shift; + done; + + while read -r control; do + + if test -v curl_flag; then + if echo "$control" | grep '^unix:' >/dev/null; then + unix_socket="$(echo "$control" | sed 's/unix:/--unix-socket /')"; + host='http://localhost'; + else + unix_socket=''; + host="$control"; + fi; + + echo "$unix_socket $host"; + + elif test -v ssh_flag; then + echo "$control" \ + | sed -E 's,^(unix:|http://|https://),,'; + + else + echo "$control"; + fi; + done; +} + + +help_unit_sock_find() +{ + cat <<__EOF__ ; +SYNOPSIS + $program_name sock find [-h] + +DESCRIPTION + Find and print the control API socket address of running + unitd(8) instances. + +OPTIONS + -h, --help + Print this help. + +__EOF__ +} + + +unit_sock_find() +{ + while test $# -ge 1; do + case "$1" in + -h | --help) + help_unit_sock_find; + exit 0; + ;; + -*) + err "sock: find: $1: Unknown option."; + ;; + *) + err "sock: find: $1: Unknown argument."; + ;; + esac; + shift; + done; + + unit_cmd \ + | while read -r cmd; do + if echo "$cmd" | grep '\--control' >/dev/null; then + echo "$cmd" \ + | sed 's/ --/\n--/g' \ + | grep '\--control' \ + | cut -d' ' -f2; + else + if ! command -v $cmd >/dev/null; then + local cmd='unitd'; + fi; + $cmd --help \ + | sed -n '/\--control/,+1p' \ + | grep 'default:' \ + | sed 's/ *default: "\(.*\)"/\1/'; + fi; + done; +} + + +while test $# -ge 1; do + case "$1" in + -h | --help) + help_unit; + exit 0; + ;; + --help-more) + help_more_unit; + exit 0; + ;; + -*) + err "$1: Unknown option."; + ;; + *) + break; + ;; + esac; + shift; +done; + +if ! test $# -ge 1; then + err "Missing command."; +fi; + +case $1 in +cmd) + shift; + unit_cmd $@; + ;; +ctl) + shift; + unit_ctl $@; + ;; +freeport) + shift; + unit_freeport $@; + ;; +json-ins) + shift; + unit_json_ins $@; + ;; +os-probe) + shift; + unit_os_probe $@; + ;; +ps) + shift; + unit_ps $@; + ;; +repo-config) + shift; + unit_repo_config $@; + ;; +sock) + shift; + unit_sock $@; + ;; +welcome) + shift; + unit_ctl_welcome $@; + ;; +*) + err "$1: Unknown command."; + ;; +esac; diff --git a/tools/unitc b/tools/unitc new file mode 100755 index 00000000..838f7ebf --- /dev/null +++ b/tools/unitc @@ -0,0 +1,235 @@ +#!/bin/bash +# unitc - a curl wrapper for configuring NGINX Unit +# https://github.com/nginx/unit/tree/master/tools +# NGINX, Inc. (c) 2022 + +# Defaults +# +ERROR_LOG=/dev/null +REMOTE=0 +SHOW_LOG=1 +NOLOG=0 +QUIET=0 +URI="" +SSH_CMD="" +METHOD=PUT +CONF_FILES=() + +while [ $# -gt 0 ]; do + OPTION=$(echo $1 | tr '[a-z]' '[A-Z]') + case $OPTION in + "-H" | "--HELP") + shift + ;; + + "-L" | "--NOLOG" | "--NO-LOG") + NOLOG=1 + shift + ;; + + "-Q" | "--QUIET") + QUIET=1 + shift + ;; + + "GET" | "PUT" | "POST" | "DELETE" | "INSERT") + METHOD=$OPTION + shift + ;; + + "HEAD" | "PATCH" | "PURGE" | "OPTIONS") + echo "${0##*/}: ERROR: Invalid HTTP method ($OPTION)" + exit 1 + ;; + + *) + if [ -r $1 ]; then + CONF_FILES+=($1) + elif [ "${1:0:1}" = "/" ] || [ "${1:0:4}" = "http" ] && [ "$URI" = "" ]; then + URI=$1 + elif [ "${1:0:6}" = "ssh://" ]; then + UNIT_CTRL=$1 + else + echo "${0##*/}: ERROR: Invalid option ($1)" + exit 1 + fi + shift + ;; + esac +done + +if [ "$URI" = "" ]; then + cat << __EOF__ +${0##*/} - a curl wrapper for managing NGINX Unit configuration + +USAGE: ${0##*/} [options] URI + +• URI is for Unit's control API target, e.g. /config +• A local Unit control socket is detected unless a remote one is specified. +• Configuration data is read from stdin. + +General options + filename … # Read configuration data from files instead of stdin + HTTP method # Default=GET, or PUT with config data (case-insensitive) + INSERT # Virtual HTTP method to prepend data to an existing array + -q | --quiet # No output to stdout + +Local options + -l | --nolog # Do not monitor the error log after applying config changes + +Remote options + ssh://[user@]remote_host[:port]/path/to/control.socket # Remote Unix socket + http://remote_host:port/URI # Remote TCP socket + + A remote Unit control socket may also be defined with the \$UNIT_CTRL + environment variable as http://remote_host:port -OR- ssh://… (as above) + +__EOF__ + exit 1 +fi + +# Figure out if we're running on the Unit host, or remotely +# +if [ "$UNIT_CTRL" = "" ]; then + if [ "${URI:0:4}" = "http" ]; then + REMOTE=1 + UNIT_CTRL=$(echo "$URI" | cut -f1-3 -d/) + URI=/$(echo "$URI" | cut -f4- -d/) + fi +elif [ "${UNIT_CTRL:0:6}" = "ssh://" ]; then + REMOTE=1 + SSH_CMD="ssh $(echo $UNIT_CTRL | cut -f1-3 -d/)" + UNIT_CTRL="--unix-socket /$(echo $UNIT_CTRL | cut -f4- -d/) _" +elif [ "${URI:0:1}" = "/" ]; then + REMOTE=1 +fi + +if [ $REMOTE -eq 0 ]; then + # Check if Unit is running, find the main process + # + PID=($(ps ax | grep unit:\ main | grep -v \ grep | awk '{print $1}')) + if [ ${#PID[@]} -eq 0 ]; then + echo "${0##*/}: ERROR: unitd not running (set \$UNIT_CTRL to configure a remote instance)" + exit 1 + elif [ ${#PID[@]} -gt 1 ]; then + echo "${0##*/}: ERROR: multiple unitd processes detected (${PID[@]})" + exit 1 + fi + + # Read the significant unitd conifuration from cache file (or create it) + # + if [ -r /tmp/${0##*/}.$PID.env ]; then + source /tmp/${0##*/}.$PID.env + else + # Check we have unitd in $PATH (and all the other tools we will need) + # + MISSING=$(hash unitd curl ps grep tr cut sed tail sleep 2>&1 | cut -f4 -d: | tr -d '\n') + if [ "$MISSING" != "" ]; then + echo "${0##*/}: ERROR: cannot find$MISSING: please install or add to \$PATH" + exit 1 + fi + + # Get control address + # + PARAMS=$(ps $PID | grep unitd | cut -f2- -dv | tr '[]' ' ' | cut -f4- -d ' ' | sed -e 's/ --/\n--/g') + CTRL_ADDR=$(echo "$PARAMS" | grep '\--control' | cut -f2 -d' ') + if [ "$CTRL_ADDR" = "" ]; then + CTRL_ADDR=$(unitd --help | grep -A1 '\--control' | tail -1 | cut -f2 -d\") + fi + + # Prepare for network or Unix socket addressing + # + if [ $(echo $CTRL_ADDR | grep -c ^unix:) -eq 1 ]; then + SOCK_FILE=$(echo $CTRL_ADDR | cut -f2- -d:) + if [ -r $SOCK_FILE ]; then + UNIT_CTRL="--unix-socket $SOCK_FILE _" + else + echo "${0##*/}: ERROR: cannot read unitd control socket: $SOCK_FILE" + ls -l $SOCK_FILE + exit 2 + fi + else + UNIT_CTRL="http://$CTRL_ADDR" + fi + + # Get error log filename + # + ERROR_LOG=$(echo "$PARAMS" | grep '\--log' | cut -f2 -d' ') + if [ "$ERROR_LOG" = "" ]; then + ERROR_LOG=$(unitd --help | grep -A1 '\--log' | tail -1 | cut -f2 -d\") + fi + + # Cache the discovery for this unit PID (and cleanup any old files) + # + rm /tmp/${0##*/}.* 2> /dev/null + echo UNIT_CTRL=\"${UNIT_CTRL}\" > /tmp/${0##*/}.$PID.env + echo ERROR_LOG=${ERROR_LOG} >> /tmp/${0##*/}.$PID.env + fi +fi + +# Choose presentation style +# +if [ $QUIET -eq 1 ]; then + OUTPUT="head -c 0" # Equivalent to >/dev/null +elif hash jq 2> /dev/null; then + OUTPUT="jq" +else + OUTPUT="cat" +fi + +# Get current length of error log before we make any changes +# +if [ -f $ERROR_LOG ] && [ -r $ERROR_LOG ]; then + LOG_LEN=$(wc -l < $ERROR_LOG) +else + NOLOG=1 +fi + +# Adjust HTTP method and curl params based on presence of stdin payload +# +if [ -t 0 ] && [ ${#CONF_FILES[@]} -eq 0 ]; then + if [ "$METHOD" = "DELETE" ]; then + $SSH_CMD curl -X $METHOD $UNIT_CTRL$URI 2> /tmp/${0##*/}.$$ | $OUTPUT + else + SHOW_LOG=$(echo $URI | grep -c ^/control/) + $SSH_CMD curl $UNIT_CTRL$URI 2> /tmp/${0##*/}.$$ | $OUTPUT + fi +else + if [ "$METHOD" = "INSERT" ]; then + if ! hash jq 2> /dev/null; then + echo "${0##*/}: ERROR: jq(1) is required to use the INSERT method; install at <https://stedolan.github.io/jq/>" + exit 1 + fi + NEW_ELEMENT=$(cat ${CONF_FILES[@]}) + echo $NEW_ELEMENT | jq > /dev/null || exit $? # Test the input is valid JSON before proceeding + OLD_ARRAY=$($SSH_CMD curl -s $UNIT_CTRL$URI) + if [ "$(echo $OLD_ARRAY | jq -r type)" = "array" ]; then + echo $OLD_ARRAY | jq ". |= [$NEW_ELEMENT] + ." | $SSH_CMD curl -X PUT --data-binary @- $UNIT_CTRL$URI 2> /tmp/${0##*/}.$$ | $OUTPUT + else + echo "${0##*/}: ERROR: the INSERT method expects an array" + exit 3 + fi + else + cat ${CONF_FILES[@]} | $SSH_CMD curl -X $METHOD --data-binary @- $UNIT_CTRL$URI 2> /tmp/${0##*/}.$$ | $OUTPUT + fi +fi + +CURL_STATUS=${PIPESTATUS[0]} +if [ $CURL_STATUS -ne 0 ]; then + echo "${0##*/}: ERROR: curl(1) exited with an error ($CURL_STATUS)" + if [ $CURL_STATUS -eq 7 ] && [ $REMOTE -eq 0 ]; then + echo "${0##*/}: Check that you have permission to access the Unit control socket, or try again with sudo(8)" + else + echo "${0##*/}: Trying to access $UNIT_CTRL$URI" + cat /tmp/${0##*/}.$$ && rm /tmp/${0##*/}.$$ + fi + exit 4 +fi +rm /tmp/${0##*/}.$$ 2> /dev/null + +if [ $SHOW_LOG -gt 0 ] && [ $NOLOG -eq 0 ] && [ $QUIET -eq 0 ]; then + echo -n "${0##*/}: Waiting for log..." + sleep $SHOW_LOG + echo "" + sed -n $((LOG_LEN+1)),\$p $ERROR_LOG +fi |