diff --git a/V2.md b/V2.md new file mode 100644 index 0000000..4f427cc --- /dev/null +++ b/V2.md @@ -0,0 +1,5 @@ +tests/v2-regression-tests.sh + +docker run -it --rm -v $PWD:/work -w /work \ + -e NPC_API_KEY="$(jq -r .api_key ~/.npc/api.key)" -e NPC_API_SECRET="$(jq -r .api_secret ~/.npc/api.key)" \ + xiaopal/npc_setup tests/v2-regression-tests.sh diff --git a/files/npc-setup.ctx.sh b/files/npc-setup.ctx.sh index d47c9ce..30e4aa5 100644 --- a/files/npc-setup.ctx.sh +++ b/files/npc-setup.ctx.sh @@ -1,235 +1,239 @@ #! /bin/bash jq_check(){ - local ARGS=() ARG OUTPUT - while ARG="$1" && shift; do - case "$ARG" in - --out|--output) - OUTPUT="$1" && shift - ;; - --stdout) - OUTPUT="/dev/fd/1" - ;; - --stderr) - OUTPUT="/dev/fd/2" - ;; - *) - ARGS=("${ARGS[@]}" "$ARG") - ;; - esac - done - local CHECK_RESULT="$(jq "${ARGS[@]}")" && [ ! -z "$CHECK_RESULT" ] \ - && jq -cre 'select(.)'<<<"$CHECK_RESULT" >${OUTPUT:-/dev/null} && return 0 - [ ! -z "$OUTPUT" ] && [ -f "$OUTPUT" ] && rm -f "$OUTPUT" - return 1 + local ARGS=() ARG OUTPUT + while ARG="$1" && shift; do + case "$ARG" in + --out|--output) + OUTPUT="$1" && shift + ;; + --stdout) + OUTPUT="/dev/fd/1" + ;; + --stderr) + OUTPUT="/dev/fd/2" + ;; + *) + ARGS=("${ARGS[@]}" "$ARG") + ;; + esac + done + local CHECK_RESULT="$(jq "${ARGS[@]}")" && [ ! -z "$CHECK_RESULT" ] \ + && jq -cre 'select(.)'<<<"$CHECK_RESULT" >${OUTPUT:-/dev/null} && return 0 + [ ! -z "$OUTPUT" ] && [ -f "$OUTPUT" ] && rm -f "$OUTPUT" + return 1 } expand_resources(){ - local LINE KEY FILTER="${1:-.}" KEY_ATTR="${EXPAND_KEY_ATTR:-name}" - dump_str_vals(){ - local ARG; for ARG in "$@"; do echo "$# $ARG"; done - } - while read -r LINE; do - local KEYS=($(eval "echo $(jq -r ".$KEY_ATTR"'|gsub("^\\*\\:|[\\s\\$]"; "")'<<<"$LINE")")) KEY_INDEX=0 - for KEY in "${KEYS[@]}"; do - [ ! -z "$KEY" ] || continue - while read -r STM_LINE; do - local STM_VAL_JQ STM_VAL STM_VAL_COUNT STM_VAL_INDEX=0 - if jq_check 'length>1 and (.[1]|strings|startswith("*:"))'<<<"$STM_LINE"; then - STM_VAL_JQ='.[1]|gsub("^\\*\\:|[\\s\\$]"; "")' - elif jq_check 'length>1 and (.[1]|strings|startswith("@:"))'<<<"$STM_LINE"; then - STM_VAL_JQ='.[1]|gsub("^\\@\\:"; "")|gsub("(?[\\s\\$\\*])";"\\\(.c)")' - else - echo "$STM_LINE" && continue - fi - while read -r STM_VAL_COUNT STM_VAL; do - (( STM_VAL_INDEX++ == KEY_INDEX % STM_VAL_COUNT )) \ - && STM_VAL="$STM_VAL" jq -c '[.[0],env.STM_VAL]' <<<"$STM_LINE" - done < <(eval dump_str_vals $(jq -r "$STM_VAL_JQ"<<<"$STM_LINE")) - done < <(KEY="$KEY" jq --argjson index "$((KEY_INDEX))" -c ". + {$KEY_ATTR:env.KEY, ${KEY_ATTR}_index:\$index}|tostream"<<<"$LINE") \ - | jq -s 'fromstream(.[])'; ((KEY_INDEX++)) - done - done < <(jq -c 'arrays[]') | jq -sc "$FILTER" + local LINE KEY FILTER="${1:-.}" KEY_ATTR="${EXPAND_KEY_ATTR:-name}" + dump_str_vals(){ + local ARG; for ARG in "$@"; do echo "$# $ARG"; done + } + while read -r LINE; do + local KEYS=($(eval "echo $(jq -r ".$KEY_ATTR"'|gsub("^\\*\\:|[\\s\\$]"; "")'<<<"$LINE")")) KEY_INDEX=0 + for KEY in "${KEYS[@]}"; do + [ ! -z "$KEY" ] || continue + while read -r STM_LINE; do + local STM_VAL_JQ STM_VAL STM_VAL_COUNT STM_VAL_INDEX=0 + if jq_check 'length>1 and (.[1]|strings|startswith("*:"))'<<<"$STM_LINE"; then + STM_VAL_JQ='.[1]|gsub("^\\*\\:|[\\s\\$]"; "")' + elif jq_check 'length>1 and (.[1]|strings|startswith("@:"))'<<<"$STM_LINE"; then + STM_VAL_JQ='.[1]|gsub("^\\@\\:"; "")|gsub("(?[\\s\\$\\*])";"\\\(.c)")' + else + echo "$STM_LINE" && continue + fi + while read -r STM_VAL_COUNT STM_VAL; do + (( STM_VAL_INDEX++ == KEY_INDEX % STM_VAL_COUNT )) \ + && STM_VAL="$STM_VAL" jq -c '[.[0],env.STM_VAL]' <<<"$STM_LINE" + done < <(eval dump_str_vals $(jq -r "$STM_VAL_JQ"<<<"$STM_LINE")) + done < <(KEY="$KEY" jq --argjson index "$((KEY_INDEX))" -c ". + {$KEY_ATTR:env.KEY, ${KEY_ATTR}_index:\$index}|tostream"<<<"$LINE") \ + | jq -s 'fromstream(.[])'; ((KEY_INDEX++)) + done + done < <(jq -c 'arrays[]') | jq -sc "$FILTER" } plan_resources(){ - local STAGE="$1" INPUT_EXPECTED="$2" INPUT_ACTUAL="$3" STAGE_MAPPER="$4" - (jq -e 'arrays' $INPUT_EXPECTED || >>$STAGE.error) \ - | EXPAND_KEY_ATTR='name' expand_resources 'map({ key:.name, value:. }) | from_entries' >$STAGE.expected \ - && [ ! -f $STAGE.error ] && jq_check 'objects' $STAGE.expected \ - && jq -c 'arrays| map({ key:.name, value:. }) | from_entries' $INPUT_ACTUAL >$STAGE.actual \ - && [ ! -f $STAGE.error ] && jq_check 'objects' $STAGE.actual \ - || { - rm -f $STAGE.* - return 1 - } - jq -sce '(.[0] | map_values({ - present: true, - actual_present: false - } + .)) * (.[1] | map_values(. + { - actual_present: true - })) - | map_values(. + { - create: (.present and (.actual_present|not)), - update: (.present and .actual_present), - destroy : (.present == false and .actual_present), - absent : (.present == null and .actual_present) - }'"${STAGE_MAPPER:+| $STAGE_MAPPER}"')' $STAGE.expected $STAGE.actual >$STAGE \ - && rm -f $STAGE.* || return 1 - jq -ce '.[]|select(.error and ((.absent or (.destroy and .force_destroy))|not))|.error' $STAGE >&2 && return 1 - jq -ce '.[]|select(.absent)' $STAGE > $STAGE.omit || rm -f $STAGE.omit - jq -ce '.[]|select(.create)' $STAGE > $STAGE.creating || rm -f $STAGE.creating - jq -ce '.[]|select(.update)' $STAGE > $STAGE.updating || rm -f $STAGE.updating - jq -ce '.[]|select(.destroy)' $STAGE > $STAGE.destroying || rm -f $STAGE.destroying + local STAGE="$1" INPUT_EXPECTED="$2" INPUT_ACTUAL="$3" STAGE_MAPPER="$4" + (jq -e 'arrays' $INPUT_EXPECTED || >>$STAGE.error) \ + | EXPAND_KEY_ATTR='name' expand_resources 'map({ key:.name, value:. }) | from_entries' >$STAGE.expected \ + && [ ! -f $STAGE.error ] && jq_check 'objects' $STAGE.expected \ + && jq -c 'arrays| map({ key:.name, value:. }) | from_entries' $INPUT_ACTUAL >$STAGE.actual \ + && [ ! -f $STAGE.error ] && jq_check 'objects' $STAGE.actual \ + || { + rm -f $STAGE.* + return 1 + } + jq -sce '(.[0] | map_values({ + present: true, + actual_present: false + } + .)) * (.[1] | map_values(. + { + actual_present: true + })) + | map_values(. + { + create: (.present and (.actual_present|not)), + update: (.present and .actual_present), + destroy : (.present == false and .actual_present), + absent : (.present == null and .actual_present) + }'"${STAGE_MAPPER:+| $STAGE_MAPPER}"')' $STAGE.expected $STAGE.actual >$STAGE \ + && rm -f $STAGE.* || return 1 + jq -ce '.[]|select(.error and ((.absent or (.destroy and .force_destroy))|not))|.error' $STAGE >&2 && return 1 + jq -ce '.[]|select(.absent)' $STAGE > $STAGE.omit || rm -f $STAGE.omit + jq -ce '.[]|select(.create)' $STAGE > $STAGE.creating || rm -f $STAGE.creating + jq -ce '.[]|select(.update)' $STAGE > $STAGE.updating || rm -f $STAGE.updating + jq -ce '.[]|select(.destroy)' $STAGE > $STAGE.destroying || rm -f $STAGE.destroying } report_resources(){ - local RESOURCES=() ARG REPORT_SUMMARY REPORT_FILTER - while ARG="$1" && shift; do - case "$ARG" in - --summary) - REPORT_SUMMARY='Y' - ;; - --report) - REPORT_FILTER="$1" && shift - ;; - *) - RESOURCES=("${RESOURCES[@]}" "$ARG") - ;; - esac - done - - do_report(){ - local RESOURCE="$1" STAGE="$NPC_STAGE/$1" - local RESOURCE_FILTER="{$RESOURCE:([{key:.name,value:.}]|from_entries)}" - [ -f $STAGE ] && { - jq -nc "{ $RESOURCE:{} }" - jq -c ".[]|select(.actual_present and (.create or .update or .destroy or .absent | not))|$RESOURCE_FILTER" $STAGE - [ -f $STAGE.creating ] && if [ ! -f $STAGE.created ]; then - [ ! -z "$REPORT_SUMMARY" ] && jq -c '{creating: [.+{resource:"'"$RESOURCE"'"}]}' $STAGE.creating - else - jq -c '.+{change_action:"created"}|'"$RESOURCE_FILTER" $STAGE.created - [ ! -z "$REPORT_SUMMARY" ] && jq -c '{created: [.+{resource:"'"$RESOURCE"'"}]}' $STAGE.created - fi - [ -f $STAGE.updating ] && if [ ! -f $STAGE.updated ]; then - jq -c '.+{change_action:"updating"}|'"$RESOURCE_FILTER" $STAGE.updating - [ ! -z "$REPORT_SUMMARY" ] && jq -c '{updating: [.+{resource:"'"$RESOURCE"'"}]}' $STAGE.updating - else - jq -c '.+{change_action:"updated"}|'"$RESOURCE_FILTER" $STAGE.updated - [ ! -z "$REPORT_SUMMARY" ] && jq -c '{updated: [.+{resource:"'"$RESOURCE"'"}]}' $STAGE.updated - fi - [ -f $STAGE.destroying ] && if [ ! -f $STAGE.destroyed ]; then - jq -c '.+{change_action:"destroying"}|'"$RESOURCE_FILTER" $STAGE.destroying - [ ! -z "$REPORT_SUMMARY" ] && jq -c '{destroying: [.+{resource:"'"$RESOURCE"'"}]}' $STAGE.destroying - else - [ ! -z "$REPORT_SUMMARY" ] && jq -c '{destroyed: [.+{resource:"'"$RESOURCE"'"}]}' $STAGE.destroyed - fi - # [ -f $STAGE.omit ] && jq -c '.+{change_action:"omit"}|'"{$RESOURCE:[.]}" $STAGE.omit - } - } - - local RESOURCE REDUCE_FILTER - for RESOURCE in "${RESOURCES[@]}"; do - REDUCE_FILTER="$REDUCE_FILTER $RESOURCE: (if \$item.$RESOURCE then ((.$RESOURCE//{}) + \$item.$RESOURCE) else .$RESOURCE end)," - done - [ ! -z "$REPORT_SUMMARY" ] && { - REDUCE_FILTER="$REDUCE_FILTER"' - creating: (if $item.creating then ((.creating//[]) + $item.creating) else .creating end), - updating: (if $item.updating then ((.updating//[]) + $item.updating) else .updating end), - destroying: (if $item.destroying then ((.destroying//[]) + $item.destroying) else .destroying end), - created: (if $item.created then ((.created//[]) + $item.created) else .created end), - updated: (if $item.updated then ((.updated//[]) + $item.updated) else .updated end), - destroyed: (if $item.destroyed then ((.destroyed//[]) + $item.destroyed) else .destroyed end)' \ - REPORT_FILTER='| with_entries(select(.value))) | . + { - changing: (.creating or .updating or .destroying), - changed: (.created or .updated or .destroyed) - }'"$REPORT_FILTER" - } - { - for RESOURCE in "${RESOURCES[@]}"; do - do_report "$RESOURCE" - done - return 0 - } | jq -sc 'reduce .[] as $item ( {}; {'"$REDUCE_FILTER"'}'"$REPORT_FILTER" + local RESOURCES=() ARG REPORT_SUMMARY REPORT_FILTER + while ARG="$1" && shift; do + case "$ARG" in + --summary) + REPORT_SUMMARY='Y' + ;; + --report) + REPORT_FILTER="$1" && shift + ;; + *) + RESOURCES=("${RESOURCES[@]}" "$ARG") + ;; + esac + done + + do_report(){ + local RESOURCE="$1" STAGE="$NPC_STAGE/$1" + local RESOURCE_FILTER="{$RESOURCE:([{key:.name,value:.}]|from_entries)}" + [ -f $STAGE ] && { + jq -nc "{ $RESOURCE:{} }" + jq -c ".[]|select(.actual_present and (.create or .update or .destroy or .absent | not))|$RESOURCE_FILTER" $STAGE + [ -f $STAGE.creating ] && if [ ! -f $STAGE.created ]; then + [ ! -z "$REPORT_SUMMARY" ] && jq -c '{creating: [.+{resource:"'"$RESOURCE"'"}]}' $STAGE.creating + else + jq -c '.+{change_action:"created"}|'"$RESOURCE_FILTER" $STAGE.created + [ ! -z "$REPORT_SUMMARY" ] && jq -c '{created: [.+{resource:"'"$RESOURCE"'"}]}' $STAGE.created + fi + [ -f $STAGE.updating ] && if [ ! -f $STAGE.updated ]; then + jq -c '.+{change_action:"updating"}|'"$RESOURCE_FILTER" $STAGE.updating + [ ! -z "$REPORT_SUMMARY" ] && jq -c '{updating: [.+{resource:"'"$RESOURCE"'"}]}' $STAGE.updating + else + jq -c '.+{change_action:"updated"}|'"$RESOURCE_FILTER" $STAGE.updated + [ ! -z "$REPORT_SUMMARY" ] && jq -c '{updated: [.+{resource:"'"$RESOURCE"'"}]}' $STAGE.updated + fi + [ -f $STAGE.destroying ] && if [ ! -f $STAGE.destroyed ]; then + jq -c '.+{change_action:"destroying"}|'"$RESOURCE_FILTER" $STAGE.destroying + [ ! -z "$REPORT_SUMMARY" ] && jq -c '{destroying: [.+{resource:"'"$RESOURCE"'"}]}' $STAGE.destroying + else + [ ! -z "$REPORT_SUMMARY" ] && jq -c '{destroyed: [.+{resource:"'"$RESOURCE"'"}]}' $STAGE.destroyed + fi + # [ -f $STAGE.omit ] && jq -c '.+{change_action:"omit"}|'"{$RESOURCE:[.]}" $STAGE.omit + } + } + + local RESOURCE REDUCE_FILTER + for RESOURCE in "${RESOURCES[@]}"; do + REDUCE_FILTER="$REDUCE_FILTER $RESOURCE: (if \$item.$RESOURCE then ((.$RESOURCE//{}) + \$item.$RESOURCE) else .$RESOURCE end)," + done + [ ! -z "$REPORT_SUMMARY" ] && { + REDUCE_FILTER="$REDUCE_FILTER"' + creating: (if $item.creating then ((.creating//[]) + $item.creating) else .creating end), + updating: (if $item.updating then ((.updating//[]) + $item.updating) else .updating end), + destroying: (if $item.destroying then ((.destroying//[]) + $item.destroying) else .destroying end), + created: (if $item.created then ((.created//[]) + $item.created) else .created end), + updated: (if $item.updated then ((.updated//[]) + $item.updated) else .updated end), + destroyed: (if $item.destroyed then ((.destroyed//[]) + $item.destroyed) else .destroyed end)' \ + REPORT_FILTER='| with_entries(select(.value))) | . + { + changing: (.creating or .updating or .destroying), + changed: (.created or .updated or .destroyed) + }'"$REPORT_FILTER" + } + { + for RESOURCE in "${RESOURCES[@]}"; do + do_report "$RESOURCE" + done + return 0 + } | jq -sc 'reduce .[] as $item ( {}; {'"$REDUCE_FILTER"'}'"$REPORT_FILTER" } apply_actions(){ - local ACTION="$1" INPUT="$2" RESULT="$3" FORK=0 && [ -f $INPUT ] || return 0 - touch $RESULT && ( - for FORK in $(seq 1 ${NPC_ACTION_FORKS:-1}); do - [ ! -z "$FORK" ] && rm -f $RESULT.$FORK $RESULT.$FORK.* || continue - while [ ! -f $RESULT.error ]; do - flock 91 && read -r ACTION_ITEM <&90 && flock -u 91 || break - $ACTION "$ACTION_ITEM" "$RESULT.$FORK" "$SECONDS $RESULT" && { - [ -f "$RESULT.$FORK.skip" ] && continue - [ -f "$RESULT.$FORK" ] || echo "$ACTION_ITEM" >"$RESULT.$FORK" - flock 91 && jq -ce '.' $RESULT.$FORK >>$RESULT && flock -u 91 && continue - } - >>$RESULT.error - rm -f "$RESULT.$FORK"; break - done 91<$RESULT & - done 90<$INPUT; wait ) - [ -f $RESULT ] && [ ! -f $RESULT.error ] && rm -f $RESULT.* && return 0 - rm -f $RESULT; return 1 + local ACTION="$1" INPUT="$2" RESULT="$3" FORK=0 && [ -f $INPUT ] || return 0 + touch $RESULT && ( + for FORK in $(seq 1 ${NPC_ACTION_FORKS:-1}); do + [ ! -z "$FORK" ] && rm -f $RESULT.$FORK $RESULT.$FORK.* || continue + while [ ! -f $RESULT.error ]; do + flock 91 && read -r ACTION_ITEM <&90 && flock -u 91 || break + $ACTION "$ACTION_ITEM" "$RESULT.$FORK" "$SECONDS $RESULT" && { + [ -f "$RESULT.$FORK.skip" ] && continue + [ -f "$RESULT.$FORK" ] || echo "$ACTION_ITEM" >"$RESULT.$FORK" + flock 91 && jq -ce '.' $RESULT.$FORK >>$RESULT && flock -u 91 && continue + } + >>$RESULT.error + rm -f "$RESULT.$FORK"; break + done 91<$RESULT & + done 90<$INPUT; wait ) + [ -f $RESULT ] && [ ! -f $RESULT.error ] && rm -f $RESULT.* && return 0 + rm -f $RESULT; return 1 } time_to_seconds(){ - local SEC="$1"; - [[ "$SEC" = *s ]] && SEC="${SEC%s}" - [[ "$SEC" = *m ]] && SEC="${SEC%m}" && ((SEC *= 60)) - echo "$SEC" + local SEC="$1"; + [[ "$SEC" = *s ]] && SEC="${SEC%s}" + [[ "$SEC" = *m ]] && SEC="${SEC%m}" && ((SEC *= 60)) + echo "$SEC" } action_check_continue(){ - local START RESULT TIMEOUT="$(time_to_seconds "${2:-$NPC_ACTION_TIMEOUT}")" - read -r START RESULT _<<<"$1"|| return 1 - (( SECONDS - START < TIMEOUT )) || { - echo "[ERROR] timeout" >&2 - return 1 - } - [ ! -f $RESULT ] && { - echo "[ERROR] cancel" >&2 - return 1 - } - return 0 + local START RESULT TIMEOUT="$(time_to_seconds "${2:-$NPC_ACTION_TIMEOUT}")" + read -r START RESULT _<<<"$1"|| return 1 + (( SECONDS - START < TIMEOUT )) || { + echo "[ERROR] timeout" >&2 + return 1 + } + [ ! -f $RESULT ] && { + echo "[ERROR] cancel" >&2 + return 1 + } + return 0 } action_sleep(){ - local WAIT_SECONDS="$(time_to_seconds "$1")" && shift; - while action_check_continue "$@"; do - (( WAIT_SECONDS-- > 0 )) || return 0; sleep 1s - done; return 1 + local WAIT_SECONDS="$(time_to_seconds "$1")" && shift; + while action_check_continue "$@"; do + (( WAIT_SECONDS-- > 0 )) || return 0; sleep 1s + done; return 1 } checked_api(){ - local FILTER ARGS=(); while ! [[ "$1" =~ ^(GET|POST|PUT|DELETE|HEAD)$ ]]; do - [ ! -z "$FILTER" ] && ARGS=("${ARGS[@]}" "$FILTER") - FILTER="$1" && shift - done; ARGS=("${ARGS[@]}" "$@") + local FILTER ARGS=(); while ! [[ "$1" =~ ^(GET|POST|PUT|DELETE|HEAD)$ ]]; do + [ ! -z "$FILTER" ] && ARGS=("${ARGS[@]}" "$FILTER") + FILTER="$1" && shift + done; ARGS=("${ARGS[@]}" "$@") - local DO_API=(npc ${NPC_API:-api}) - [ ! -z "$NPC_API_LOCK" ] && DO_API=('flock' "$NPC_API_LOCK" "${DO_API[@]}") - [ ! -z "$NPC_API_SUCCEED_NO_RESPONSE" ] || ARGS=(--error "${ARGS[@]}") - local RESPONSE="$("${DO_API[@]}" "${ARGS[@]}" && \ - [ ! -z "$NPC_API_SUCCEED_NO_RESPONSE" ] && echo '{"ok":"no response"}' )" && [ ! -z "$RESPONSE" ] || { - [ ! -z "$OPTION_SILENCE" ] || [ ! -z "$NPC_API_SUCCEED_NO_RESPONSE" ] || echo "[ERROR] No response." >&2 - return 1 - } - [ "${NPC_API:-api}" == "api" ] && { - jq_check .code <<<"$RESPONSE" && [ "$(jq -r .code <<<"$RESPONSE")" != "200" ] && { - [ ! -z "$OPTION_SILENCE" ] || echo "[ERROR] $RESPONSE" >&2 - return 1 - } - } - if [ ! -z "$FILTER" ]; then - jq -cre "($FILTER)//empty" <<<"$RESPONSE" && return 0 - else - jq_check '.' <<<"$RESPONSE" && return 0 - fi - [ ! -z "$OPTION_SILENCE" ] || echo "[ERROR] $RESPONSE" >&2 - return 1 + local DO_API=(npc ${NPC_API:-api}) + [ ! -z "$NPC_API_LOCK" ] && DO_API=('flock' "$NPC_API_LOCK" "${DO_API[@]}") + [ ! -z "$NPC_API_SUCCEED_NO_RESPONSE" ] || ARGS=(--error "${ARGS[@]}") + local RESPONSE && RESPONSE="$("${DO_API[@]}" "${ARGS[@]}")" || { + [ ! -z "$OPTION_SILENCE" ] || echo "[ERROR] ${RESPONSE:-No response}" >&2 + return 1 + } + [ ! -z "$NPC_API_SUCCEED_NO_RESPONSE" ] && [ -z "$RESPONSE" ] && RESPONSE='{"ok":"no response"}' + [ ! -z "$RESPONSE" ] || { + [ ! -z "$OPTION_SILENCE" ] || echo "[ERROR] ${RESPONSE:-No response}" >&2 + return 1 + } + [ "${NPC_API:-api}" == "api" ] && { + jq_check .code <<<"$RESPONSE" && [ "$(jq -r .code <<<"$RESPONSE")" != "200" ] && { + [ ! -z "$OPTION_SILENCE" ] || echo "[ERROR] $RESPONSE" >&2 + return 1 + } + } + if [ ! -z "$FILTER" ]; then + jq -cre "($FILTER)//empty" <<<"$RESPONSE" && return 0 + else + jq_check '.' <<<"$RESPONSE" && return 0 + fi + [ ! -z "$OPTION_SILENCE" ] || echo "[ERROR] $RESPONSE" >&2 + return 1 } checked_api2(){ - NPC_API=api2 checked_api "$@" + NPC_API=api2 checked_api "$@" } diff --git a/files/npc-setup.image.sh b/files/npc-setup.image.sh index a408ef1..03f6140 100644 --- a/files/npc-setup.image.sh +++ b/files/npc-setup.image.sh @@ -7,10 +7,10 @@ init_instance_images(){ jq_check '.npc_instance_images|arrays' $INPUT && { plan_resources "$STAGE" \ <(jq -c '.npc_instance_images//[]' $INPUT || >>$STAGE.error) \ - <(npc api 'json.images|map({ - id: .imageId, - name: .imageName - })' GET "/api/v1/vm/privateimages?pageSize=9999&pageNum=1&keyword=" || >>$STAGE.error) \ + <(checked_api2 '.Images|map({ + id: .ImageId, + name: .ImageName + })' POST '/nvm?Action=DescribeImages&Version=2017-12-14&Limit=9999&Offset=0' '{"Filter":{"ImageType": ["Private"]}}' || >>$STAGE.error) \ '. + {update: false}' || return 1 } return 0 @@ -23,7 +23,7 @@ lookup_from_instance(){ } ( exec 100>$STAGE.lock && flock 100 [ ! -f $STAGE ] && { - load_instances '{id: .uuid,name: .name}' >$STAGE || rm -f $STAGE + load_instances '{id: .InstanceId,name: .InstanceName}' >$STAGE || rm -f $STAGE } ) [ -f $STAGE ] && INSTANCE_NAME="$INSTANCE_NAME" \ @@ -37,13 +37,13 @@ instance_images_create(){ local IMAGE="$1" RESULT="$2" CTX="$3" && [ ! -z "$IMAGE" ] || return 1 local FROM_INSTANCE="$(INSTANCES_LOOKUP_KEY='from_instances' instances_lookup "$(jq -r '.from_instance//empty'<<<"$IMAGE")")" \ && [ ! -z "$FROM_INSTANCE" ] || return 1 - local SAVE_IMAGE="$(FROM_INSTANCE="$FROM_INSTANCE" jq -c '{ - name: .name, - uuid: env.FROM_INSTANCE, - description: (.description//"created by npc-setup") - }'<<<"$IMAGE")" + local IMAGE_ID SAVE_IMAGE_PARAMS="$(FROM_INSTANCE="$FROM_INSTANCE" jq -r '{ + ImageName: .name, + InstanceId: env.FROM_INSTANCE, + Description: (.description//"created by npc-setup"|@base64) + }|to_entries|map(@uri"\(.key)=\(.value)")|join("&")'<<<"$IMAGE")" instances_wait_instance "$FROM_INSTANCE" "$CTX" \ - && local IMAGE_ID="$(checked_api '.imageId' POST "/api/v1/vm/privateimage" "$SAVE_IMAGE")" \ + && IMAGE_ID="$(checked_api2 '.ImageId' GET "/nvm?Action=CreateImage&Version=2017-12-14&$SAVE_IMAGE_PARAMS")" \ && [ ! -z "$IMAGE_ID" ] && instances_wait_instance "$FROM_INSTANCE" "$CTX" && { echo "[INFO] instance_image '$IMAGE_ID' saved." >&2 return 0 @@ -54,7 +54,7 @@ instance_images_create(){ instance_images_destroy(){ local IMAGE="$1" RESULT="$2" CTX="$3" && [ ! -z "$IMAGE" ] || return 1 local IMAGE_ID="$(jq -r .id<<<"$IMAGE")" && [ ! -z "$IMAGE_ID" ] || return 1 - checked_api DELETE "/api/v1/vm/privateimage/$IMAGE_ID" && { + checked_api2 GET "/nvm?Action=DeleteImage&Version=2017-12-14&ImageId=$IMAGE_ID" && { echo "[INFO] instance_image '$IMAGE_ID' destroyed." >&2 return 0 } diff --git a/files/npc-setup.instance.sh b/files/npc-setup.instance.sh index b002542..c333400 100644 --- a/files/npc-setup.instance.sh +++ b/files/npc-setup.instance.sh @@ -3,489 +3,413 @@ setup_resources "instances" MAPPER_PRE_LOAD_INSTANCE='{ - id: .uuid, - name: .name, - status: .status, - lan_ip: .vnet_ip, - zone: .azCode, + id: .InstanceId, + name: .InstanceName, + status: .Status, + lan_ip: (.PrivateIpAddresses[0]), + zone: (.Placement.ZoneId), - inet_ip: (.public_ip//false), - corp_ip: (.private_ip//false), - - actual_volumes: (.uuid as $uuid |.attached_volumes//[]|map({ - key:.name, - value:{ - name:.name, - instance_id: $uuid, - volume_uuid: .volumeId, - device: .mountPath - } - })|from_entries), - actual_wan_ip: (.public_ip//false), - actual_wan_id: (.public_port_id//false), - actual_wan_capacity: (if .bandWidth then "\(.bandWidth|tonumber)M" else false end), - actual_image: .images[0].imageName, - actual_type: { cpu:.vcpu, memory:"\(.memory_gb)G"} - }' + inet_ip: (.PublicIpAddresses[0]//false), + corp_ip: (.PrivateIdcIpAddresses[0]//false), + + actual_volumes: (.InstanceId as $uuid |.AttachVolumes//[]|map({ + key:.DiskName, + value:{ + id: .DiskId, + name:.DiskName, + instance_id: $uuid, +# v2 api 不支持的特性 +# volume_uuid: .volumeId, + device: .Device + } + })|from_entries), + + actual_wan_ip: (.PublicIpAddresses[0]//false), +# v2 api 不支持的特性 +# actual_wan_id: (.public_port_id//false), + actual_wan_capacity: (if .InternetMaxBandwidth then "\(.InternetMaxBandwidth|tonumber)M" else false end), + actual_image: (.Image.ImageName), + actual_type: { spec:.SpecType } + }' MAPPER_LOAD_INSTANCE='. + (if .volumes then - {volumes: ((.volumes//{}) * (.actual_volumes//{}))} - else {} end) - | . + {ssh_key_file:(.ssh_key_file//.default_ssh_key_file)} - | . + (if (.wan_ip=="new" or .wan_ip==true or .wan_ip=="any") and .actual_wan_ip then - {wan_ip: .actual_wan_ip} - else {} end)' + {volumes: ((.volumes//{}) * (.actual_volumes//{}))} + else {} end) + | . + {ssh_key_file:(.ssh_key_file//.default_ssh_key_file)} + | . + (if (.wan_ip=="new" or .wan_ip==true or .wan_ip=="any") and .actual_wan_ip then + {wan_ip: .actual_wan_ip} + else {} end)' FILTER_LOAD_INSTANCE='select(.)|$instance + .|'"$MAPPER_LOAD_INSTANCE" FILTER_INSTANCE_STATUS='.lan_ip and (.status=="ACTIVE" or .status=="SHUTOFF")' FILTER_PLAN_VOLUMES='. + (if .volumes then - {plan_volumes: ( - (.volumes//{} | with_entries(.value |= . + {actual_present: false})) - * (.actual_volumes//{} | with_entries(.value |= . + {actual_present: true})) - | with_entries(.value |= . + { - mount: ((.actual_present|not) and .present), - unmount: (.actual_present and (.present == false)) - }) - )} - else {} end)' + {plan_volumes: ( + (.volumes//{} | with_entries(.value |= . + {actual_present: false})) + * (.actual_volumes//{} | with_entries(.value |= . + {actual_present: true})) + | with_entries(.value |= . + { + mount: ((.actual_present|not) and .present), + unmount: (.actual_present and (.present == false)) + }) + )} + else {} end)' init_instances(){ - local INPUT="$1" STAGE="$2" + local INPUT="$1" STAGE="$2" - jq_check '.npc_instances|arrays' $INPUT && { - plan_resources "$STAGE" \ - <(jq -c '. as $input | .npc_instances | map( . - + ( if env.NPC_SSH_KEY|length>0 then {ssh_keys:((.ssh_keys//[])+[env.NPC_SSH_KEY]|unique)} else {} end ) - + ( if env.NPC_SSH_KEY_FILE|length>0 then {default_ssh_key_file: env.NPC_SSH_KEY_FILE} else {} end ) - )' $INPUT || >>$STAGE.error) \ - <(load_instances "$MAPPER_PRE_LOAD_INSTANCE"' - | if '"$FILTER_INSTANCE_STATUS"' then . else . + {error: "\(.name): status=\(.status), lan_ip=\(.lan_ip)"} end - '|| >>$STAGE.error) \ - '. + (if .volumes then {volumes: (.volumes|map( - if (strings//false) then { key:., value: {name:., present: true}} else { key: .name, value: ({present: true} + .) } end - )|from_entries)} else {} end) - |'"$MAPPER_LOAD_INSTANCE"' - |. + (if .wan_ip then - if .actual_wan_ip|not then - {plan_wan_ip:{bind: true}} - elif .wan_ip != .actual_wan_ip then - {plan_wan_ip:{bind: true, unbind: true}} - elif .wan_capacity and (.wan_capacity|ascii_upcase) != .actual_wan_capacity then - {plan_wan_ip:{bind: true, unbind: true, rebind:true}} - else {} end - elif .wan_ip == false and .actual_wan_ip then - {plan_wan_ip:{unbind: true}} - else {} end) - |'"$FILTER_PLAN_VOLUMES"' - |. + (if .update then {update: false} -# do not recreate -# + if .instance_type and .actual_type != .instance_type then -# {update: true, recreate: true} -# else {} end -# + if .instance_image and .actual_image != .instance_image then -# {update: true, recreate: true} -# else {} end - + if .plan_volumes and (.plan_volumes|map(select(.mount or .unmount))|length>0) then - {update: true, update_volumes: true} - else {} end - + if .plan_wan_ip then - {update: true} - else {} end - else {} end)' || return 1 - } - return 0 + jq_check '.npc_instances|arrays' $INPUT && { + plan_resources "$STAGE" \ + <(jq -c '. as $input | .npc_instances | map( . + + ( if env.NPC_SSH_KEY|length>0 then {ssh_keys:((.ssh_keys//[])+[env.NPC_SSH_KEY]|unique)} else {} end ) + + ( if env.NPC_SSH_KEY_FILE|length>0 then {default_ssh_key_file: env.NPC_SSH_KEY_FILE} else {} end ) + )' $INPUT || >>$STAGE.error) \ + <(load_instances "$MAPPER_PRE_LOAD_INSTANCE"' + | if '"$FILTER_INSTANCE_STATUS"' then . else . + {error: "\(.name): status=\(.status), lan_ip=\(.lan_ip)"} end + '|| >>$STAGE.error) \ + '. + (if .volumes then {volumes: (.volumes|map( + if (strings//false) then { key:., value: {name:., present: true}} else { key: .name, value: ({present: true} + .) } end + )|from_entries)} else {} end) + |'"$MAPPER_LOAD_INSTANCE"' + |. + (if .wan_ip then + if .actual_wan_ip|not then + {plan_wan_ip:{bind: true}} + elif .wan_ip != .actual_wan_ip then + {plan_wan_ip:{bind: true, unbind: true}} + elif .wan_capacity and (.wan_capacity|ascii_upcase) != .actual_wan_capacity then + {plan_wan_ip:{bind: true, unbind: true, rebind:true}} + else {} end + elif .wan_ip == false and .actual_wan_ip then + {plan_wan_ip:{unbind: true}} + else {} end) + |'"$FILTER_PLAN_VOLUMES"' + |. + (if .update then {update: false} +# 废弃 recreate 选项 +# + if .instance_type and .actual_type != .instance_type then +# {update: true, recreate: true} +# else {} end +# + if .instance_image and .actual_image != .instance_image then +# {update: true, recreate: true} +# else {} end +# load_instances 列表不返回 actual_volumes 无法提前判断是否需要更新, 需要总是 update_volumes +# + if .plan_volumes and (.plan_volumes|map(select(.mount or .unmount))|length>0) then + + if .plan_volumes then + {update: true, update_volumes: true} + else {} end + + if .plan_wan_ip then + {update: true} + else {} end + else {} end)' || return 1 + } + return 0 } instances_lookup_image(){ - local IMAGE_NAME="$1" - for IMAGE_TYPE in "privateimages" "publicimages"; do - local STAGE="$NPC_STAGE/$IMAGE_TYPE" - ( exec 100>$STAGE.lock && flock 100 - [ ! -f $STAGE ] && { - npc api 'json.images' GET "/api/v1/vm/$IMAGE_TYPE?pageSize=9999&pageNum=1&keyword=" >$STAGE || rm -f $STAGE - } - ) - [ -f $STAGE ] && IMAGE_NAME="$IMAGE_NAME" \ - jq -re '(env.IMAGE_NAME | capture("^/(?.+)/(?[a-z]+)?$|^(?.+)$")) as $lookup - |map(select( - ($lookup.text and (.imageName==$lookup.text or .imageId==$lookup.text)) or - ($lookup.regex and (.imageName | test($lookup.regex;$lookup.flags))) )) - |.[0]//empty|.imageId//empty' $STAGE \ - && return 0 - done - echo "[ERROR] instance_image - '$IMAGE_NAME' not found" >&2 - return 1 -} - -instances_acquire_ip(){ - local LOOKUP_IP="$1" STAGE="$NPC_STAGE/nce-ips" - [ ! -z "$LOOKUP_IP" ] || return 1 - [ "$LOOKUP_IP" != "new" ] && [ "$LOOKUP_IP" != "true" ] && { - ( exec 100>$STAGE.lock && flock 100 - [ ! -f $STAGE ] && { - npc api 'json.ips|arrays' GET '/api/v1/ips?status=available&type=nce&offset=0&limit=9999' >$STAGE || rm -f $STAGE - } - [ -f $STAGE ] && LOOKUP_IP="$LOOKUP_IP" jq -c '(map(select(env.LOOKUP_IP=="any" or .ip==env.LOOKUP_IP))|.[0])as $match | if $match then ($match, map(select(.id != $match.id))) else empty end' $STAGE | { - read -r MATCH && [ ! -z "$MATCH" ] && read -r IPS && echo "$IPS" >$STAGE && echo "$MATCH" - } - ) && return 0 - [ "$LOOKUP_IP" != "any" ] && { - echo "[ERROR] ip=$LOOKUP_IP not available" >&2 - return 1 - } - } - npc api 'json.ips|arrays|.[0]//empty' POST '/api/v1/ips' '{"nce": 1}' && return 0 - echo "[ERROR] failed to create ip" >&2 - return 1 + local IMAGE_NAME="$1" + for IMAGE_TYPE in "Private" "Public"; do + local STAGE="$NPC_STAGE/$IMAGE_TYPE" + ( exec 100>$STAGE.lock && flock 100 + [ ! -f $STAGE ] && { + npc api2 'json.Images' POST '/nvm?Action=DescribeImages&Version=2017-12-14&Limit=9999&Offset=0' \ + "$(export IMAGE_TYPE && jq -nc '{Filter: {ImageType: [env.IMAGE_TYPE]}}')" >$STAGE || rm -f $STAGE + } + ) + [ -f $STAGE ] && IMAGE_NAME="$IMAGE_NAME" \ + jq -re '(env.IMAGE_NAME | capture("^/(?.+)/(?[a-z]+)?$|^(?.+)$")) as $lookup + |map(select( + ($lookup.text and (.ImageName==$lookup.text or .ImageId==$lookup.text)) or + ($lookup.regex and (.ImageName | test($lookup.regex;$lookup.flags))) )) + |.[0]//empty|.ImageId//empty' $STAGE \ + && return 0 + done + echo "[ERROR] instance_image - '$IMAGE_NAME' not found" >&2 + return 1 } instances_prepare(){ - local INSTANCE="$1" - jq -ce 'select(.prepared)'<<<"$INSTANCE" && return 0 + local INSTANCE="$1" + jq -ce 'select(.prepared)'<<<"$INSTANCE" && return 0 - local IMAGE_ID INSTANCE_TYPE_CONFIG="{}" SSH_KEYS_CONFIG="{}" - jq_check '.create or .recreate'<<<"$INSTANCE" && { - local IMAGE_NAME="$(jq -r '.instance_image//.default_instance_image//empty'<<<"$INSTANCE")" && [ ! -z "$IMAGE_NAME" ] || { - echo '[ERROR] instance_image required.' >&2 - return 1 - } - IMAGE_ID="$(instances_lookup_image "$IMAGE_NAME")" && [ ! -z "$IMAGE_ID" ] || return 1 + local IMAGE_ID INSTANCE_TYPE_CONFIG="{}" SSH_KEYS_CONFIG="{}" + jq_check '.create or .recreate'<<<"$INSTANCE" && { + local IMAGE_NAME="$(jq -r '.instance_image//.default_instance_image//empty'<<<"$INSTANCE")" && [ ! -z "$IMAGE_NAME" ] || { + echo '[ERROR] instance_image required.' >&2 + return 1 + } + IMAGE_ID="$(instances_lookup_image "$IMAGE_NAME")" && [ ! -z "$IMAGE_ID" ] || return 1 - INSTANCE_TYPE_CONFIG="$(instance_type_normalize "$(jq -c '.instance_type//empty'<<<"$INSTANCE")" '{instance_type: .}')" - [ ! -z "$INSTANCE_TYPE_CONFIG" ] || return 1 + INSTANCE_TYPE_CONFIG="$(instance_type_normalize "$(jq -c '.instance_type//empty'<<<"$INSTANCE")" '{instance_type: .}')" + [ ! -z "$INSTANCE_TYPE_CONFIG" ] || return 1 - jq -r '.ssh_keys//empty|.[]'<<<"$INSTANCE" | check_ssh_keys && \ - SSH_KEYS_CONFIG="$(jq -r '.ssh_keys//empty|.[]'<<<"$INSTANCE" | check_ssh_keys --output '.' | jq -sc '{checked_ssh_keys: .}')" && \ - [ ! -z "$SSH_KEYS_CONFIG" ] || return 1 - } - - local PLAN_VOLUMES PLAN_VOLUMES_CONFIG='{}' - jq_check '.plan_volumes'<<<"$INSTANCE" && { - while read -r VOLUME; do - local VOLUME_NAME="$(jq -r '.name'<<<"$VOLUME")" - local VOLUME_ID="$(volumes_lookup "$VOLUME_NAME" '.id')" && [ ! -z "$VOLUME_ID" ] || return 1 - [ ! -z "$PLAN_VOLUMES" ] || PLAN_VOLUMES="$(jq -c '.plan_volumes//empty'<<<"$INSTANCE")" - PLAN_VOLUMES="$(export VOLUME_NAME VOLUME_ID; jq -c '.[env.VOLUME_NAME] |= . + {id: env.VOLUME_ID}'<<<"$PLAN_VOLUMES")" - done < <(jq -c '.plan_volumes[]|select(.present and (.volume_uuid|not))'<<<"$INSTANCE") - [ ! -z "$PLAN_VOLUMES" ] && PLAN_VOLUMES_CONFIG="$(jq -c '{plan_volumes: .}'<<<"$PLAN_VOLUMES")" - } + jq -r '.ssh_keys//empty|.[]'<<<"$INSTANCE" | check_ssh_keys && \ + SSH_KEYS_CONFIG="$(jq -r '.ssh_keys//empty|.[]'<<<"$INSTANCE" | check_ssh_keys --output '.' | jq -sc '{checked_ssh_keys: .}')" && \ + [ ! -z "$SSH_KEYS_CONFIG" ] || return 1 + } + + local PLAN_VOLUMES PLAN_VOLUMES_CONFIG='{}' + jq_check '.plan_volumes'<<<"$INSTANCE" && { + while read -r VOLUME; do + local VOLUME_NAME="$(jq -r '.name'<<<"$VOLUME")" + local VOLUME_ID="$(volumes_lookup "$VOLUME_NAME" '.id')" && [ ! -z "$VOLUME_ID" ] || return 1 + [ ! -z "$PLAN_VOLUMES" ] || PLAN_VOLUMES="$(jq -c '.plan_volumes//empty'<<<"$INSTANCE")" + PLAN_VOLUMES="$(export VOLUME_NAME VOLUME_ID; jq -c '.[env.VOLUME_NAME] |= . + {id: env.VOLUME_ID}'<<<"$PLAN_VOLUMES")" + done < <(jq -c '.plan_volumes[]|select(.present and (.id|not))'<<<"$INSTANCE") + [ ! -z "$PLAN_VOLUMES" ] && PLAN_VOLUMES_CONFIG="$(jq -c '{plan_volumes: .}'<<<"$PLAN_VOLUMES")" + } - local WAN_IP - jq_check '.plan_wan_ip and .plan_wan_ip.bind and (.plan_wan_ip.rebind|not)'<<<"$INSTANCE" && { - WAN_IP="$(instances_acquire_ip "$(jq -r '.wan_ip'<<<"$INSTANCE")")" && [ ! -z "$WAN_IP" ] || return 1 - } - local WAN_CONFIG="$(jq --argjson acquired "${WAN_IP:-"{}"}" -c '{ - wan_ip: ($acquired.ip//.wan_ip//.actual_wan_ip), - wan_id: ($acquired.id//.wan_id//.actual_wan_id), - wan_capacity: (.wan_capacity//.actual_wan_capacity) - } | select(.wan_ip)//{}'<<<"$INSTANCE")" + local WAN_IP + jq_check '.plan_wan_ip and .plan_wan_ip.bind and (.plan_wan_ip.rebind|not)'<<<"$INSTANCE" && { +# deprecated @ 2019-02-21 +# WAN_IP="$(instances_acquire_ip "$(jq -r '.wan_ip'<<<"$INSTANCE")")" && [ ! -z "$WAN_IP" ] || return 1 + echo "[ERROR] acquire wan ip not supported" >&2 + return 1 + } + local WAN_CONFIG="$(jq --argjson acquired "${WAN_IP:-"{}"}" -c '{ + wan_ip: ($acquired.ip//.wan_ip//.actual_wan_ip), + wan_id: ($acquired.id//.wan_id//.actual_wan_id), + wan_capacity: (.wan_capacity//.actual_wan_capacity) + } | select(.wan_ip)//{}'<<<"$INSTANCE")" - local VPC_CONFIG="{}" VPC_NETWORK VPC_SUBNET VPC_SECURITY_GROUP - jq_check '.vpc_network//.vpc'<<<"$INSTANCE" && { - VPC_NETWORK="$(vpc_networks_lookup "$(jq -r '.vpc_network//.vpc//empty'<<<"$INSTANCE")")" \ - && [ ! -z "$VPC_NETWORK" ] || return 1 - VPC_SUBNET="$(vpc_subnets_lookup "$(jq -r '.vpc_subnet//empty'<<<"$INSTANCE")" "$VPC_NETWORK")" \ - && [ ! -z "$VPC_SUBNET" ] || return 1 - VPC_SECURITY_GROUP="$(vpc_security_groups_lookup "$(jq -r '.vpc_security_group//empty'<<<"$INSTANCE")" "$VPC_NETWORK")" \ - && [ ! -z "$VPC_SECURITY_GROUP" ] || return 1 - VPC_CONFIG="$(export VPC_NETWORK VPC_SUBNET VPC_SECURITY_GROUP; jq -nc '{ - vpc_network: env.VPC_NETWORK, - vpc_subnet: env.VPC_SUBNET, - vpc_security_group: env.VPC_SECURITY_GROUP - }')" - } + local VPC_CONFIG="{}" VPC_NETWORK VPC_SUBNET VPC_SECURITY_GROUP + jq_check '.vpc_network//.vpc'<<<"$INSTANCE" && { + VPC_NETWORK="$(vpc_networks_lookup "$(jq -r '.vpc_network//.vpc//empty'<<<"$INSTANCE")")" \ + && [ ! -z "$VPC_NETWORK" ] || return 1 + VPC_SUBNET="$(vpc_subnets_lookup "$(jq -r '.vpc_subnet//empty'<<<"$INSTANCE")" "$VPC_NETWORK")" \ + && [ ! -z "$VPC_SUBNET" ] || return 1 + VPC_SECURITY_GROUP="$(vpc_security_groups_lookup "$(jq -r '.vpc_security_group//empty'<<<"$INSTANCE")" "$VPC_NETWORK")" \ + && [ ! -z "$VPC_SECURITY_GROUP" ] || return 1 + VPC_CONFIG="$(export VPC_NETWORK VPC_SUBNET VPC_SECURITY_GROUP; jq -nc '{ + vpc_network: env.VPC_NETWORK, + vpc_subnet: env.VPC_SUBNET, + vpc_security_group: env.VPC_SECURITY_GROUP + }')" + } - IMAGE_ID="$IMAGE_ID" \ - jq -c '. + { - prepared: true, - instance_image_id: env.IMAGE_ID - }'" +$INSTANCE_TYPE_CONFIG +$PLAN_VOLUMES_CONFIG +$WAN_CONFIG +$VPC_CONFIG +$SSH_KEYS_CONFIG"<<<"$INSTANCE" && return 0 || return 1 + IMAGE_ID="$IMAGE_ID" \ + jq -c '. + { + prepared: true, + instance_image_id: env.IMAGE_ID + }'" +$INSTANCE_TYPE_CONFIG +$PLAN_VOLUMES_CONFIG +$WAN_CONFIG +$VPC_CONFIG +$SSH_KEYS_CONFIG"<<<"$INSTANCE" && return 0 || return 1 } instances_wait_instance(){ - local INSTANCE INSTANCE_ID="$1" CTX="$2" && shift && shift && [ ! -z "$INSTANCE_ID" ] || return 1 - local ARGS=("$@") && (( ${#ARGS[@]} > 0)) || ARGS=('select(.)') - while action_check_continue "$CTX"; do - INSTANCE="$(npc api "json|$MAPPER_PRE_LOAD_INSTANCE" GET "/api/v1/vm/$INSTANCE_ID")" && [ ! -z "$INSTANCE" ] && { - jq_check 'select(.status=="ERROR")'<<<"$INSTANCE" && { - echo "[ERROR] instance '$INSTANCE_ID' status 'ERROR'." >&2 - return 9 - } - jq "select($FILTER_INSTANCE_STATUS)"<<<"$INSTANCE" | jq_check "${ARGS[@]}" && return 0 - } - sleep "$NPC_ACTION_PULL_SECONDS" - done - return 1 + local INSTANCE INSTANCE_ID="$1" CTX="$2" && shift && shift && [ ! -z "$INSTANCE_ID" ] || return 1 + local ARGS=("$@") && (( ${#ARGS[@]} > 0)) || ARGS=('select(.)') + while action_check_continue "$CTX"; do + INSTANCE="$(npc api2 "json|$MAPPER_PRE_LOAD_INSTANCE" GET "/nvm?Action=DescribeInstance&Version=2017-12-14&InstanceId=$INSTANCE_ID")" && [ ! -z "$INSTANCE" ] && { + jq_check 'select(.status=="ERROR")'<<<"$INSTANCE" && { + echo "[ERROR] instance '$INSTANCE_ID' status 'ERROR'." >&2 + return 9 + } + jq "select($FILTER_INSTANCE_STATUS)"<<<"$INSTANCE" | jq_check "${ARGS[@]}" && return 0 + } + sleep "$NPC_ACTION_PULL_SECONDS" + done + return 1 } instances_create(){ - local INSTANCE="$(instances_prepare "$1")" RESULT="$2" CTX="$3" && [ -z "$INSTANCE" ] && return 1 - local API2_CREATE='' API_CREATE='api_create_instance'; jq_check '.vpc_network'<<<"$INSTANCE" && { - API2_CREATE='api2_create_instance' - API_CREATE="$API2_CREATE" - } - while true; do - local RESPONSE="$("$API_CREATE" "$INSTANCE" "$CTX")" && [ ! -z "$RESPONSE" ] || return 1 - local INSTANCE_ID="$(jq -r '.id//empty'<<<"$RESPONSE")" && [ ! -z "$INSTANCE_ID" ] \ - && instances_wait_instance "$INSTANCE_ID" "$CTX" \ - && { - echo "[INFO] instance '$INSTANCE_ID' created." >&2 - [ ! -z "$API2_CREATE" ] || { - instances_update_volumes "$INSTANCE_ID" "$INSTANCE" "$CTX" || return 1 - instances_update_wan "$INSTANCE_ID" "$INSTANCE" "$CTX" || return 1 - } - instances_wait_instance "$INSTANCE_ID" "$CTX" \ - --argjson instance "$INSTANCE" "$FILTER_LOAD_INSTANCE" --out $RESULT || return 1 - return 0 - } + local INSTANCE="$(instances_prepare "$1")" RESULT="$2" CTX="$3" && [ -z "$INSTANCE" ] && return 1 + jq_check '.vpc_network'<<<"$INSTANCE" || { +# 废弃非 vpc 主机创建 @ 2019-02-21 + echo "[ERROR] create non-vpc instance not supported" >&2 + return 1 + } + while true; do + local RESPONSE="$(api2_create_instance "$INSTANCE" "$CTX")" && [ ! -z "$RESPONSE" ] || return 1 + local INSTANCE_ID="$(jq -r '.id//empty'<<<"$RESPONSE")" && [ ! -z "$INSTANCE_ID" ] \ + && instances_wait_instance "$INSTANCE_ID" "$CTX" \ + && { + echo "[INFO] instance '$INSTANCE_ID' created." >&2 +# 废弃创建后 wan 和 volumes 更新 @ 2019-02-21 +# [ ! -z "$API2_CREATE" ] || { +# instances_update_volumes "$INSTANCE_ID" "$INSTANCE" "$CTX" || return 1 +# instances_update_wan "$INSTANCE_ID" "$INSTANCE" "$CTX" || return 1 +# } + instances_wait_instance "$INSTANCE_ID" "$CTX" \ + --argjson instance "$INSTANCE" "$FILTER_LOAD_INSTANCE" --out $RESULT || return 1 + return 0 + } - # status == ERROR, @See instances_wait_instance - [ "$?" == "9" ] && { - instances_destroy "$(export INSTANCE_ID && jq -nc '{id: env.INSTANCE_ID}')" "$RESULT" "$CTX" && continue - return 1 - } + # status == ERROR, @See instances_wait_instance + [ "$?" == "9" ] && { + instances_destroy "$(export INSTANCE_ID && jq -nc '{id: env.INSTANCE_ID}')" "$RESULT" "$CTX" && continue + return 1 + } - echo "[ERROR] $RESPONSE" >&2 - # {"code":4030001,"msg":"Api freq out of limit."} - [ "$(jq -r .code <<<"$RESPONSE")" = "4030001" ] && ( - exec 100>$NPC_STAGE/instances.retries && flock 100 \ - && action_sleep "$NPC_ACTION_RETRY_SECONDS" "$CTX" ) && continue - return 1 - done -} - -api_create_instance(){ - local INSTANCE="$1" && local CREATE_INSTANCE="$(jq -c '{ - bill_info: "HOUR", - server_info: ({ - azCode: (.zone//.az), - instance_name: .name, - ssh_key_names: (.ssh_keys//[]), - image_id: .instance_image_id, - cpu_weight: (.instance_type.cpu//0), - memory_weight: ((.instance_type.memory//"0G")|sub("[Gg]$"; "")|tonumber), - ssd_weight: ((.instance_type.ssd//"20G")|sub("[Gg]$"; "")|tonumber), - type: (.instance_type.type//.default_instance_type.type), - series: (.instance_type.series//.default_instance_type.series), - useVPC: (if .vpc_network then true else false end), - networkId: (if .vpc_network then .vpc_network else false end), - subnetId: (if .vpc_network then .vpc_subnet else false end), - securityGroup:(if .vpc_network then .vpc_security_group else false end), - usePrivateIP: (if .vpc_network and .vpc_corp then true else false end), - useLifeCycleIP: (if .vpc_network and .vpc_inet then true else false end), - bandwidth: (if .vpc_network and .vpc_inet then - (.vpc_inet_capacity//"1M"|sub("[Mm]$"; "")|tonumber) - else false end), - description: (.description//"created by npc-setup") - } | with_entries(select(.value))) - }'<<<"$INSTANCE")" - ( - exec 100>$NPC_STAGE/instances.create_lock && flock 100 - local RESPONSE="$(npc api --error 'json|((arrays|{id:.[0]})//{})+(objects//{})' POST /api/v1/vm "$CREATE_INSTANCE")" \ - && echo "$RESPONSE" \ - && [ ! -z "$RESPONSE" ] \ - && local INSTANCE_ID="$(jq -r '.id//empty'<<<"$RESPONSE")" \ - && [ ! -z "$INSTANCE_ID" ] - # \ - #&& sleep 1s; # 等待1秒,避免 Api freq out of limit - ) + echo "[ERROR] $RESPONSE" >&2 + # {"code":4030001,"msg":"Api freq out of limit."} + [ "$(jq -r .code <<<"$RESPONSE")" = "4030001" ] && ( + exec 100>$NPC_STAGE/instances.retries && flock 100 \ + && action_sleep "$NPC_ACTION_RETRY_SECONDS" "$CTX" ) && continue + return 1 + done } api2_create_instance(){ - local INSTANCE="$1" CTX="$2" PLAN_VOLUME + local INSTANCE="$1" CTX="$2" PLAN_VOLUME - jq_check '.plan_volumes'<<<"$INSTANCE" && while read -r PLAN_VOLUME; do - local VOLUME_NAME="$(jq -r '.name'<<<"$PLAN_VOLUME")" VOLUME_ID="$(jq -r '.id'<<<"$PLAN_VOLUME")" - [ ! -z "$VOLUME_NAME" ] && [ ! -z "$VOLUME_ID" ] || { - echo "[ERROR] invalid instance volume '$PLAN_VOLUME'." >&2 - return 1 - } - local VOLUME="$(volumes_wait_status "$VOLUME_ID" "$CTX" '.')" && [ ! -z "$VOLUME" ] || return 1 - jq_check '.available'<<<"$VOLUME" || { - echo "[ERROR] volume '$VOLUME_NAME' not available" >&2 - return 1 - } - done < <(jq -c '.plan_volumes[]|select(.present)'<<<"$INSTANCE") - - local CREATE_INSTANCE="$(jq -c '{ - PayType: "PostPaid", - InstanceName: .name, - ImageId: .instance_image_id, - SpecType: .instance_type.spec, - KeyPairNames: (.checked_ssh_keys|map({name:.name, fingerprint: .fingerprint})), - Placement: ({ - ZoneId: (.zone//.az) - }|with_entries(select(.value))), - VirtualPrivateCloud: ({ - VpcId: .vpc_network, - SubnetId: (if .vpc_network then .vpc_subnet else false end) - }|with_entries(select(.value))), - SecurityGroupIds: (if .vpc_network then [.vpc_security_group] else [] end), - AssociatePublicIpAddress: (if .vpc_network and .vpc_inet then true else false end), - InternetMaxBandwidth: (if .vpc_network and .vpc_inet then - (.vpc_inet_capacity//"1M"|sub("[Mm]$"; "")|tonumber) - else false end), - NetworkChargeType: (if .vpc_network and .vpc_inet then "TRAFFIC" else false end), - AssociatePrivateIdcIpAddress: (if .vpc_network and .vpc_corp then true else false end), - Personality: (.user_data//{} | to_entries | map({ - Path: .key, - Contents: .value - }) | select(length > 0) // false), - DataVolumes: (.data_volumes//[] | map({ - VolumeType: (.type//"EPHEMERAL"), - VolumeSize: (.capacity|sub("[Gg]$"; "")|tonumber) - }) | select(length > 0) // false), - AttachVolumeIds: (if .plan_volumes then (.plan_volumes|map(select(.present)|.id)) else false end), - Description: .description - } | with_entries(select(.value))'<<<"$INSTANCE")" - ( - export INSTANCE_ID="$(NPC_API_LOCK="$NPC_STAGE/instances.create_lock" checked_api2 '.Instances//[]|.[0]//empty' POST "/nvm?Action=CreateInstance&Version=2017-12-14" "$CREATE_INSTANCE")" \ - && [ ! -z "$INSTANCE_ID" ] && jq -nr '{ id: env.INSTANCE_ID }' - ) + jq_check '.plan_volumes'<<<"$INSTANCE" && while read -r PLAN_VOLUME; do + local VOLUME_NAME="$(jq -r '.name'<<<"$PLAN_VOLUME")" VOLUME_ID="$(jq -r '.id'<<<"$PLAN_VOLUME")" + [ ! -z "$VOLUME_NAME" ] && [ ! -z "$VOLUME_ID" ] || { + echo "[ERROR] invalid instance volume '$PLAN_VOLUME'." >&2 + return 1 + } + local VOLUME="$(volumes_wait_status "$VOLUME_ID" "$CTX" '.')" && [ ! -z "$VOLUME" ] || return 1 + jq_check '.available'<<<"$VOLUME" || { + echo "[ERROR] volume '$VOLUME_NAME' not available" >&2 + return 1 + } + done < <(jq -c '.plan_volumes[]|select(.present)'<<<"$INSTANCE") + + local CREATE_INSTANCE="$(jq -c '{ + PayType: "PostPaid", + InstanceName: .name, + ImageId: .instance_image_id, + SpecType: .instance_type.spec, + KeyPairNames: (.checked_ssh_keys|map({name:.name, fingerprint: .fingerprint})), + Placement: ({ + ZoneId: (.zone//.az) + }|with_entries(select(.value))), + VirtualPrivateCloud: ({ + VpcId: .vpc_network, + SubnetId: (if .vpc_network then .vpc_subnet else false end) + }|with_entries(select(.value))), + SecurityGroupIds: (if .vpc_network then [.vpc_security_group] else [] end), + AssociatePublicIpAddress: (if .vpc_network and .vpc_inet then true else false end), + InternetMaxBandwidth: (if .vpc_network and .vpc_inet then + (.vpc_inet_capacity//"1M"|sub("[Mm]$"; "")|tonumber) + else false end), + NetworkChargeType: (if .vpc_network and .vpc_inet then "TRAFFIC" else false end), + AssociatePrivateIdcIpAddress: (if .vpc_network and .vpc_corp then true else false end), + Personality: (.user_data//{} | to_entries | map({ + Path: .key, + Contents: .value + }) | select(length > 0) // false), + DataVolumes: (.data_volumes//[] | map({ + VolumeType: (.type//"EPHEMERAL"), + VolumeSize: (.capacity|sub("[Gg]$"; "")|tonumber) + }) | select(length > 0) // false), + AttachVolumeIds: (if .plan_volumes then (.plan_volumes|map(select(.present)|.id)) else false end), + Description: .description + } | with_entries(select(.value))'<<<"$INSTANCE")" + ( + export INSTANCE_ID="$(NPC_API_LOCK="$NPC_STAGE/instances.create_lock" checked_api2 '.Instances//[]|.[0]//empty' POST "/nvm?Action=CreateInstance&Version=2017-12-14" "$CREATE_INSTANCE")" \ + && [ ! -z "$INSTANCE_ID" ] && jq -nr '{ id: env.INSTANCE_ID }' + ) } instances_update_volumes(){ - local INSTANCE_ID="$1" INSTANCE="$2" CTX="$3" MOUNT_FILTER UNMOUNT_FILTER WAIT_INSTANCE - if jq_check '.volumes and (.create or .recreate)'<<<"$INSTANCE"; then - # 云主机操作系统未就绪可能导致绑定云硬盘失败 - WAIT_INSTANCE="Y" - MOUNT_FILTER='select(.present)' - elif jq_check '.volumes and .update and .update_volumes'<<<"$INSTANCE"; then - INSTANCE="$(instances_wait_instance "$INSTANCE_ID" "$CTX" --stdout \ - --argjson instance "$INSTANCE" \ - "$FILTER_LOAD_INSTANCE | $FILTER_PLAN_VOLUMES")" \ - && [ ! -z "$INSTANCE" ] || return 1 - jq_check '.plan_volumes and (.plan_volumes|map(select(.mount or .unmount))|length>0)'<<<"$INSTANCE" || return 0 - MOUNT_FILTER='select(.mount)' - UNMOUNT_FILTER='select(.unmount)' - else - return 0 - fi - [ ! -z "$UNMOUNT_FILTER" ] && { - while read -r VOLUME_NAME; do - volumes_unmount "$INSTANCE_ID" "$VOLUME_NAME" "$CTX" || return 1 - done < <(jq -cr ".plan_volumes[]|$UNMOUNT_FILTER|.name"<<<"$INSTANCE") - } - [ ! -z "$MOUNT_FILTER" ] && { - while read -r VOLUME_NAME; do - volumes_mount "$INSTANCE_ID" "$VOLUME_NAME" "$CTX" "$WAIT_INSTANCE" || return 1 - done < <(jq -cr ".plan_volumes[]|$MOUNT_FILTER|.name"<<<"$INSTANCE") - } - return 0 -} - -instances_update_wan(){ - local INSTANCE_ID="$1" INSTANCE="$2" CTX="$3" UPDATE_LOCK="$NPC_STAGE/instances.update_wan" - jq_check '.update and (.recreate|not) and .plan_wan_ip and .plan_wan_ip.unbind'<<<"$INSTANCE" &&{ - local PARAMS="$(jq -r '@uri "pubIp=\(.actual_wan_ip)&portId=\(.actual_wan_id)"'<<<"$INSTANCE")" - (exec 100>$UPDATE_LOCK && flock 100 && checked_api DELETE "/api/v1/vm/$INSTANCE_ID/action/unmountPublicIp?$PARAMS") || { - echo '[ERROR] failed to unbind wan_ip.' >&2 - return 1 - } - while action_check_continue "$CTX"; do - npc api 'json|select(.status=="available")' \ - GET "$(jq -r '@uri "/api/v1/ips/\(.actual_wan_id)"'<<<"$INSTANCE")" \ - | jq_check '.ip' && break - sleep "$NPC_ACTION_PULL_SECONDS" - done || return 1 - } - jq_check '.plan_wan_ip and .plan_wan_ip.bind'<<<"$INSTANCE" &&{ - local PARAMS="$(jq -r '@uri "pubIp=\(.wan_ip)&portId=\(.wan_id)&qosMode=netflow&bandWidth=\(.wan_capacity//"1M"|sub("[Mm]$"; "")|tonumber)"'<<<"$INSTANCE")" - (exec 100>$UPDATE_LOCK && flock 100 && checked_api PUT "/api/v1/vm/$INSTANCE_ID/action/mountPublicIp?$PARAMS") || { - echo '[ERROR] failed to bind wan_ip.' >&2 - return 1 - } - while action_check_continue "$CTX"; do - npc api 'json|select(.status=="binded")' \ - GET "$(jq -r '@uri "/api/v1/ips/\(.wan_id)"'<<<"$INSTANCE")" \ - | jq_check '.ip' && break - sleep "$NPC_ACTION_PULL_SECONDS" - done || return 1 - } - return 0 + local INSTANCE_ID="$1" INSTANCE="$2" CTX="$3" MOUNT_FILTER UNMOUNT_FILTER WAIT_INSTANCE + if jq_check '.volumes and (.create or .recreate)'<<<"$INSTANCE"; then + # 云主机操作系统未就绪可能导致绑定云硬盘失败 + WAIT_INSTANCE="Y" + MOUNT_FILTER='select(.present)' + elif jq_check '.volumes and .update and .update_volumes'<<<"$INSTANCE"; then + INSTANCE="$(instances_wait_instance "$INSTANCE_ID" "$CTX" --stdout \ + --argjson instance "$INSTANCE" \ + "$FILTER_LOAD_INSTANCE | $FILTER_PLAN_VOLUMES")" \ + && [ ! -z "$INSTANCE" ] || return 1 + jq_check '.plan_volumes and (.plan_volumes|map(select(.mount or .unmount))|length>0)'<<<"$INSTANCE" || return 0 + MOUNT_FILTER='select(.mount)' + UNMOUNT_FILTER='select(.unmount)' + else + return 0 + fi + [ ! -z "$UNMOUNT_FILTER" ] && { + while read -r VOLUME_NAME; do + volumes_unmount "$INSTANCE_ID" "$VOLUME_NAME" "$CTX" || return 1 + done < <(jq -cr ".plan_volumes[]|$UNMOUNT_FILTER|.name"<<<"$INSTANCE") + } + [ ! -z "$MOUNT_FILTER" ] && { + while read -r VOLUME_NAME; do + volumes_mount "$INSTANCE_ID" "$VOLUME_NAME" "$CTX" "$WAIT_INSTANCE" || return 1 + done < <(jq -cr ".plan_volumes[]|$MOUNT_FILTER|.name"<<<"$INSTANCE") + } + return 0 } instances_update(){ - local INSTANCE="$(instances_prepare "$1")" RESULT="$2" CTX="$3" && [ -z "$INSTANCE" ] && return 1 - jq_check '.recreate'<<<"$INSTANCE" && { - instances_destroy "$INSTANCE" "$RESULT" "$CTX" \ - && instances_create "$INSTANCE" "$RESULT" "$CTX" \ - && return 0 - return 1 - } - local INSTANCE_ID="$(jq -r .id<<<"$INSTANCE")" - instances_wait_instance "$INSTANCE_ID" "$CTX" && { - instances_update_volumes "$INSTANCE_ID" "$INSTANCE" "$CTX" || return 1 - instances_update_wan "$INSTANCE_ID" "$INSTANCE" "$CTX" || return 1 - instances_wait_instance "$INSTANCE_ID" "$CTX" \ - --argjson instance "$INSTANCE" "$FILTER_LOAD_INSTANCE" --out $RESULT || return 1 - echo "[INFO] instance '$INSTANCE_ID' updated." >&2 - return 0 - } - return 1 + local INSTANCE="$(instances_prepare "$1")" RESULT="$2" CTX="$3" && [ -z "$INSTANCE" ] && return 1 + jq_check '.recreate'<<<"$INSTANCE" && { + instances_destroy "$INSTANCE" "$RESULT" "$CTX" \ + && instances_create "$INSTANCE" "$RESULT" "$CTX" \ + && return 0 + return 1 + } + local INSTANCE_ID="$(jq -r .id<<<"$INSTANCE")" + instances_wait_instance "$INSTANCE_ID" "$CTX" && { + instances_update_volumes "$INSTANCE_ID" "$INSTANCE" "$CTX" || return 1 +# 废弃 wan 更新 @ 2019-02-21 +# instances_update_wan "$INSTANCE_ID" "$INSTANCE" "$CTX" || return 1 + instances_wait_instance "$INSTANCE_ID" "$CTX" \ + --argjson instance "$INSTANCE" "$FILTER_LOAD_INSTANCE" --out $RESULT || return 1 + echo "[INFO] instance '$INSTANCE_ID' updated." >&2 + return 0 + } + return 1 } instances_destroy(){ - local INSTANCE="$1" - local INSTANCE_ID="$(jq -r .id<<<"$INSTANCE")" - checked_api DELETE "/api/v1/vm/$INSTANCE_ID" && { - echo "[INFO] instance '$INSTANCE_ID' destroyed." >&2 - return 0 - } - return 1 + local INSTANCE="$1" + local INSTANCE_ID="$(jq -r .id<<<"$INSTANCE")" + checked_api2 GET "/nvm?Action=DeleteInstance&Version=2017-12-14&InstanceId=$INSTANCE_ID" && { + echo "[INFO] instance '$INSTANCE_ID' destroyed." >&2 + return 0 + } + return 1 } load_instances(){ - local PAGE_SIZE=50 PAGE_NUM FILTER="${1:-.}" PAGE_TOTAL PAGES=() FORKS FORK - { - PAGE_NUM=1 && read -r PAGE_TOTAL || return 1 - fork_next_page(){ - [ ! -z "$FORKS" ] || FORKS="$(mktemp -d)" - (( PAGE_NUM ++ )) && local FORK="$PAGE_NUM" - mkfifo "$FORKS/$FORK" && PAGES=("${PAGES[@]}" "$FORKS/$FORK" ) || return 1 - ( - npc api 'json.instances[]' GET "/api/v1/vm/allInstanceInfo?pageSize=$PAGE_SIZE&pageNum=$FORK" >"$FORKS/$FORK.load" - cat "$FORKS/$FORK.load" >"$FORKS/$FORK" - ) & return 0 - } - (( PAGE_TOTAL > 1 )) && for FORK in $(seq 1 ${NPC_ACTION_FORKS:-1}); do - (( PAGE_NUM < PAGE_TOTAL )) && fork_next_page || break - done - jq -c "select(.)|$FILTER" - while [ ! -z "${PAGES[0]}" ]; do - jq -c "select(.)|$FILTER" <"${PAGES[0]}" - unset PAGES[0] && PAGES=("${PAGES[@]}") - (( PAGE_NUM < PAGE_TOTAL )) && fork_next_page - done - } < <(npc api 'json|.total_page,.instances[]' GET "/api/v1/vm/allInstanceInfo?pageSize=$PAGE_SIZE&pageNum=1") \ - | jq -sc '.' - wait; [ ! -z "$FORKS" ] && rm -fr "$FORKS" - return 0 + local PAGE_SIZE=50 PAGE_NUM FILTER="${1:-.}" PAGE_TOTAL PAGES=() FORKS FORK + { + PAGE_NUM=1 && read -r PAGE_TOTAL || return 1 + fork_next_page(){ + [ ! -z "$FORKS" ] || FORKS="$(mktemp -d)" + (( PAGE_NUM ++ )) && local FORK="$PAGE_NUM" + mkfifo "$FORKS/$FORK" && PAGES=("${PAGES[@]}" "$FORKS/$FORK" ) || return 1 + ( + npc api2 'json.Instances[]' POST "/nvm?Action=DescribeInstanceList&Version=2017-12-14&Limit=$PAGE_SIZE&Offset=$(( (FORK-1) * PAGE_SIZE))" '{}' >"$FORKS/$FORK.load" + cat "$FORKS/$FORK.load" >"$FORKS/$FORK" + ) & return 0 + } + (( PAGE_TOTAL > 1 )) && for FORK in $(seq 1 ${NPC_ACTION_FORKS:-1}); do + (( PAGE_NUM < PAGE_TOTAL )) && fork_next_page || break + done + jq -c "select(.)|$FILTER" + while [ ! -z "${PAGES[0]}" ]; do + jq -c "select(.)|$FILTER" <"${PAGES[0]}" + unset PAGES[0] && PAGES=("${PAGES[@]}") + (( PAGE_NUM < PAGE_TOTAL )) && fork_next_page + done + } < <(npc api2 "json|(.TotalCount/$PAGE_SIZE|-.|floor|-.),.Instances[]" \ + POST "/nvm?Action=DescribeInstanceList&Version=2017-12-14&Limit=$PAGE_SIZE&Offset=0" '{}') \ + | jq -sc '.' + wait; [ ! -z "$FORKS" ] && rm -fr "$FORKS" + return 0 } instances_lookup(){ - local INSTANCE="$1" FILTER="${2:-.id}" STAGE="$NPC_STAGE/${INSTANCES_LOOKUP_KEY:-instances}.lookup" - ( exec 100>$STAGE.lock && flock 100 - [ ! -f $STAGE ] && { - load_instances '{ - id: .uuid, - name: .name, - lan_ip: .vnet_ip, - zone: .azCode, - inet_ip: (.public_ip//false), - corp_ip: (.private_ip//false) - }' >$STAGE || rm -f $STAGE - } - ) - [ ! -z "$INSTANCE" ] && [ -f $STAGE ] && INSTANCE="$INSTANCE" \ - jq_check --stdout '.[]|select(.id == env.INSTANCE or .name == env.INSTANCE)|'"$FILTER" $STAGE \ - && return 0 - echo "[ERROR] instance '$INSTANCE' not found" >&2 - return 1 + local INSTANCE="$1" FILTER="${2:-.id}" STAGE="$NPC_STAGE/${INSTANCES_LOOKUP_KEY:-instances}.lookup" + ( exec 100>$STAGE.lock && flock 100 + [ ! -f $STAGE ] && { + load_instances '{ + id: .InstanceId, + name: .InstanceName, + lan_ip: (.PrivateIpAddresses[0]), + zone: (.Placement.ZoneId), + inet_ip: (.PublicIpAddresses[0]//false), + corp_ip: (.PrivateIdcIpAddresses[0]//false) + }' >$STAGE || rm -f $STAGE + } + ) + [ ! -z "$INSTANCE" ] && [ -f $STAGE ] && INSTANCE="$INSTANCE" \ + jq_check --stdout '.[]|select(.id == env.INSTANCE or .name == env.INSTANCE)|'"$FILTER" $STAGE \ + && return 0 + echo "[ERROR] instance '$INSTANCE' not found" >&2 + return 1 } report_filters 'if .instances then - .instances |= map_values( - if .volumes then - (.volumes |= with_entries(select(.value.present))) - else . end - ) - else . end' \ No newline at end of file + .instances |= map_values( + if .volumes then + (.volumes |= with_entries(select(.value.present))) + else . end + ) + else . end' \ No newline at end of file diff --git a/files/npc-setup.ssh_key.sh b/files/npc-setup.ssh_key.sh index ca58f29..834c0e7 100644 --- a/files/npc-setup.ssh_key.sh +++ b/files/npc-setup.ssh_key.sh @@ -1,79 +1,88 @@ #! /bin/bash ssh_key_fingerprint(){ - local KEY_FILE="$1" && [ -f "$KEY_FILE" ] \ - && read _ FINGERPRINT _<<<"$(ssh-keygen -lE md5 -f "$KEY_FILE")" \ - && echo "${FINGERPRINT#MD5:}" + local KEY_FILE="$1" && [ -f "$KEY_FILE" ] \ + && read _ FINGERPRINT _<<<"$(ssh-keygen -lE md5 -f "$KEY_FILE")" \ + && echo "${FINGERPRINT#MD5:}" } check_ssh_keys(){ - local STAGE="$NPC_STAGE/ssh_keys" FORCE_CREATE CHECK_FILE OUTPUT_FILTER - while ARG="$1" && shift; do - case "$ARG" in - --check-file) - CHECK_FILE=Y - ;; - --create) - FORCE_CREATE=Y - CHECK_FILE=Y - ;; - --output) - OUTPUT_FILTER="$1" && shift - ;; - esac - done - while read -r SSH_KEY; do - ( exec 100>$STAGE.lock && flock 100 - [ -f $STAGE ] || { - npc api --error GET "/api/v1/secret-keys" \ - | jq -ce 'arrays//(objects|select(.code==4040005)|[]) - |map({key:.name, value:.})|from_entries' >$STAGE \ - && [ -s $STAGE ] \ - || { rm -f $STAGE; exit 1; } - } - local STAGE_SSH_KEY="$(jq -c --arg ssh_key "$SSH_KEY" '.[$ssh_key]//empty' $STAGE)" \ - SSH_KEY_FILE="$(cd ~; pwd)/.npc/ssh_key.$SSH_KEY" - [ ! -z "$STAGE_SSH_KEY" ] && { - [ ! -z "$OUTPUT_FILTER" ] && jq -cr "$OUTPUT_FILTER"<<<"$STAGE_SSH_KEY" - [ ! -z "$CHECK_FILE" ] || exit 0 - [ -f "$SSH_KEY_FILE" ] \ - && [ "$(ssh_key_fingerprint "$SSH_KEY_FILE")" = "$(jq -r '.fingerprint'<<<"$STAGE_SSH_KEY")" ] \ - && exit 0 - } - [ -f "$SSH_KEY_FILE" ] && { - [ ! -z "$STAGE_SSH_KEY" ] && { - echo "[ERROR] ssh_key '$SSH_KEY' fingerprint mismatch" >&2 - exit 1 - } - echo "[ERROR] '$SSH_KEY_FILE' already exists" >&2 - exit 1 - } - local SSH_KEY_ID - [ ! -z "$STAGE_SSH_KEY" ] && SSH_KEY_ID="$(jq -r '.id'<<<"$STAGE_SSH_KEY")" || { - echo "[ERROR] ssh_key '$SSH_KEY' not found" >&2 - [ ! -z "$FORCE_CREATE" ] && { - rm -f $STAGE && mkdir -p "$(dirname "$SSH_KEY_FILE")" - STAGE_SSH_KEY="$(npc api 'json|select(.id and .fingerprint)' \ - POST /api/v1/secret-keys "$(jq -Rc "{key_name:.}"<<<"$SSH_KEY")")" + local STAGE="$NPC_STAGE/ssh_keys" FORCE_CREATE CHECK_FILE OUTPUT_FILTER + while ARG="$1" && shift; do + case "$ARG" in + --check-file) + CHECK_FILE=Y + ;; + --create) + FORCE_CREATE=Y + CHECK_FILE=Y + ;; + --output) + OUTPUT_FILTER="$1" && shift + ;; + esac + done + while read -r SSH_KEY; do + ( exec 100>$STAGE.lock && flock 100 + [ -f $STAGE ] || { + checked_api2 '.Results|map({key:.KeyName, value:{ + id: .Id, + name: .KeyName, + fingerprint: .Fingerprint + }})|from_entries' \ + GET '/keypair?Action=ListKeyPair&Version=2018-02-08&Limit=9999&Offset=0' >$STAGE || { + rm -f $STAGE; exit 1 + } + } + local STAGE_SSH_KEY="$(jq -c --arg ssh_key "$SSH_KEY" '.[$ssh_key]//empty' $STAGE)" \ + SSH_KEY_FILE="$(cd ~; pwd)/.npc/ssh_key.$SSH_KEY" + [ ! -z "$STAGE_SSH_KEY" ] && { + [ ! -z "$OUTPUT_FILTER" ] && jq -cr "$OUTPUT_FILTER"<<<"$STAGE_SSH_KEY" + [ ! -z "$CHECK_FILE" ] || exit 0 + [ -f "$SSH_KEY_FILE" ] \ + && [ "$(ssh_key_fingerprint "$SSH_KEY_FILE")" = "$(jq -r '.fingerprint'<<<"$STAGE_SSH_KEY")" ] \ + && exit 0 + } + [ -f "$SSH_KEY_FILE" ] && { + [ ! -z "$STAGE_SSH_KEY" ] && { + echo "[ERROR] ssh_key '$SSH_KEY' fingerprint mismatch" >&2 + exit 1 + } + echo "[ERROR] '$SSH_KEY_FILE' already exists" >&2 + exit 1 + } + local SSH_KEY_ID + [ ! -z "$STAGE_SSH_KEY" ] && SSH_KEY_ID="$(jq -r '.id'<<<"$STAGE_SSH_KEY")" || { + echo "[ERROR] ssh_key '$SSH_KEY' not found" >&2 + [ ! -z "$FORCE_CREATE" ] && { + rm -f "$STAGE" + STAGE_SSH_KEY="$(checked_api2 '{ + id: .Id, + name: .KeyName, + fingerprint: .Fingerprint, + private_key: .PrivateKey + }' GET "/keypair?Action=CreateKeyPair&Version=2018-02-08&KeyName=$SSH_KEY")" && \ [ ! -z "$STAGE_SSH_KEY" ] && { - [ ! -z "$OUTPUT_FILTER" ] && jq -cr "$OUTPUT_FILTER"<<<"$STAGE_SSH_KEY" - SSH_KEY_ID="$(jq -r '.id'<<<"$STAGE_SSH_KEY")" - } || { - echo "[ERROR] Failed to create ssh_key '$SSH_KEY'" >&2 - } - } - } - [ ! -z "$SSH_KEY_ID" ] && [ ! -z "$FORCE_CREATE" ] && { - npc api GET "/api/v1/secret-keys/$SSH_KEY_ID" >$SSH_KEY_FILE \ - && chmod 600 $SSH_KEY_FILE \ - && [ "$(ssh_key_fingerprint "$SSH_KEY_FILE")" = "$(jq -r '.fingerprint'<<<"$STAGE_SSH_KEY")" ] \ - && exit 0 - rm -f $SSH_KEY_FILE - echo "[ERROR] Failed to download ssh_key '$SSH_KEY'" >&2 - } - echo "[ERROR] '$SSH_KEY_FILE' not exists" >&2 - exit 1 - ) || return 1 - done - return 0 + [ ! -z "$OUTPUT_FILTER" ] && jq -cr "$OUTPUT_FILTER"<<<"$STAGE_SSH_KEY" + SSH_KEY_ID="$(jq -r '.id'<<<"$STAGE_SSH_KEY")" + } || { + echo "[ERROR] Failed to create ssh_key '$SSH_KEY'" >&2 + } + } + } + [ ! -z "$SSH_KEY_ID" ] && [ ! -z "$FORCE_CREATE" ] && { + mkdir -p "$(dirname "$SSH_KEY_FILE")" + checked_api2 '.PrivateKey//empty' \ + GET "/keypair?Action=GetKeyPairPrivateKey&Version=2018-02-08&Id=$SSH_KEY_ID" >$SSH_KEY_FILE \ + && chmod 600 $SSH_KEY_FILE \ + && [ "$(ssh_key_fingerprint "$SSH_KEY_FILE")" = "$(jq -r '.fingerprint'<<<"$STAGE_SSH_KEY")" ] \ + && exit 0 + rm -f $SSH_KEY_FILE + echo "[ERROR] Failed to download ssh_key '$SSH_KEY'" >&2 + } + echo "[ERROR] '$SSH_KEY_FILE' not exists" >&2 + exit 1 + ) || return 1 + done + return 0 } diff --git a/files/npc-setup.volume.sh b/files/npc-setup.volume.sh index 32112d8..2b5f0ae 100644 --- a/files/npc-setup.volume.sh +++ b/files/npc-setup.volume.sh @@ -3,21 +3,29 @@ setup_resources "volumes" FILTER_LOAD_VOLUME='{ - name: .name, - id: .id, - actual_capacity: "\(.size)G", - available: (.status != "mount_succ"), - volume_uuid: .volume_uuid, - instance_id: (if .status == "mount_succ" then .service_name else null end) + name: .DiskName, + id: .DiskId, + actual_type: .Type, + actual_capacity: "\(.Capacity)G", + available: (.Status != "mount_succ"), + volume_uuid: .VolumeUUID, + instance_id: (if .Status == "mount_succ" then .AttachedInstance else null end) }' +VOLUME_TYPE_ALIASES='{ + "SSD": "CloudSsd", + "SAS": "CloudSas", + "C_SSD": "CloudSsd", + "C_SAS": "CloudSas", + "NBS_SSD": "CloudHighPerformanceSsd" +}' load_volumes(){ - local LIMIT=20 OFFSET=0 + local LIMIT=50 OFFSET=0 while (( LIMIT > 0 )); do - local PARAMS="limit=$LIMIT&offset=$OFFSET" && LIMIT=0 + local PARAMS="Limit=$LIMIT&Offset=$OFFSET" && LIMIT=0 while read -r VOLUME_ENTRY; do - LIMIT=20 && jq -c "select(.)|$FILTER_LOAD_VOLUME"<<<"$VOLUME_ENTRY" - done < <(npc api 'json.volumes[]' GET "/api/v1/cloud-volumes?$PARAMS") + LIMIT=50 && jq -c "select(.)|$FILTER_LOAD_VOLUME"<<<"$VOLUME_ENTRY" + done < <(npc api2 'json.DiskCxts[]' POST "/ncv?Action=ListDisk&Version=2017-12-28&$PARAMS" '{}') (( OFFSET += LIMIT )) done | jq -sc '.' } @@ -38,60 +46,29 @@ init_volumes(){ return 0 } -api_create_volume(){ - local CREATE_VOLUME="$1" - ( - exec 100>$NPC_STAGE/volumes.create_lock && flock 100 - local RESPONSE="$(checked_api . POST "/api/v1/cloud-volumes" "$CREATE_VOLUME")" \ - && echo "$RESPONSE" \ - && [ ! -z "$RESPONSE" ] \ - && local VOLUME_ID="$(jq -r '.id//empty'<<<"$RESPONSE")" \ - && [ ! -z "$VOLUME_ID" ] \ - && sleep 0.2s || echo "[ERROR] ${RESPONSE:-No response}" >&2 - ) -} - volumes_create(){ local VOLUME="$1" RESULT="$2" CTX="$3" && [ ! -z "$VOLUME" ] || return 1 - local CREATE_VOLUME="$(jq -c '{ - volume_name: .name, - az_name: (.zone//.az), - type: .type, - format:(.format//"Raw"), - size: (.capacity|sub("[Gg]$"; "")|tonumber) - }|with_entries(select(.value))'<<<"$VOLUME")" - while true; do - local RESPONSE="$(api_create_volume "$CREATE_VOLUME")" && [ ! -z "$RESPONSE" ] \ - && local VOLUME_ID="$(jq -r '.id//empty' <<<"$RESPONSE")" && [ ! -z "$VOLUME_ID" ] \ - && volumes_wait_status "$VOLUME_ID" "$CTX" && { - echo "[INFO] volume '$VOLUME_ID' created." >&2 - return 0 - } - # {"code":4030001,"msg":"Api freq out of limit."} - [ "$(jq -r .code <<<"$RESPONSE")" = "4030001" ] && ( - exec 100>$NPC_STAGE/volumes.retries && flock 100 \ - && action_sleep "$NPC_ACTION_RETRY_SECONDS" "$CTX" ) && continue - return 1 - done + local VOLUME_ID && VOLUME_ID="$(NPC_API_LOCK="$NPC_STAGE/volumes.create_lock" \ + checked_api2 '.DiskIds[0]//empty' \ + GET "/ncv?Action=CreateDisk&Version=2017-12-28&$(jq -r --argjson aliases "$VOLUME_TYPE_ALIASES" '{ + Scope: (.scope//"NVM"), + PricingModel: "PostPaid", + ZoneId: (.zone//.az), + Name: .name, + Type: (if .type then $aliases[.type|ascii_upcase]//.type else "CloudSsd" end), + Capacity: (.capacity|sub("[Gg]$"; "")|tonumber) + }|to_entries|map(@uri"\(.key)=\(.value)")|join("&")'<<<"$VOLUME")")" && [ ! -z "$VOLUME_ID" ] && { + echo "[INFO] volume '$VOLUME_ID' created." >&2 + return 0 + } + return 1 } volumes_update(){ local VOLUME="$1" RESULT="$2" CTX="$3" && [ ! -z "$VOLUME" ] || return 1 - local VOLUME_ID="$(jq -r .id<<<"$VOLUME")" && [ ! -z "$VOLUME_ID" ] || return 1 - local SIZE="$(jq -r '.capacity|sub("[Gg]$"; "")|tonumber'<<<"$VOLUME")" MOUNT_INSTANCE_ID - jq_check '.available'<<<"$VOLUME" || { - MOUNT_INSTANCE_ID="$(jq -r '.instance_id'<<<"$VOLUME")" - unmount_instance_volume "$MOUNT_INSTANCE_ID" "$VOLUME_ID" \ - && volumes_wait_status "$VOLUME_ID" "$CTX" || return 1 - } - checked_api '{code:status}' '.' PUT "/api/v1/cloud-volumes/$VOLUME_ID/actions/resize?size=$SIZE" >/dev/null \ - && volumes_wait_status "$VOLUME_ID" "$CTX" || return 1 - [ ! -z "$MOUNT_INSTANCE_ID" ] && { - mount_instance_volume "$MOUNT_INSTANCE_ID" "$VOLUME_ID" \ - && volumes_wait_status "$VOLUME_ID" "$CTX" || return 1 - } - echo "[INFO] volume '$VOLUME_ID' updated." >&2 - return 0 + # resize 已废弃 @ 2019-02-25 + echo "[ERROR] volume resize deprecated." >&2 + return 1 } volumes_destroy(){ @@ -102,7 +79,7 @@ volumes_destroy(){ unmount_instance_volume "$MOUNT_INSTANCE_ID" "$VOLUME_ID" \ && volumes_wait_status "$VOLUME_ID" "$CTX" || return 1 } - checked_api '{code:status}' '.' DELETE "/api/v1/cloud-volumes/$VOLUME_ID" >/dev/null && { + checked_api2 GET "/ncv?Action=DeleteDisk&Version=2017-12-28&DiskId=$VOLUME_ID" && { echo "[INFO] volume '$VOLUME_ID' destroyed." >&2 return 0 } @@ -112,10 +89,11 @@ volumes_destroy(){ volumes_wait_status(){ local VOLUME_ID="$1" CTX="$2" FILTER="$3" && [ ! -z "$VOLUME_ID" ] || return 1 while action_check_continue "$CTX"; do + local API2_DESCRIBE=(GET "/ncv?Action=DescribeDisk&Version=2017-12-28&DiskId=$VOLUME_ID") if [ ! -z "$FILTER" ]; then - OPTION_SILENCE=Y checked_api 'if .status|endswith("ing")|not then ('"$FILTER_LOAD_VOLUME|$FILTER"') else empty end' GET "/api/v1/cloud-volumes/$VOLUME_ID" && return 0 + OPTION_SILENCE=Y checked_api2 '.DiskCxt|if .Status|endswith("ing")|not then ('"$FILTER_LOAD_VOLUME|$FILTER"') else empty end' "${API2_DESCRIBE[@]}" && return 0 else - OPTION_SILENCE=Y checked_api '.status|endswith("ing")|not' GET "/api/v1/cloud-volumes/$VOLUME_ID" >/dev/null && return 0 + OPTION_SILENCE=Y checked_api2 '.DiskCxt|.Status|endswith("ing")|not' "${API2_DESCRIBE[@]}" >/dev/null && return 0 fi sleep "$NPC_ACTION_PULL_SECONDS" done @@ -136,18 +114,6 @@ volumes_lookup(){ return 1 } -#mount_instance_volume(){ -# local INSTANCE_ID="$1" VOLUME_UUID="$2" -# checked_api PUT "/api/v1/vm/$INSTANCE_ID/action/mount_volume/$VOLUME_UUID" -# # TODO: handle {"code":"4000797","msg":"Please retry."} -#} - -#unmount_instance_volume(){ -# local INSTANCE_ID="$1" VOLUME_UUID="$2" -# checked_api DELETE "/api/v1/vm/$INSTANCE_ID/action/unmount_volume/$VOLUME_UUID" -# # TODO: handle {"code":"4000797","msg":"Please retry."} -#} - mount_instance_volume(){ local INSTANCE_ID="$1" DISK_ID="$2" checked_api2 GET "/nvm?Action=AttachDisk&Version=2017-12-14&InstanceId=$INSTANCE_ID&DiskId=$DISK_ID" diff --git a/tests/v2-regression-tests.sh b/tests/v2-regression-tests.sh new file mode 100644 index 0000000..0cea978 --- /dev/null +++ b/tests/v2-regression-tests.sh @@ -0,0 +1,202 @@ +#!/bin/bash + +# openapi v1 下线改造回归测试 + +declare TEST_ACTION='' TEST_ACTION_DONE='' +test_act(){ + [ ! -z "$TEST_ACTION" ] && [ -z "$TEST_ACTION_DONE" ] && test_ok + TEST_ACTION="$1" && shift && TEST_ACTION_DONE='' && echo "[ACTION - $TEST_ACTION] $*">&2 +} +test_ok(){ + echo "[OK - $TEST_ACTION] $*">&2 && TEST_ACTION_DONE='Y' && return 0 +} +test_err(){ + echo "[ERR - $TEST_ACTION] $*">&2; return 1 +} + +trap 'test_err "TEST FAIL !!!" || true && exit 1' EXIT +set -e -o pipefail + +test_act "install role files" && rm -f /usr/bin/npc && \ + (INSTALLED_ROLE="/etc/ansible/roles/xiaopal.npc_setup" && \ + cd "$(dirname "${BASH_SOURCE[0]}")" && cd .. && \ + cp -rv * "$INSTALLED_ROLE/" && \ + ansible-playbook "$INSTALLED_ROLE/tests/noop.yml") + +test_act "init ssh key" && npc playbook -<<\EOF +--- +- hosts: localhost + gather_facts: no + roles: + - role: xiaopal.npc_setup + npc_config: + ssh_key: + name: v2-regression-tests-tmp1 +EOF + + +test_act "create vpc instances + volumes" && npc playbook -<<\EOF +--- +- hosts: localhost + gather_facts: no + roles: + - role: xiaopal.npc_setup + npc_config: + ssh_key: + name: v2-regression-tests + npc_volumes: + - name: test-hd-{1,2} + zone: cn-east-1b + capacity: 10G + npc_instances: + - name: test-vm + zone: cn-east-1b + instance_type: {series: 2, type: 2, cpu: 4, memory: 8G} + instance_image: Debian 8.6 + vpc: defaultVPCNetwork + vpc_subnet: default + vpc_security_group: default + vpc_inet: yes + vpc_inet_capacity: 10m + volumes: + - name: test-hd-1 + - test-hd-2 + present: yes + tasks: + - debug: msg={{npc}} +EOF + +test_act "save images" && npc playbook -<<\EOF +--- +- hosts: localhost + gather_facts: no + roles: + - role: xiaopal.npc_setup + npc_instance_images: + - name: test-image-1 + from_instance: test-vm +EOF + +test_act "create instance from private image" && npc playbook -<<\EOF +--- +- hosts: localhost + gather_facts: no + roles: + - role: xiaopal.npc_setup + npc_config: + ssh_key: + name: v2-regression-tests + npc_instances: + - name: test-vm-pri-image + zone: cn-east-1b + instance_type: {series: 2, type: 2, cpu: 4, memory: 8G} + instance_image: test-image-1 + vpc: defaultVPCNetwork + vpc_subnet: default + vpc_security_group: default + vpc_inet: yes + vpc_inet_capacity: 10m +EOF + +test_act "delete images" && npc playbook -<<\EOF +--- +- hosts: localhost + gather_facts: no + roles: + - role: xiaopal.npc_setup + npc_instance_images: + - name: test-image-1 + present: no + npc_instances: + - name: test-vm-pri-image + zone: cn-east-1b + present: no +EOF + +test_act "mount volumes" && npc playbook -<<\EOF +--- +- hosts: localhost + gather_facts: no + roles: + - role: xiaopal.npc_setup + npc_volumes: + - name: test-hd-3 + zone: cn-east-1b + capacity: 10G + npc_instances: + - name: test-vm + zone: cn-east-1b + volumes: + - name: test-hd-3 + present: yes + tasks: + - debug: msg={{npc}} +EOF + +test_act "unmount volumes" && npc playbook -<<\EOF +--- +- hosts: localhost + gather_facts: no + roles: + - role: xiaopal.npc_setup + npc_instances: + - name: test-vm + zone: cn-east-1b + volumes: + - name: test-hd-1 + present: no + present: yes + tasks: + - debug: msg={{npc}} +EOF + + +test_act "destroy instances + volumes" && npc playbook -<<\EOF +--- +- hosts: localhost + gather_facts: no + roles: + - role: xiaopal.npc_setup + npc_volumes: + - name: test-hd-{1,2,3} + zone: cn-east-1b + present: no + npc_instances: + - name: test-vm + zone: cn-east-1b + present: no + tasks: + - debug: msg={{npc}} +EOF + +test_act "create more volumes" && npc playbook -<<\EOF +--- +- hosts: localhost + gather_facts: no + roles: + - role: xiaopal.npc_setup + npc_volumes: + - name: test-hd-ext-{1..10} + zone: cn-east-1b + type: ssd + capacity: 10G + tasks: + - debug: msg={{npc}} +EOF + +test_act "delete more volumes" && npc playbook -<<\EOF +--- +- hosts: localhost + gather_facts: no + roles: + - role: xiaopal.npc_setup + npc_volumes: + - name: test-hd-ext-{1..10} + zone: cn-east-1b + present: no + tasks: + - debug: msg={{npc}} +EOF + + +trap - EXIT && test_ok "ALL TESTS SUCCESS" \ No newline at end of file