summaryrefslogtreecommitdiffstats
path: root/cac-api
diff options
context:
space:
mode:
Diffstat (limited to 'cac-api')
-rwxr-xr-xcac-api493
1 files changed, 493 insertions, 0 deletions
diff --git a/cac-api b/cac-api
new file mode 100755
index 0000000..b653323
--- /dev/null
+++ b/cac-api
@@ -0,0 +1,493 @@
+#! /bin/sh
+#
+#? cac-api - CloudAtCost API command line interface
+#?
+#? 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
+'
+}
+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_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_secrets" >/dev/null 2>&1 || :
+
+
+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
+}
+
+
+
+
+
+case ${run-true} in
+ true) cac_api "$@";;
+esac