#! /bin/sh # #? cac - CloudAtCost tool #? #? Usage: #? set -euf #PATH=$PWD/bin:$PATH #export PATH urlencode() { #! /bin/sh sed ' s/%/%25/g s/ /%20/g s/!/%21/g s/"/%22/g s/#/%23/g s/\$/%24/g s/\&/%26/g s/'\''/%27/g s/(/%28/g s/)/%29/g s/\*/%2a/g s/+/%2b/g s/,/%2c/g s/-/%2d/g s/\./%2e/g s/\//%2f/g s/:/%3a/g s/;/%3b/g s//%3e/g s/?/%3f/g s/@/%40/g s/\[/%5b/g s/\\/%5c/g s/\]/%5d/g s/\^/%5e/g s/_/%5f/g s/`/%60/g s/{/%7b/g s/|/%7c/g s/}/%7d/g s/~/%7e/g ' } # cac_servers_cache=${cac_servers_cache-$HOME/tmp/cac_servers_cache.json} cac_templates_cache=${cac_templates_cache-$HOME/tmp/cac_templates_cache.json} cac_secrets=${cac_secrets-$HOME/.secrets/cac} . "$cac_secrets" >/dev/null 2>&1 || : cac() { __cac_cli__command=${1-help} shift || : __cac_cli__"$__cac_cli__command" "$@" } #? cac help #? Print this help message. #? __cac_cli__help() {( exec sed < "$0" -n ' s/^#?\( \(.*\)\)\?/\2/p ' )} #? cac console SERVERSPEC #? Print console URL. #? __cac_cli__console() {( server=$(__cac_cli__getserver "$1") sid=$(echo $server | jq -r .sid) # TODO check reply status == ok _cac_post_api_v1 console sid="$sid" | jq -r .console )} #? cac servers #? Print cached servers JSON. #? __cac_cli__servers() { jq -r . $cac_servers_cache } #? cac update #? Fetch and cache servers and templates JSON. #? __cac_cli__update() {( umask 0077 for resource in servers templates; do { json=$(_cac_list$resource) eval file=\$cac_${resource}_cache echo $json | jq . > "$file".tmp mv "$file".tmp "$file" } & done wait )} #? cac getserver SERVERSPEC #? Print cached server JSON. #? __cac_cli__getserver() {( case $1 in *:*) k=${1%%:*} v=${1#*:} ;; *) k=label v=${1#*:} ;; esac if result=$(jq \ -e \ --arg k "$k" \ --arg v "$v" \ ' map(select(.[$k]==$v)) | if (. | length) == 1 then .[0] else null end ' \ $cac_servers_cache); then echo $result | jq -r . else echo "$0 getserver $k:$v => not unique server found" >&2 exit 23 fi )} #? cac generatenetworking SERVERSPEC #? __cac_cli__generatenetworking() {( server=$(__cac_cli__getserver "$1") hostname=$(echo $server | jq -r .label) address=$(echo $server | jq -r .ip) gateway=$(echo $server | jq -r .gateway) nameserver=8.8.8.8 netmask=$(echo $server | jq -r .netmask) prefix=$(netmask-to-prefix $netmask) #printf '# Generated file: %s generatenetworking %s %s\n' "$0" "$1" "$2" #printf '# on %s\n' "$(date -Is)" #printf '\n' printf '_:\n' printf '\n' printf '{\n' printf ' networking.hostName = "%s";\n' $hostname printf ' networking.interfaces.enp2s1.ip4 = [\n' printf ' {\n' printf ' address = "%s";\n' $address printf ' prefixLength = %d;\n' $prefix printf ' }\n' printf ' ];\n' printf ' networking.defaultGateway = "%s";\n' $gateway printf ' networking.nameservers = [\n' printf ' "%s"\n' $nameserver printf ' ];\n' printf '}\n' )} #? cac powerop SERVERSPEC (poweron|poweroff|reset) #? Activate server power operations. #? __cac_cli__powerop() {( server=$(__cac_cli__getserver "$1") action=$2 sid=$(echo $server | jq -r .sid) reply=$(_cac_post_api_v1 powerop sid="$sid" action="$action") case $(echo $reply | jq -r .status) in ok) echo $reply | jq -r . >&2 __cac_cli__update ;; *) echo bad reply: >&2 echo $reply | jq -r . >&2 exit 23 ;; esac )} #? cac pushconfig SERVERSPEC [PREFIX=/] #? __cac_cli__pushconfig() {( server=$(__cac_cli__getserver "$1") prefix=${2-/} hostname=$(echo $server | jq -r .label) address=$(echo $server | jq -r .ip) target=root@$address RSYNC_RSH='sshpass -e ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' SSHPASS=$(echo $server | jq -r .rootpass) export RSYNC_RSH SSHPASS pushgit . $target:$prefix/etc/nixos/ pushgit hosts $target:$prefix/etc/nixos/hosts/ pushgit tmp/nixpkgs/$hostname $target:$prefix/etc/nixos/nixpkgs/ pushdir secrets/$hostname/nix $target:$prefix/etc/nixos/secrets/ pushdir secrets/$hostname/rsync $target:$prefix/ echo "_:{imports=[./modules/$hostname];}" \ | $RSYNC_RSH "$target" tee "$prefix/etc/nixos/configuration.nix" \ > /dev/null ## TODO chmod and chown secrets )} #? cac setlabel SERVERSPEC LABEL #? __cac_cli__setlabel() {( server=$(__cac_cli__getserver "$1") label=$2 sid=$(echo $server | jq -r .sid) reply=$(_cac_post_api_v1 renameserver sid="$sid" name="$label") case $(echo $reply | jq -r .status) in ok) echo $reply | jq -r . >&2 __cac_cli__update ;; *) echo bad reply: >&2 echo $reply | jq -r . >&2 exit 23 ;; esac )} #? cac setmode SERVERSPEC (normal|safe) #? __cac_cli__setmode() {( server=$(__cac_cli__getserver "$1") mode=$2 sid=$(echo $server | jq -r .sid) reply=$(_cac_post_api_v1 runmode sid="$sid" mode="$mode") case $(echo $reply | jq -r .status) in ok) echo $reply | jq -r . >&2 __cac_cli__update ;; *) echo bad reply: >&2 echo $reply | jq -r . exit 23 ;; esac )} #? cac ssh SERVERSPEC #? __cac_cli__ssh() {( server=$(__cac_cli__getserver "$1") shift address=$(echo $server | jq -r .ip) target=root@$address SSHPASS=$(echo $server | jq -r .rootpass) export SSHPASS exec sshpass -e ssh \ -S none \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ $target \ "$@" )} #? cac waitstatus SERVERSPEC ("Powered On"|...) #? Blocks until server has specfied state. #? __cac_cli__waitstatus() { server=$(__cac_cli__getserver "$1") status=$(echo $server | jq -r .status) case $status in $2) return ;; esac echo "$(date -Is) Waiting for status: $2; current status: $status ..." >&2 __cac_cli__waitforcacheupdate __cac_cli__waitstatus "$@" } # XXX for __cac_cli__waitforcacheupdate and __cac_cli__poll cache means $cac_servers_cache #? cac waitforcacheupdate COMMAND [ARGS...] #? Blocks until cache has been updated then executes "$@". #? __cac_cli__waitforcacheupdate() { case $(inotifywait --format %f -q -e moved_to $(dirname $cac_servers_cache)) in $(basename $cac_servers_cache)) "$@";; *) __cac_cli__waitforcacheupdate "$@";; esac } #? cac poll [TIMESPEC=1m] #? Continuously update cache, sleeping at least $1 between updates. #? __cac_cli__poll() { __cac_cli__update t=${1-1m} echo "$(date -Is) cache updated; sleeping $t ..." >&2 sleep "$t" __cac_cli__poll "$@" } #? #? SERVERSPEC is a query like "mode:Safe", "sdate:08/04/2015", etc. #? See `cac servers` to get an inspiration. #? #? See sleep(1) for TIMESPEC. #? _cac_listservers() {( servers=$(_cac_get_api_v1 listservers) status=$(echo $servers | jq -r .status) if [ "$status" = ok ]; then echo "$servers" | jq -r .data else echo "cac_listservers: bad listservers status: $status" >&2 exit 1 fi )} _cac_listtemplates() {( servers=$(_cac_get_api_v1 listtemplates) status=$(echo $servers | jq -r .status) if [ "$status" = ok ]; then echo "$servers" | jq -r .data else echo "cac_listtemplates: bad listtemplates status: $status" >&2 exit 1 fi )} # rsyncfiles : lines filename |> local-dir x rsync-target -> ? |> ? rsyncfiles() {( set -x rsync \ --rsync-path="mkdir -p \"$2\" && rsync" \ -vzrlptD \ --files-from=- \ "$1"/ \ "$2" )} # gitfiles : git-work-tree -> lines filename gitfiles() { git -C "$1" archive --format=tar HEAD | tar t | sed '/\/$/d' } # pushgit : git-work-tree x rsync-target -> ? pushgit() { gitfiles "$1" | rsyncfiles "$1" "$2" } # dirfiles : local-dir -> lines filename dirfiles() {( cd "$1" find . -type f | sed 's/^\.\///' )} # pushdir : local-dir x rsync-target -> ? pushdir() { dirfiles "$1" | rsyncfiles "$1" "$2" } _cac_get_api_v1() { _cac_curl_api_v1 -G "$@" } _cac_post_api_v1() { _cac_curl_api_v1 -XPOST "$@" } _cac_curl_api_v1() { _cac_exec curl -sS "$1" "https://panel.cloudatcost.com/api/v1/$2.php" $( shift 2 set -- "$@" login="$cac_login" key="$cac_key" for arg; do echo -d $(printf '%s' "$arg" | urlencode) done ) } _cac_exec() { if test -z "${cac_via-}"; then env -- "$@" else ssh -q "$cac_via" -t "$@" fi } case ${run-true} in true) cac "$@";; esac