#! /bin/sh # #? cac-api - CloudAtCost API 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} cac_secrets=${cac_secrets-$HOME/.secrets/cac-api} cac_api() { __cac_api_cli__command=${1-help} shift || : __cac_api_cli__"$__cac_api_cli__command" "$@" } #? cac-api help [REGEX] #? Show help message. If a regex is specified, then show usage of matching #? commands. #? __cac_api_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_api_cli__help | sed -n ' /^cac-api '"$regex"'/,/^$/p ' fi < "$0" | filter )} #? cac-api console SERVERSPEC #? Print console URL. #? __cac_api_cli__console() {( server=$(__cac_api_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-api servers #? Print cached servers JSON. #? __cac_api_cli__servers() { jq -r . $cac_servers_cache } #? cac-api tasks #? Print cached tasks JSON. #? __cac_api_cli__tasks() { jq -r . $cac_tasks_cache } #? cac-api templates #? Print cached templates JSON. #? __cac_api_cli__templates() { jq -r . $cac_templates_cache } #? cac-api resources #? Print CloudPRO resources JSON. #? __cac_api_cli__resources() { jq -r . $cac_resources_cache } #? cac-api update #? Fetch and cache state JSON. #? __cac_api_cli__update() {( umask 0077 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" } & done wait )} #? cac-api getserver SERVERSPEC #? Print cached server JSON. #? __cac_api_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-api generatenetworking SERVERSPEC #? Generate NixOS module with networking configuration. #? __cac_api_cli__generatenetworking() {( server=$(__cac_api_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-api powerop SERVERSPEC (poweron|poweroff|reset) #? Activate server power operations. #? __cac_api_cli__powerop() {( server=$(__cac_api_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-api powerop' "$reply" )} #? cac-api setlabel SERVERSPEC LABEL #? __cac_api_cli__setlabel() {( server=$(__cac_api_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-api setlabel' "$reply" )} #? cac-api setmode SERVERSPEC (normal|safe) #? __cac_api_cli__setmode() {( server=$(__cac_api_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-api setmode' "$reply" )} #? cac-api ssh SERVERSPEC #? __cac_api_cli__ssh() {( server=$(__cac_api_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-api waitstatus SERVERSPEC ("Powered On"|...) #? Blocks until server has specfied state. #? __cac_api_cli__waitstatus() { server=$(__cac_api_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_api_cli__waitforcacheupdate __cac_api_cli__waitstatus "$@" } # XXX for __cac_api_cli__waitforcacheupdate and __cac_api_cli__poll cache means $cac_servers_cache #? cac-api waitforcacheupdate COMMAND [ARGS...] #? Blocks until cache has been updated then executes "$@". #? __cac_api_cli__waitforcacheupdate() { case $(inotifywait --format %f -q -e moved_to $(dirname $cac_servers_cache)) in $(basename $cac_servers_cache)) "$@";; *) __cac_api_cli__waitforcacheupdate "$@";; esac } #? cac-api poll [TIMESPEC=1m] #? Continuously update cache, sleeping at least $1 between updates. #? __cac_api_cli__poll() { __cac_api_cli__update t=${1-1m} echo "$(date -Is) cache updated; sleeping $t ..." >&2 sleep "$t" __cac_api_cli__poll "$@" } #? cac-api 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-api templates`) #? __cac_api_cli__build() {( reply=$(export "$@"; _cac_post_api_v1 cloudpro/build \ cpu="$cpu" \ ram="$ram" \ storage="$storage" \ os="$os" \ ) _cac_handle_reply 'cac-api build' "$reply" )} #? cac-api delete SERVERSPEC #? Delete / terminate server to add resources. #? __cac_api_cli__delete() {( server=$(__cac_api_cli__getserver "$1") sid=$(echo $server | jq -r .sid) reply=$(_cac_post_api_v1 cloudpro/delete sid="$sid") _cac_handle_reply 'cac-api delete' "$reply" )} #? #? SERVERSPEC is a query like "mode:Safe", "sdate:08/04/2015", etc. #? See `cac-api 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 )} # rsyncfiles : lines filename |> local-dir x rsync-target -> ? |> ? rsyncfiles() {( set -x rsync \ --rsync-path="mkdir -p \"$2\" && rsync" \ -vzrlptD \ --files-from=- \ "$1"/ \ "$2" )} _cac_handle_reply() {( label=$1 reply=$2 case $(echo $reply | jq -r .status) in ok) echo $reply | jq -r . >&2 __cac_api_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 } 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 ' } netmask_to_prefix() {( #! /bin/sh 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 )} # #? #? cac-api will try to load secrets from $cac_secrets #? and return with error code 1. __cac_load_secrets() { if ! test -r "$cac_secrets"; then echo "unable to load secrets from '$cac_secrets'" >&2 __cac_api_cli__help return 1 else . "$cac_secrets" fi } __cac_load_secrets case ${run-true} in true) cac_api "$@";; esac