#! /usr/bin/env bash # #? cac - CloudAtCost command line interface #? #? Usage: #? set -euf #PATH=$PWD/bin:$PATH #export PATH cac_resources_cache=${cac_resources_cache-$HOME/tmp/cac_resources_cache.json} cac_servers_cache=${cac_servers_cache-$HOME/tmp/cac_servers_cache.json} cac_tasks_cache=${cac_tasks_cache-$HOME/tmp/cac_tasks_cache.json} cac_templates_cache=${cac_templates_cache-$HOME/tmp/cac_templates_cache.json} update_interval=${update_interval-10} cac_secrets=${cac_secrets-$HOME/.secrets/cac} _load_secrets() { if test ! -r "$cac_secrets" ;then echo "unable to load secrets from cac_secrets='$cac_secrets'" >&2 __cac_cli__help return 1 else . "$cac_secrets" fi } cac() { __cac_cli__command=${1-help} shift || : __cac_cli__"$__cac_cli__command" "$@" } #? cac help [REGEX] #? Show help message. If a regex is specified, then show usage of matching #? commands. #? __cac_cli__help() {( regex=${1-} # test -t expects GNU coreutils if test -t 0 >/dev/null 2>&1; then filter() { help=$(cat) echo "$help" | if test $(echo "$help" | wc -l) -gt $(tput lines); then $PAGER "$@" else cat "$@" fi } else filter() { cat "$@" } fi if test -z "$regex"; then sed -n ' s/^#?\( \(.*\)\)\?/\2/p ' else __cac_cli__help | sed -n ' /^cac '"$regex"'/,/^$/p ' fi < "$0" | filter )} #? 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 )} _maybe_update(){ if test ! -r "$1" -o -n "${always_update:-}" ;then echo "$(date -Is) updating cache file" >&2 __cac_cli__update fi } #? cac servers #? Print cached servers JSON. #? __cac_cli__servers() { _maybe_update $cac_servers_cache jq -r . $cac_servers_cache } #? cac tasks #? Print cached tasks JSON. #? __cac_cli__tasks() { _maybe_update $cac_tasks_cache jq -r . $cac_tasks_cache } #? cac templates #? Print cached templates JSON. #? __cac_cli__templates() { _maybe_update $cac_templates_cache jq -r . $cac_templates_cache } #? cac resources #? Print CloudPRO resources JSON. #? __cac_cli__resources() { _maybe_update $cac_resources_cache jq -r . $cac_resources_cache } #? cac update #? Fetch and cache state JSON. #? __cac_cli__update() {( umask 0077 pids="" for x in \ resources \ servers \ tasks \ templates \ # This line intentionally left blank. do { json=$(_cac_fetch_$x) eval file=\$cac_${x}_cache echo $json | jq . > "$file".tmp mv "$file".tmp "$file" } & pids="$pids $!" done for pid in $pids; do wait $pid;done )} #? cac getserver SERVERSPEC #? Print cached server JSON. #? __cac_cli__getserver() {( case $1 in *:*) k=${1%%:*} v=${1#*:} ;; *) k=label v=${1#*:} ;; esac _maybe_update $cac_servers_cache 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") 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 '_:\n' printf '\n' printf '{\n' 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") _cac_handle_reply 'cac powerop' "$reply" )} #? 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") _cac_handle_reply 'cac setlabel' "$reply" )} #? 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") _cac_handle_reply 'cac setmode' "$reply" )} #? 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 # always_update is not compatible with inotifywait if test -z "${always_update:-}";then echo "$(date -Is) Waiting for status: $2; current status: $status ..." >&2 __cac_cli__waitforcacheupdate __cac_cli__waitstatus "$@" else echo "$(date -Is) Waiting ${update_interval}secs for status: $2; current status: $status ..." sleep ${update_interval} __cac_cli__waitstatus "$@" fi } # 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 "$@" } #? cac build cpu=.. ram=.. storage=.. os=.. #? Build a server from available resources. #? cpu = 1/2/3/4/5/6/7/8 limit: 16 #? ram = 1024 (must be multiple of 4. ex. 1024 / 2048 / 3096) limit: 32768 #? storage = 10/20/30/40/50 ... etc limit: 1000 #? os = 75 (must be an #id from `cac templates`) #? __cac_cli__build() {( reply=$(export "$@"; _cac_post_api_v1 cloudpro/build \ cpu="$cpu" \ ram="$ram" \ storage="$storage" \ os="$os" \ ) _cac_handle_reply 'cac build' "$reply" )} #? cac delete SERVERSPEC #? Delete / terminate server to add resources. #? __cac_cli__delete() {( server=$(__cac_cli__getserver "$1") sid=$(echo $server | jq -r .sid) reply=$(_cac_post_api_v1 cloudpro/delete sid="$sid") _cac_handle_reply 'cac delete' "$reply" )} #? #? Uses the following `environment variables`: #? cac_resources_cache=$HOME/tmp/cac_resources_cache.json #? cac_servers_cache=$HOME/tmp/cac_servers_cache.json #? cac_tasks_cache=$HOME/tmp/cac_tasks_cache.json #? cac_templates_cache=$HOME/tmp/cac_templates_cache.json #? cac_secrets=$HOME/.secrets/cac #? run=true < set to false to be able to source this file #? always_update= < set to true to always update when using cache files #? #? You can override these by setting them beforehand. #? #? `cac_secrets` will be sourced and may provide the following two entries: #? cac_login= #? cac_key= #? #? 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_fetch_servers() {( res=$(_cac_get_api_v1 listservers) status=$(echo $res | jq -r .status) if [ "$status" = ok ]; then echo "$res" | jq -r .data else echo "cac_fetch_servers: bad status: $status" >&2 exit 1 fi )} _cac_fetch_tasks() {( res=$(_cac_get_api_v1 listtasks) status=$(echo $res | jq -r .status) if [ "$status" = ok ]; then echo "$res" | jq -r .data else echo "cac_fetch_tasks: bad status: $status" >&2 exit 1 fi )} _cac_fetch_templates() {( res=$(_cac_get_api_v1 listtemplates) status=$(echo $res | jq -r .status) if [ "$status" = ok ]; then echo "$res" | jq -r .data else echo "cac_fetch_templates: bad status: $status" >&2 exit 1 fi )} _cac_fetch_resources() {( res=$(_cac_get_api_v1 cloudpro/resources) status=$(echo $res | jq -r .status) if [ "$status" = ok ]; then echo "$res" | jq -r .data else echo "cac_resources: bad cloudpro/resources status: $status" >&2 exit 1 fi )} _cac_handle_reply() {( label=$1 reply=$2 case $(echo $reply | jq -r .status) in ok) echo $reply | jq -r . >&2 __cac_cli__update ;; *) echo $label: bad reply: >&2 echo $reply | jq -r . >&2 exit 23 ;; esac )} _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 } _main() { _load_secrets || exit 1 case ${run-true} in true) cac "$@";; esac } # # imported: urlencode() { 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 ' } netmask_to_prefix() {( set -euf netmask=$1 binaryNetmask=$(echo $1 | sed 's/^/obase=2;/;s/\./;/g' | bc | tr -d \\n) binaryPrefix=$(echo $binaryNetmask | sed -n 's/^\(1*\)0*$/\1/p') if ! echo $binaryPrefix | grep -q .; then echo $0: bad netmask: $netmask >&2 exit 4 fi printf %s $binaryPrefix | tr -d 0 | wc -c )} # rsyncfiles : lines filename |> local-dir x rsync-target -> ? |> ? rsyncfiles() {( set -x rsync \ --rsync-path="mkdir -p \"$2\" && rsync" \ -vzrlptD \ --files-from=- \ "$1"/ \ "$2" )} # _main "$@"