#!/bin/bash # addepactionusage "links" "add" "ahost aport zhost zport' # better cinfirmation when removing a link # clean up action output # add node issue with menu when type isnt specified # rename node issue when matching multiple # rename project # node stop givesjq error # connect xxx & [opens iterm (or whatever)] # when oepning new project and caching, "open" the project # when quitting or changing projects, "close" curproject # warn if curproject is closed VER=0.1 # helper function to turn a directory of scripts into inline ## defaults at the end of this script function scriptstoinline() { local outfile files echo "About to convert scripts from $SCRIPTDIR to inline format:" files=$(ls -A "${SCRIPTDIR}") echo "$files" | sed -e 's/^/ - /' echo outfile="" while [[ -z $outfile ]]; do getstr ":" "./out" "Enter file to place output in" outfile="$retstr" if [[ -e $outfile ]]; then getyn "n" "'$outfile' already exists. Overwrite" [[ $? -ne 0 ]] && outfile="" fi done cp /dev/null $outfile; for f in $SCRIPTDIR/*; do echo -n "Processing ${f}..." echo "#START_INLINE:$(basename $f)" >> $outfile cat "$f" >> $outfile echo "#END_INLINE:$(basename $f)" >> $outfile echo "done" done echo echo "All done. Replace inline code with the contents of: $outfile" } function mkscripts() { local dir dir="$1" info "Creating helper scripts in ${dir}" cat "$THISSCRIPT" | awk -v dir="$dir" -F: 'BEGIN { curfile=""; } /^#START_INLINE/ { curfile = dir "/" $2; printf(" %s...", curfile) > "/dev/stderr"; } /^#END_INLINE/ { curfile=""; printf("done\n") > "/dev/stderr"; } (curfile != "") { print > curfile; }' } function initauth() { local u p doscripts=0 files [[ ! -e $CONFDIR ]] && mkdir -p "$CONFDIR" && info "Created $CONFDIR" [[ ! -e $TMPDIR ]] && mkdir -p "$TMPDIR" && info "Created $CACHEDIR" [[ ! -e $CACHEDIR ]] && mkdir -p "$CACHEDIR" && info "Created $TMPDIR" [[ ! -e $SCRIPTDIR ]] && mkdir -p "$SCRIPTDIR" && info "Created $SCRIPTDIR" [[ ! -e $HISTFILE ]] && cp /dev/null "$HISTFILE" && info "Created $HISTFILE" # if [[ -e $AUTHFILE ]]; then # warn "Authentication file $AUTHFILE already exists - skipping creation." # else # info "Creating authentication file" # read -p "Enter gns username: " u # read -sp "Enter gns password: " p # echo # cp /dev/null "${AUTHFILE}" # [[ $? -ne 0 ]] && error "Can't create $AUTHFILE" && return 1 # echo "$u" >"${AUTHFILE}" # echo "$p" >>"${AUTHFILE}" # chmod 600 "$AUTHFILE" # info "Authentication details stored in $AUTHFILE" # fi [[ ! -e $RCFILE ]] && echo "# each line in this file is used as a command-line argument" > "$RCFILE" && info "Created initial $RCFILE" [[ ! -e $SRVFILE ]] && echo "exampleserver:gns3.example.net:3080" > "$SRVFILE" && info "Created initial $SRVFILE - you need to update this!" files=$(ls -A "${SCRIPTDIR}") if [[ -n $files ]]; then warn "Script directory $SCRIPTDIR already contains files:" echo -en "$YELLOW" echo "$files" | sed -e 's/^/ - /' echo echo -en "$PLAIN" getyn n "Overwrite scripts with defaults" [[ $? -eq 0 ]] && doscripts=1 || doscripts=0 else doscripts=1 fi if [[ $doscripts -eq 1 ]]; then mkscripts "$SCRIPTDIR" fi echo -e "$BOLD${CYAN}Initialisation of $BOLD$CONFDIR$PLAIN complete.$PLAIN" return 0 } function cache_uuids() { # cache_uuids $$ [locations] local loc epidx epname api_endpoint curlres mainpid thisfile local where mainpid="$1" shift if [[ $# -ge 1 ]]; then where="$*" else where="$curlocs" fi for loc in $where; do thisfile="${CACHEFILEBASE}.${loc}" cp /dev/null "$thisfile" for epname in ${ep_name[@]}; do info "- Caching UUID data for $loc $epname" epidx=$(getepidx $epname) api_endpoint="${ep_apiendpoint[$epidx]}" curlres=$(runcurlget $loc $api_endpoint) [[ $? -ne 0 ]] && error "curl to $thisurl failed" && return 1 echo "$curlres" | jq -r "try (.[] | [ .${ep_idfield[$epidx]}, .name ] | @csv) catch empty" | tr -d '"' >> "$thisfile" done done kill -SIGUSR1 $mainpid } function loadcachefile() { # loadcachefile [dc1 dc2 ...] local f line dc added if [[ ! -d "$CACHEDIR" ]]; then return 1 fi clear_cache added=0 if [[ $# -ge 1 ]]; then for dc in $*; do f=${CACHEFILEBASE}.${dc} if [[ -e ${f} ]]; then while read -r line ; do adduuid "${line%%,*}" "${line##*,}" if [[ $? -eq 0 ]]; then added=$((added + 1)) fi done < "${f}" fi done addmsgq $(info "Loaded $added UUIDs from cache files for $*" 2>&1) else ls ${CACHEFILEBASE}.* >/dev/null 2>&1 if [[ $? -eq 0 ]]; then for f in ${CACHEFILEBASE}.*; do while read -r line ; do adduuid ${line%%,*} ${line##*,} done < "$f" done addmsgq $(info "Loaded cache files for all servers" 2>&1) fi fi } function uuid_callback() { nuuids=0 loadcachefile $curlocs uuidend=$(($($GDATE +%s%N)/1000)) uuidsecs=$(echo "scale=2; ($uuidend - $uuidstart) / 1000000;" | bc) if [[ $nuuids -ge 1 ]]; then addmsgq $(info "Cached $nuuids UUID(s) for $curlocs in $uuidsecs seconds." 2>&1) else addmsgq $(error "No UUIDs found for [$curlocs]" 2>&1) fi CACHING="" unset recache_pid trap - SIGUSR1 } function start_recache_if_needed() { local dc f needed toload needed="" toload="" for dc in $curlocs; do f=${CACHEFILEBASE}.${dc} if [[ -e ${f} ]]; then toload="$toload $dc" else needed="$needed $dc" fi done [[ -n $toload ]] && loadcachefile $toload [[ -n $needed ]] && start_recache $needed } function start_recache() { local rv uuidstart=$(($($GDATE +%s%N)/1000)) if [[ -z $recache_pid ]]; then # setup callback for uuid cache reload handling trap uuid_callback SIGUSR1 info "Initiating UUID cache refresh for $curlocs..." CACHING="background task: refreshing cache for $curlocs" cache_uuids $$ $* & recache_pid=$! rv=0 else rv=1 fi return $rv } # populates global retstr function getstr() { # getstr last_prompt_char default_answer "prompt string" local lastchar def prompt rv lastchar="$1" shift def="$1" shift prompt="$*" retstr="" echo -n -e "${PURPLE}$prompt [$BOLD$def$PLAIN$PURPLE]${lastchar}${PLAIN} " read retstr echo -e "$PLAIN" [[ -z $retstr ]] && retstr="$def" } # populates global yn, returns 0 for yes, 1 for no function getyn() { # getyn default_answer "prompt string" local def prompt rv def="$1" shift prompt="$*" yn="" while [[ $yn != "y" && $yn != "n" ]]; do getstr "?" "$def" "$prompt" yn="$retstr" done if [[ $yn == "y" ]]; then rv=0 else rv=1 fi return $rv } function arraycontains() { # arraycontains arrayname lookfor wildcard local arr lookfor varname wild lookfor_modified varname="$1" lookfor="$2" shift 2 eval arr=( '"${'${varname}'[@]}"' ) if [[ $# -ge 1 ]]; then lookfor_modified=" $lookfor" else lookfor_modified=" $lookfor " fi [[ " ${arr[@]} " =~ $lookfor_modified ]] && return 0 return 1 } function getcmd() { local prefix pstr input varname rv varname=$1 if [[ -z $curproj ]]; then prefix="[no_project_selected]" else prefix="[${curproj}]" fi #echo "${BOLD}$prefix rosh> ${PLAIN}" pstr="$prefix gnscli> " [[ ! -z $CACHING ]] && info "$CACHING" set -f builtin read -p "$(echo -en $BOLD)${pstr}$(echo -e "$PLAIN") " -e -r input set +f rv=$? eval "$varname='$input'" return $rv } function listprojects() { local x format format=" %-24s|%-36s${PLAIN}\n" printf "${UNDERLINE}${BOLD}$format" "Project" "UUID" for x in ${!proj_name[@]}; do printf "$format" "${proj_name[$x]}" "${proj_id[$x]}" done } function listservers() { local x format format=" %-12s|%-24s|%-34s${PLAIN}\n" printf "${UNDERLINE}${BOLD}$format" "Location" "Host" "API URL" for x in ${!loc_name[@]}; do printf "$format" "${loc_name[$x]}" "${loc_engine[$x]}" "${loc_api[$x]}" done } function profile() { local fname fname=${FUNCNAME[2]} [[ -z $fname ]] && fname=${FUNCNAME[1]} [[ $PROFILING -eq 1 ]] && echo -e "${YELLOW}${BOLD}${FUNCNAME[1]}(): ${PLAIN}${YELLOW}$*${PLAIN}" 1>&2 } function debug() { local force=0 if [[ $1 == "-f" ]]; then shift force=1 fi if [[ $VERBOSE -eq 1 ]]; then if [[ $force -eq 1 ]]; then echo -e "${PURPLE}${BOLD}${FUNCNAME[1]}(): ${PLAIN}${PURPLE}$*${PLAIN}" >/dev/stderr else echo -e "${PURPLE}${BOLD}${FUNCNAME[1]}(): ${PLAIN}${PURPLE}$*${PLAIN}" 1>&2 fi fi } function info() { echo -e "${CYAN}${ITALIC}[$*]${PLAIN}" 1>&2 } function error() { [[ $innotify -eq 1 ]] && fail echo -e "${RED}${BOLD}ERROR: ${PLAIN}${RED}$*${PLAIN}" 1>&2 } function notify() { echo -en "${PURPLE}${BOLD}* ${PLAIN}${PURPLE}$*...${PLAIN} " 1>&2 innotify=1 } function notify_nodots() { echo -e "${PURPLE}${BOLD}* ${PLAIN}${PURPLE}$*${PLAIN} " 1>&2 } function ok() { local msg=${*:-ok} [[ $innotify -eq 0 ]] && return 1 echo -e "$GREEN$msg$PLAIN" 1>&2 innotify=0 } function fail() { local msg=${*:-failed} [[ $innotify -eq 0 ]] && return 1 echo -e "$RED$msg$PLAIN" 1>&2 innotify=0 } function warn() { echo -e "${YELLOW}${BOLD}Warning: ${PLAIN}${YELLOW}$*${PLAIN}" 1>&2 } function usage() { echo "usage: $0 OPTIONS command" echo echo " -g path Specify path to perl Graph::Easy binary" echo " -h Show this text." echo " -i Generate initial auth file." echo " -s server Set initial server." echo " Valid servers: [${loc_name[@]}]" echo " -p project Set initial project." echo " -P Profiling mode." echo " -v Verbose mode." echo } function addcmd() { local adm [[ -z $ncmds ]] && ncmds=0 if [[ $1 == "-a" ]]; then adm=1 shift else adm=0 fi cmd_name[$ncmds]="$1" cmd_desc[$ncmds]="$2" cmd_adm[$ncmds]="$adm" if [[ $3 == ** ]]; then cmd_minargs[$ncmds]="${3%%+*}" cmd_maxargs[$ncmds]="99" cmd_needargs[$ncmds]=">= ${cmd_minargs[$ncmds]}" elif [[ $3 == *-* ]]; then cmd_minargs[$ncmds]="${3%%-*}" cmd_maxargs[$ncmds]="${3##*-}" cmd_needargs[$ncmds]="${cmd_minargs[$ncmds]}-${cmd_maxargs[$ncmds]}" else cmd_minargs[$ncmds]="$3" cmd_maxargs[$ncmds]="$3" cmd_needargs[$ncmds]="$3" fi shift 3 cmd_aliases[$ncmds]=" $* " cmd_usage[$ncmds]="" cmd_optionname[$ncmds]="" cmd_optiondesc[$ncmds]="" cmd_maxoptnamelen[$ncmds]=0 ncmds=$((ncmds + 1)) } function addcmdalias() { local defarg [[ -z $ncmdaliases ]] && ncmdaliases=0 cmdalias_src[$ncmdaliases]="$1" cmdalias_dst[$ncmdaliases]="$2" defarg="$3" cmdalias_defaultarg[$ncmdaliases]="$defarg" ncmdaliases=$((ncmdaliases + 1)) } function addmsgq() { msgq[$nmsgq]="$*" nmsgq=$((nmsgq + 1)) } function dumpmsgq() { local x for x in ${!msgq[@]}; do echo -e "${msgq[$x]}" done msgq=() nmsgq=0 } function clear_cache() { uuid_id=() uuid_name=() nuuids=0 } function adduuid() { # uuid name # local i # add uuid_id[$nuuids]="${1}" uuid_name[$nuuids]="${2}" nuuids=$((nuuids + 1)) # i=$(uuid_to_idx "${1}") # if [[ $? -eq 0 ]]; then # # update # uuid_name[$i]="${2}" # return 1 # else # # add # uuid_id[$nuuids]="${1}" # uuid_name[$nuuids]="${2}" # nuuids=$((nuuids + 1)) # return 0 # fi } function uuid_to_name() { local x #for x in ${!uuid_id[@]}; do # if [[ ${uuid_id[$x]} == $1 ]]; then # echo "${uuid_name[$x]}" # return 0 # fi #done # faster for ((x=0; x<${#uuid_id[@]}; ++x)); do if [[ "${uuid_id[$x]}" == "$1" ]]; then echo "${uuid_name[$x]}" return 0 fi done echo "$1" return 1 } function uuid_to_idx() { local x for x in ${!uuid_id[@]}; do if [[ ${uuid_id[$x]} == $1 ]]; then echo "${$x}" return 0 fi done return 1 } function addloc() { [[ -z $nlocs ]] && nlocs=0 loc_name[$nlocs]="$1" if [[ $nlocs -eq 0 ]]; then curlocs="$1" fi loc_engine[$nlocs]="$2" loc_port[$nlocs]="$3" loc_api[$nlocs]="http://${loc_engine[$nlocs]}:${loc_port[$nlocs]}/v2" nlocs=$((nlocs + 1)) } function addproject() { [[ -z $nprojects ]] && nprojects=0 proj_id[$nprojects]="$1" proj_name[$nprojects]="$2" nprojects=$((nprojects + 1)) } # Don't use '#'. Use ^ for spaces. function addepfields() { # addepfields ep_name field1 field2 field3 ... local idx idx=$(getepidx $1) if [[ $? -ne 0 ]]; then error "appepfields(): can't find endpoint named '$1'" return 1 fi shift while [[ $# -ge 1 ]]; do [[ -z ${ep_fields[$idx]} ]] && ep_fields[$idx]="${1}" || ep_fields[$idx]="${ep_fields[$idx]}#${1}" shift done } # Don't use '#'. Use ^ for spaces. function addeptitles() { # addeptitles ep_name title1 title2 title3 ... # Don't use '#' or spaces. local idx idx=$(getepidx $1) if [[ $? -ne 0 ]]; then error "appeptitles(): can't find endpoint named '$1'" return 1 fi shift while [[ $# -ge 1 ]]; do ep_titles[$idx]="${ep_titles[$idx]}#${1}" shift done } function addendpoint() { # addendpoint ep_name ep_apiendpoint ep_jqobjectname ep_idfieldname [defaultfieldname] [[ -z $neps ]] && neps=0 ep_name[$neps]="$1" ep_apiendpoint[$neps]="$2" ep_jqobj[$neps]="$3" ep_idfield[$neps]="$4" ep_defaultfield[$neps]="${5:-name}" ep_alias[$neps]="" ep_fields[$neps]="" ep_titles[$neps]="Server" ep_validactions[$neps]="" neps=$((neps + 1)) } function addepalias() { # addeptitles ep_name alias1 alias2 alias3 ... local idx idx=$(getepidx $1) if [[ $? -ne 0 ]]; then error "appepalias(): can't find endpoint named '$1'" return 1 fi shift while [[ $# -ge 1 ]]; do ep_alias[$idx]="${ep_alias[$idx]} ${1} " shift done } function addepactions() { # addepactions ep_name action_name [action_name2] ... local idx idx=$(getepidx $1) if [[ $? -ne 0 ]]; then error "appepaction(): can't find endpoint named '$1'" return 1 fi shift while [[ $# -ge 1 ]]; do ep_validactions[$idx]="${ep_validactions[$idx]} ${1} " shift done } function getcmdidx() { # getcmdidx commandname adminonly(1|0) local x adm lookfor match lookfor=$1 adm=$2 match=0 for x in ${!cmd_name[@]}; do #echo "check alias '${cmd_aliases[$x]}' against '* $lookfor *'" >/dev/stderr if [[ ${cmd_name[$x]} == $lookfor ]]; then match=1 elif [[ ${cmd_aliases[$x]} == *\ $lookfor\ * ]]; then match=1 else match=0 fi if [[ $match -eq 1 ]]; then if [[ -z $adm || ${cmd_adm[$x]} == $adm ]]; then echo "$x" return 0 fi fi done return 1 } function addcmdusage() { # addcmdusage cmd_name "usage goes here" "line 2" "line 3" ... local cname idx text cname="$1" shift idx=$(getcmdidx $cname) [[ $? -ne 0 ]] && return 1 text="" while [[ $# -ge 1 ]]; do [[ -z $text ]] && text="$1" || text="$text\n$1" shift 1 done cmd_usage[$idx]="$text" } function addcmdoption() { # addcmdusage cmd_name "usage goes here" "line 2" "line 3" ... local cname idx text cname="$1" shift idx=$(getcmdidx $cname) if [[ $? -ne 0 ]]; then error "Tried to addcmdoption() for nonexistant command '$cname'" kill $$ fi while [[ $# -ge 1 ]]; do [[ -z ${cmd_optionname[$idx]} ]] && cmd_optionname[$idx]="$1" || cmd_optionname[$idx]="${cmd_optionname[$idx]}#$1" [[ ${#1} -gt ${cmd_maxoptnamelen[$idx]} ]] && cmd_maxoptnamelen[$idx]=${#1} shift 1 [[ -z ${cmd_optiondesc[$idx]} ]] && cmd_optiondesc[$idx]="$1" || cmd_optiondesc[$idx]="${cmd_optiondesc[$idx]}#$1" shift 1 done } function showcmdhelp() { local real_cname cname idx text ln x format optname_arr optdesc_arr len cname="$1" shift idx=$(getcmdidx $cname) if [[ $? -ne 0 ]]; then error "Invalid command '$cname'" return 1 fi real_cname="${cmd_name[$idx]}" [[ ${cmd_adm[$idx]} -eq 1 ]] && real_cname="\\${real_cname}" # make array of options IFS='#' read -r -a optname_arr <<< "${cmd_optionname[$idx]}" IFS='#' read -r -a optdesc_arr <<< "${cmd_optiondesc[$idx]}" len=${cmd_maxoptnamelen[$idx]} echo -n "Usage: ${real_cname} " [[ $len -gt 0 ]] && echo -n "[OPTIONS] " # show usage if we have it if [[ -n ${cmd_usage[$idx]} ]]; then echo -e "${cmd_usage[$idx]}" | awk '(NR == 1) { print }' else echo fi echo " ${cmd_desc[$idx]}." if [[ -n ${cmd_usage[$idx]} ]]; then echo echo -e "${cmd_usage[$idx]}" | awk '(NR > 1) { printf(" %s\n",$0); }' fi # show options if [[ $len -gt 0 ]]; then echo format=" %-${len}s %s\n" for x in ${!optname_arr[@]}; do printf "$format" "${optname_arr[$x]}" "${optdesc_arr[$x]}" done fi # Show aliases if [[ -n ${cmd_aliases[$idx]// /} ]]; then echo echo "Aliases for '${real_cname}': ${cmd_aliases[$idx]}" fi echo } function getprojidx() { # getprojidx project_name_or_uuid local x for x in ${!proj_name[@]}; do if [[ ${proj_name[$x]} == $1 || ${proj_id[$x]} == $1 ]]; then echo "$x" return 0 fi done return 1 } function getlocidx() { # getlocidx local x for x in ${!loc_name[@]}; do if [[ ${loc_name[$x]} == $1 ]]; then echo "$x" return 0 fi done return 1 } function getlocidx_byregion() { # getlocidx local x for x in ${!loc_region[@]}; do if [[ ${loc_region[$x]} == $1 ]]; then echo "$x" return 0 fi done return 1 } function getalllocs() { echo ${loc_name[@]} } function get() { # getloc local idx idx=$(getlocidx $1) if [[ $? -ne 0 ]]; then return 1 fi eval "echo \${loc_${2}[$idx]}" } function runcurlget() { # location api_endpoint(ovname) local thisapi thisurl curlres rv local thisauthdom u p thisauthstr local loc api_andpoint loc="$1" api_endpoint="$2" thisapi=$(get $loc api) [[ -z $thisapi ]] && echo "cant find api for '$loc'" && return 1 thisurl="$thisapi/$api_endpoint" thisurl=${thisurl/_CURPROJECT_/$curprojid} debug "url is $thisurl" #thisauthdom=$(get $loc authdomain) #u=$(head -1 "$AUTHFILE") #p=$(tail -1 "$AUTHFILE") #thisauthstr="$u@$thisauthdom:$p" #unset u #unset p debug "curl -sk --header 'Accept: application/json' $thisurl" curlres=$(curl -sk --header 'Accept: application/json' $thisurl) rv=$? [[ $rv -ne 0 ]] && error "curl to $thisurl failed" echo "$curlres" return $rv } function runcurldatapost() { # location api_endpoint endpoint_uuid "curl data" local thisapi thisurl curlres rv local loc api_endpoint obname ovmethod local endpoint_uuid local curlmethod="POST" local data="" loc="$1" api_endpoint="$2" endpoint_uuid="$3" data="$4" thisapi=$(get $loc api) [[ -z $thisapi ]] && echo "cant find api for '$loc'" && return 1 #thisauthdom=$(get $loc authdomain) thisurl="$thisapi/$api_endpoint" thisurl=${thisurl/\/v2\/templates/\/v2\/projects\/_CURPROJECT_\/templates} # special case thisurl=${thisurl/_CURPROJECT_/$curprojid} if [[ -n $endpoint_uuid ]]; then thisurl="$thisurl/$endpoint_uuid" fi if [[ $GLOBALACTION == "mv" ]]; then curlmethod="PUT" else curlmethod="POST" fi debug -f "loc is $loc" debug -f "api_endpoint is $api_endpoint" debug -f "endpoint_uuid is $endpoint_uuid" debug -f "data is $data" debug -f "thisapi is $thisapi" debug -f "thisurl is $thisurl" debug -f "curlmethod is $curlmethod" debug -f "curl -X${curlmethod} -sk --header 'Content-type: application/xml' --header 'Accept: application/json' -d \"$data\" $thisurl" curlres=$(curl -X${curlmethod} -sk --header 'Content-type: application/xml' --header 'Accept: application/json' -d "$data" $thisurl) rv=$? [[ $rv -ne 0 ]] && error "curl ${curlmethod} to $thisurl failed" echo "$curlres" return $rv } function runcurlaction() { # location api_endpoint(ovname) obname ovmethod ["curl data"] local thisapi thisurl curlres rv #local thisauthdom u p thisauthstr local loc api_endpoint obname ovmethou local data="" local curlmethod="POST" #local data="" loc="$1" api_endpoint="$2" obname="$3" ovmethod="$4" data="$5" thisapi=$(get $loc api) [[ -z $thisapi ]] && echo "cant find api for '$loc'" && return 1 #thisauthdom=$(get $loc authdomain) debug -f "data is $data" thisurl="$thisapi/$api_endpoint/$obname" [[ -n $ovmethod ]] && thisurl="${thisurl}/$ovmethod" thisurl=${thisurl/_CURPROJECT_/$curprojid} #u=$(head -1 "$AUTHFILE") #p=$(tail -1 "$AUTHFILE") #thisauthstr="$u@$thisauthdom:$p" #unset u #unset p if [[ $GLOBALACTION == "del" ]]; then curlmethod="DELETE" else curlmethod="POST" fi debug -f "curl -X$curlmethod -sk --header 'Content-type: application/xml' --header 'Accept: application/json' -d \"$data\" $thisurl" curlres=$(curl -X$curlmethod -sk --header 'Content-type: application/xml' --header 'Accept: application/json' -d "$data" $thisurl) rv=$? [[ $rv -ne 0 ]] && error "curl ${curlmethod} to $thisurl failed" echo "$curlres" return $rv } # getarrayopt arrname f # # ie. if myvar=("aaa" "-z" "-ofirst_test" "123" "-osecond_test" # # then "getarrayopt myvar o" will output "first_test" function getarrayopt() { # getarrayopt arrname optname local arrname optname arrtext optval rv newarr local localdb localdb=0 arrname="$1" optname="$2" [[ $localdb -eq 1 ]] && info "look for -$optname in \$$arrname" # fix in case array indices aren't sequential arrtext=$(eval "echo \"\${${arrname}[@]}\"") newarr=( $arrtext ) [[ $localdb -eq 1 ]] && info "arrtext is '$arrtext'" if arraycontains $arrname "-$optname" WILDCARD; then local this # get array index of given option idx=$(echo "${arrtext}" | awk -v lookfor="-$optname" '{ for (x=1;x<=NF;x++) { if (index($x,lookfor) == 1) print (x-1); } }') [[ $localdb -eq 1 ]] && info " found at idx $idx" this=${newarr[$idx]} optval=${this##*-$optname} # strip "-x" from the start optval=${optval%% *} # strip all other options from the end [[ $localdb -eq 1 ]] && info " optval is $optval" rv=0 else [[ $localdb -eq 1 ]] && info " not found" optval="" rv=1 fi echo "$optval" return $rv } # convert action name into gns method name function getgnsmethod() { local rv=0 case "$1" in open) echo "open";; close) echo "close";; start) echo "start";; stop) echo "stop";; add) echo "";; del) echo "";; mv) echo "";; #migrate) echo "migrate";; *) echo rv=1; ;; esac return $rv } function makejson() { # makejson key1:val1^key2:val2^... local plaindata tok toks key val tnum=1 format plaindata="$1" printf "%s" '{' IFS='^' read -ra toks <<< "$plaindata" for tok in "${toks[@]}"; do [[ $tnum -ne 1 ]] && printf "%s" "," key=${tok%:*} val=${tok#*:} if [[ $val =~ ^[0-9]+$ ]]; then format='"%s":%s' else format='"%s":"%s"' fi printf "$format" "$key" "$val" tnum=$((tnum + 1)) done printf "%s\n" '}' } function runaction_loc() { # runaction_loc syd|etc api_endpoint action_name ob_uuid ob_name "extra info" [options] local thisgrid thisauthdom thisurl thisauthstr curlres loc u p local api_endpoint jq_obj opts opts_arr deffield epidx r1 r2 rv local actionname method trv extrainfo local curldata="" crv obuuid rv=0 loc="$1" epidx="$2" actionname="$3" obuuid="$4" obname="$5" extrainfo="$6" thisgrid=$(get $loc grid) api_endpoint="${ep_apiendpoint[$epidx]}" jq_obj="${ep_jqobj[$epidx]}" deffield="${ep_defaultfield[$epidx]}" shift 6 opts="$*" opts_arr=( $opts ) debug -f "arglist - loc: $loc" debug -f "arglist - epidx: $epidx" debug -f "arglist - actionname: $actionname" debug -f "arglist - obuuid: $obuuid" debug -f "arglist - obname: $obname" debug -f "arglist - extrainfo : $extrainfo" debug -f "arglist: remaining opts: $opts" if [[ $actionname == "add" || $actionname == "mv" ]]; then if [[ -n $RAWJSONPOSTDATA ]]; then curldata="${RAWJSONPOSTDATA}" else curldata=$(makejson "$extrainfo") fi fi RAWJSONPOSTDATA="" # special case if [[ $actionname == "connect" ]]; then local locidx # todo: only a single curloc, not multiple locidx=$(getlocidx $curlocs) if [[ $? -ne 0 ]]; then error "Couldn't determine index of current location '$curlocs'" return 1 fi tmux ls 2>&1 | egrep -q "^$curproj:" if [[ $? -eq 0 ]]; then debug "using existing tmux session with: tmux neww -S -n $obname telnet ${loc_engine[$locidx]} ${extrainfo}" # create or attach to window tmux neww -S -n $obname telnet ${loc_engine[$locidx]} ${extrainfo} trv=$? else debug "making new tmux session with: tmux new -s gnscli -n $obname -d telnet ${loc_engine[$locidx]} ${extrainfo}" # create a new session and window tmux new -s $curproj -n $obname -d telnet ${loc_engine[$locidx]} ${extrainfo} trv=$? fi if [[ $trv -eq 0 ]]; then tmux a -t $curproj else error "tmux call failed" return 1 fi else method=$(getgnsmethod "$actionname") if [[ $? -ne 0 ]] ; then error "Can't determine gns API method for command '$actionname'" return 1 fi debug -f "ABOUT TO CALL CURL FUNCTION...." debug -f "location: ${loc}" debug -f "api endpoint: ${api_endpoint}" debug -f "jq obj: ${jq_obj}" debug -f "actionname: ${actionname} (gns method: '$method')" debug -f "obuuid: ${obuuid}" debug -f "options: ${opts}" debug -f "curldata: ${curldata}" if [[ $action == "add" || $action == "mv" ]]; then # note: "obuuid" is empty in some cases, eg: # adding a builtin node like VPCS # adding a link curlres=$(runcurldatapost "$loc" "$api_endpoint" "$obuuid" "$curldata" ) crv=$? else curlres=$(runcurlaction "$loc" "$api_endpoint" "$obuuid" "$method" "$curldata" ) crv=$? fi if [[ $curlres =~ $CURLERRORSTRINGS ]]; then if jq -e . >/dev/null 2>&1 <<< "$curlres"; then error "curl returned json error:" echo -en "$RED" >/dev/stderr echo "$curlres" | jq . | sed -e 's/^/ /' >/dev/stderr echo -e "$PLAIN" >/dev/stderr else error "curl returned error: [$curlres]" fi crv=99 fi if [[ $crv -ne 0 ]]; then rv=1 fi debug -f "curlres is [$curlres]" echo "$curlres" fi return $rv } # uses globals jqscrjpt, jqfilter, awkscript function apply_filters() { # retvar wantquotes [f][s][a] local retvar which data wantquotes rawopt qstr retvar="$1" wantquotes="${2:-0}" debug "wantquotes is $wantquotes" if [[ $wantquotes -eq 0 ]]; then rawopt="-r" qstr="without quotes (jq -r)" elif [[ $wantquotes -eq -1 ]]; then rawopt="-r" qstr="REALLY without quotes (jq -r | sed_remove_quotes)" else rawopt="" qstr="with quotes" fi [[ -z $retvar ]] && return 1 which="${3:-sfa}" eval "data=\"\$$retvar\"" debug "applying: $which $qstr" debug " wantquotes is $wantquotes" debug " jqfilter is $jqfilter" debug " jqscript is $jqscript" debug " awkscript is $awkscript" if [[ $which == *f* && -n $jqfilter ]]; then #debug "$retvar pre jqfilter is $data" echo "$data" >/tmp/a data=$(echo "$data" | jq $rawopt "$jqfilter" 2>/dev/null) if [[ $wantquotes -eq -1 ]]; then data=$(echo "$data" | tr -d '"') fi #debug "$retvar post jqfilter is $data" fi if [[ $which == *s* && -n $jqscript ]]; then debug "jqscript contents: $(cat $jqscript)" debug "$retvar pre jqscript is $data" data=$(echo "$data" | jq $rawopt -f "$jqscript") debug "$retvar post jqscript is $data" fi if [[ $which == *a* && -n $awkscript ]]; then debug "$retvar pre awkscript is $data" data=$(echo "$data" | awk -f "$awkscript") fi eval $retvar=\"\$data\" return 0 } function getdata_loc() { # getdata_loc syd|etc api_endpoint [options] local thisgrid thisauthdom thisurl thisauthstr curlres loc u p local api_endpoint jq_obj opts opts_arr deffield epidx r1 r2 rv rv=0 loc="$1" epidx="$2" thisgrid=$(get $loc grid) api_endpoint="${ep_apiendpoint[$epidx]}" jq_obj="${ep_jqobj[$epidx]}" deffield="${ep_defaultfield[$epidx]}" shift 2 opts="$*" opts_arr=( $opts ) debug "options: ${opts}" curlres=$(runcurlget $loc $api_endpoint) [[ $? -ne 0 ]] && error "curl to $loc API failed" && return 1 debug "outmode is $outmode" debug "jqscript is $jqscript" debug "jqfilter is $jqfilter" debug "awkscript is $awkscript" if [[ $outmode == "raw" ]]; then r1="$curlres" # only apply jqfilter apply_filters r1 $QUOTES f [[ -n $r1 ]] && echo "$r1" || rv=2 elif [[ $outmode == "verbose" ]]; then r1="$curlres" apply_filters r1 $REALLYNOQUOTES [[ -n $r1 ]] && echo "$r1" || rv=2 else # namesonly r1=$(echo "$curlres") apply_filters r1 debug "semi-final r1 is [$r1]" if [[ $cmd == "list" ]]; then debug "Global cmd is $GLOBALCMD" if [[ $GLOBALCMD == "action" ]]; then # ie. we end up with each line being: # location,object,object_uuid r1=$(echo "$r1" | egrep -v "^$" | sed "s/^/$loc,/") else r1=$(echo "$r1" | egrep -v "^$") fi fi debug "final r1 is [$r1]" [[ -n $r1 ]] && echo "$r1" | sed 's/__END__://' || rv=2 fi debug "about to exit with code $rv" return $rv } function getlocidx() { # getlocidx locname local x lookfor lookfor="$1" for x in ${!loc_name[@]}; do if [[ ${loc_name[$x]} == $lookfor ]]; then echo "$x" return 0 fi done return 1 } function getepidx() { # getepidx epname local x lookfor lookfor="$1" for x in ${!ep_name[@]}; do if [[ ${ep_name[$x]} == $lookfor || ${ep_alias[$x]} == *\ $lookfor\ * ]]; then echo "$x" return 0 elif [[ ${ep_name[$x]} == ${lookfor}s || ${ep_alias[$x]} == *\ ${lookfor}s\ * ]]; then echo "$x" return 0 fi done return 1 } function getepjqobj() { local x for x in ${!ep_name[@]}; do if [[ ${ep_name[$x]} == $1 || ${ep_alias[$x]} == *\ $1\ * ]]; then echo "${ep_jqobj[$x]}" return 0 fi done return 1 } function getepovname() { local x for x in ${!ep_name[@]}; do if [[ ${ep_name[$x]} == $1 || ${ep_alias[$x]} == *\ $1\ * ]]; then echo "${ep_apiendpoint[$x]}" return 0 fi done return 1 } function getbasefilter() { # epidx [regexp_filter] local bf x first epidx local refilter defaultvalue="unknown" thisdefault local filterarg epidx="$1" shift filterarg="$*" [[ $filterarg == "*" ]] && filterarg=".*" [[ -n "$*" ]] && refilter="select(.${ep_defaultfield[$epidx]}|test(\"^$*$\"))" || refilter="." IFS='#' read -r -a titles <<< "${ep_titles[$epidx]}" IFS='#' read -r -a fields <<< "${ep_fields[$epidx]}" bf="" for x in ${titles[@]}; do x=${x//^/ } [[ -z $bf ]] && bf="([" || bf="$bf, " bf="${bf}\"${x}\"" done #bf="${bf}] ), (.$jq_obj[] | $refilter | [\"_LOCATION_\"" bf="${bf}] ), (.[] | $refilter | [\"_LOCATION_\"" first=1 for x in ${fields[@]}; do thisdefault="not_found" [[ $x == ".status" ]] && thisdefault="project_closed" x=${x//^/ } #bf="${bf}, try(${x}) catch(\"xx\")" bf="${bf}, ${x} // \"${thisdefault}\"" done bf="${bf}]) | @csv" echo "${bf}" } function profile_start() { profile "start profiling" prof_start=$(($($GDATE +%s%N)/1000)) prof_last="$prof_start" } function profile_mark() { local now secs now=$(($($GDATE +%s%N)/1000)) secs=$(echo "scale=2; ($now - $prof_last) / 1000000;" | bc) profile "[+${secs} secs] $* " prof_last="$now" } # Output CSV contents as a nicely formatted table. # If a heading row is detected, it is bolded and underlines. # The last table line is also underlined. # A blank line is printed before each change of first column value - this is generally the data centre name. function csv_to_table() { # csv_to_table num_data_lines_excluding_heading "csv_string_with_newlines" local ndatalines msg local table local data ln local widths uuidcolumn uuidcolumns thisuuid repl line local col colidx profile_start ndatalines="$1" shift 1 msg="$*" table=$(echo -e "$msg" | column -t -s,) profile_mark "made initial table" # Find out which columns need a UUID lookup if [[ $usecache -eq 1 ]]; then uuidcolumns=$(csv_find_uuid_cols "$msg") table=${table//_UUID/ } else uuidcolumns="" fi profile_mark "found uuid columns" # find all heading columns widths=$(find_fieldwidths "$table") debug "table before repl [\n$table]" debug "widths is [$widths]" # At this point, $table is the full table without UUID replacement ln=1 table2="" while read -r line ; do if [[ $ln -gt 1 ]]; then # Replace UUIDS with names from cache for uuidcolumn in $uuidcolumns; do uuidcolumn=$((uuidcolumn - 1)) thisuuid=${line:${uuidcolumn}:${UUIDLENGTH}} debug "uuidcol $uuidcolumn: thisuuid is [$thisuuid]" repl=$( printf "%-${UUIDLENGTH}s" "$(uuid_to_name $thisuuid)") debug "repl is $repl" [[ -n $repl ]] && line="${line/${thisuuid}/$repl}" done fi table2="${table2}${line}\n" ln=$((ln + 1)) done <<< "$table" profile_mark "replaced uuids with names" table="${table2}" ; unset table2 # save RAM debug "table after repl [\n$table]" # re-render table since field lengths may have changed table=$(flatten_table "$table") debug "table in csv is: [$table]" table=$(echo -e "$table" | column -t -s, ) debug "table reformatted is: [$table]" # add underlines table=$(echo -e "$table" | awk -v ndatalines=$ndatalines 'BEGIN { max=0 } { if (length($0) > max) max=length($0); } { if (match($0, "Server|TOTAL") == 1) { hdgline=1; } else { hdgline=0 } if (!hdgline && pfw != "" && $1 != pfw) { print " " } if (NR == 1 ) { printf "\033[1m\033[4m" $0 "\033[0m\n" } else if (NR == ndatalines) { print; for (n=0;n0) { idx=match(substr($0,start), "[a-zA-Z-]+_UUID"); if (idx>0) { uuidcols = uuidcols " " start+idx-1; start+=(idx+RLENGTH); } } } END { print uuidcols }' } function find_fieldwidths() { #echo "$*" | head -1 | awk 'BEGIN {start=1} { idx=1; while (idx>0) { idx=match(substr($0,start), "[a-zA-Z-]+"); if (idx>0) { start+=(idx+RLENGTH); } if (idx>1) { cols = cols " " start-1} } } END { print cols }' echo "$*" | head -1 | awk 'BEGIN {start=1} { idx=1; iter=1; while (idx>0) { idx=match(substr($0,start), "[a-zA-Z-]+"); start += RSTART; start += RLENGTH; this=start-laststart; if (this>0 && iter > 1) cols = cols " " this; laststart=start; iter++; } } END { print cols }' } function flatten_table() { local awkscript read -d '' awkscript << 'EOF' (NR == 1) { for (i=1; i<=NF; i++) { pos=index($0, $i) if (i>1) { len=pos-lastpos; wids = wids " " (pos-lastpos) } lastpos=pos; } len=length($0)-lastpos+1; wids = wids " " (length($0)-lastpos+1); FIELDWIDTHS=wids } { OFS="," $1=$1 for (i=1; i<=NF; i++) { gsub(" +$","",$i); } print } EOF echo -e "$*" | awk "$awkscript" } function json_extract() { # json_extract "jsondata" jsonfield1 var1 jsonfield2 var2 ... local jsondata jsonfield varname val rv=0 jsondata="$1" shift 1 while [[ $# -ge 2 ]]; do local thisrv jsonfield="$1" varname="$2" shift 2 val=$(echo "$jsondata" | jq -r "${jsonfield}" 2>/dev/null) thisrv=$? [[ -z $val ]] && $thisrv=1 [[ $thisrv -ne 0 ]] && val="" eval "$varname=\"$val\"" rv=$(($rv + $thisrv)) done return $rv } # populates global JSON_RESULTS function runaction() { # runaction targetlist options local targetlist local loc ob obuuid what api_endpoint jq_obj local start end opts opts_a local x epidx local actionname line all local force=0 foreground=0 f rv files n thisfile jqres goterror local good allgoodresults errs allbadresults # oooo change this local jqf='[ "_DC_", .job.id, "_OB_", .status ] | @csv' local jqf_bad='[ "_DC_", "_OB_", .fault.detail, .fault.reason, .status ] | @csv' local objecttype extrainfo start=$(($($GDATE +%s%N)/1000)) lastqsecs="" lastprocsecs="" what="$1" shift actionname="$1" GLOBALACTION="$actionname" shift targetlist="$1" shift opts="$*" opts_a=( $* ) epidx=$(getepidx $what) [[ $? -ne 0 ]] && error "unknown endpoint '$what'" && return 1 jq_obj="${ep_jqobj[$epidx]}" api_endpoint="${ep_apiendpoint[$epidx]}" [[ -z $api_endpoint ]] && error "no endpointname for endpoint '$what'" && return 1 debug "what = $what" debug "actionname = $actionname" debug "targetlist = $targetlist" debug "opts is $opts" arraycontains opts_a "-f" && force=1 || force=0 arraycontains opts_a "-F" && foreground=1 || foreground=0 JSON_RESULTS="" if [[ $foreground -eq 1 ]]; then local thistmpfile # just use one line line=$(echo "$targetlist" | head -1) loc=$(echo "$line" | cut -d, -f1) ob=$(echo "$line" | cut -d, -f2) obuuid=$(echo "$line" | cut -d, -f3) extrainfo="$(echo "$line" | cut -d, -f4-)" thistmpfile="$TMPDIR/run,$loc,$ob" runaction_loc $loc $epidx $actionname $obuuid $ob "$extrainfo" $opts > "$thistmpfile" jqres=$(cat ${thistmpfile} | jq . 2>/dev/null) if [[ $? -eq 0 ]]; then JSON_RESULTS="${JSON_RESULTS}$jqres" fi rm -f $TMPDIR/run,* else pids="" while read -r line ; do debug -f "processing line: [$line]" loc=$(echo "$line" | cut -d, -f1) ob=$(echo "$line" | cut -d, -f2) obuuid=$(echo "$line" | cut -d, -f3) extrainfo=$(echo "$line" | cut -d, -f4-) debug -f "--> loc = $loc" debug -f "--> ob = $ob" debug -f "--> obuuid = $obuuid" debug -f "--> extrainfo = $extrainfo" runaction_loc "$loc" "$epidx" "$actionname" "$obuuid" "$ob" "$extrainfo" $opts > "$TMPDIR/run,$loc,$ob" & pids="$pids $!" done <<< "$targetlist" wait $pids files=( $TMPDIR/run,* ) unset ACTIONRES_LOC unset ACTIONRES_MSG n=0 errs=0 good=0 rv=0 # Capitalise first letter objecttype=$(echo ${what:0:1} | tr '[a-z]' '[A-Z]')${what:1} # oooo # change this jq based on thr action allgoodresults="Server,${objecttype},Job Status" allbadresults="" for f in ${!files[@]} ; do local thiscsv thisfile="${files[$f]}" debug "processing file $thisfile" debug "contents: $(cat $thisfile)" ACTIONRES_LOC[$n]=$(echo "$thisfile" | cut -d, -f2) ACTIONRES_OB[$n]=$(echo "$thisfile" | cut -d, -f3) jqres=$(cat ${thisfile} | jq . 2>/dev/null) if [[ $? -eq 0 ]]; then ACTIONRES_MSG[$n]="$jqres" JSON_RESULTS="${JSON_RESULTS}$jqres" else ACTIONRES_MSG[$n]=$(cat ${thisfile}) fi if [[ ${ACTIONRES_MSG[$n]} == *detail*reason* ]]; then goterror=1 elif [[ ${ACTIONRES_MSG[$n]} == *rror* ]]; then goterror=1 elif [[ ${ACTIONRES_MSG[$n]} == *message* ]]; then goterror=1 else goterror=0 fi if [[ $goterror -eq 1 ]]; then #thiscsv_bad=$(echo "${ACTIONRES_MSG[$n]}" | jq -r "${jqf_bad}" | sed -e "s/_DC_/${ACTIONRES_LOC[$n]}/g" | sed -e "s/_OB_/${ACTIONRES_OB[$n]}/" | tr -d '"[]' | egrep -v "^$") thiscsv_bad=$(echo "${ACTIONRES_MSG[$n]}" | jq -r . | sed -e "s/_DC_/${ACTIONRES_LOC[$n]}/g" | sed -e "s/_OB_/${ACTIONRES_OB[$n]}/" | egrep -v "^$") allbadresults="${allbadresults}\n${thiscsv_bad}" errs=$((errs + 1)) else thiscsv=$(echo "${ACTIONRES_MSG[$n]}" | jq -r "${jqf}" | sed -e "s/_DC_/${ACTIONRES_LOC[$n]}/g" | sed -e "s/_OB_/${ACTIONRES_OB[$n]}/" | tr -d '"' | egrep -v "^$") allgoodresults="${allgoodresults}\n${thiscsv}" good=$((good + 1)) #echo -e "$GREEN" >/dev/stderr fi n=$((n + 1)) done echo if [[ $errs -gt 0 ]]; then local fullres_bad echo -e "${RED}$errs x '${BOLD}$actionname${PLAIN}${RED}' actions failed:${PLAIN}" echo -e "${RED}" echo "$allbadresults" | sed -e 's/^/ /' echo -e "${PLAIN}" echo fi if [[ $good -ge 1 ]]; then local fullres echo -e "${GREEN}$good x '${BOLD}$actionname${PLAIN}${GREEN}' actions submitted successfully.${PLAIN}" fullres=$(csv_to_table $(($good + 1)) "$allgoodresults") echo "$fullres" fi if [[ $good -ge 1 ]]; then rv=0 else rv=1 fi rm -f $TMPDIR/run,* fi end=$(($($GDATE +%s%N)/1000)) lastqsecs=$(echo "scale=2; ($end - $start) / 1000000;" | bc) return $rv } function validate_action() { # nodes/vms/etv actionname local what acion epidx what="$1" action="$2" debug "validating action '$action' on obtype '$what'" epidx=$(getepidx $what) [[ $? -ne 0 ]] && return 1 [[ ${ep_validactions[$epidx]} != *\ $action\ * ]] && return 1 return 0 } function getdata() { # getdata options local loc what api_endpoint jq_obj local start end opts opts_a parallel local idx thisopt outfile datawithheading bf local basefilter outmode bitswewant x epidx titles fields local heading cols wantcols local cmd fullres line everything uuidcols ln local usecache refilter local w greenwords yellowwords redwords uuidcol local errordebug=0 local quiet=0 ignorecase=0 start=$(($($GDATE +%s%N)/1000)) lastqsecs="" lastprocsecs="" what="$1" shift cmd="$1" shift set -f opts="$*" opts_a=( $* ) set +f epidx=$(getepidx $what) [[ $? -ne 0 ]] && error "unknown endpoint '$what'" && return 1 jq_obj="${ep_jqobj[$epidx]}" api_endpoint="${ep_apiendpoint[$epidx]}" [[ -z $api_endpoint ]] && error "no endpointname for endpoint '$what'" && return 1 #outmode=namesonly # global if [[ $cmd == "show" ]]; then outmode=namesonly # global elif [[ $cmd == "net" ]]; then outmode=verbose # global else outmode=verbose # global fi arraycontains opts_a "-r" && outmode=raw arraycontains opts_a "-q" && quiet=1 arraycontains opts_a "-v" && outmode=verbose arraycontains opts_a "-s" && outmode=namesonly arraycontains opts_a "-c" && ignorecase=1 arraycontains opts_a "-n" && usecache=0 || usecache=1 arraycontains opts_a "-e" && errordebug=1 debug "cmd is $cmd" debug "opts is $opts" outfile=$(getarrayopt opts_a o) refilter=$(getarrayopt opts_a f) # Allow standard globs rather than regexp globs [[ $refilter == "*" ]] && refilter=".*" if [[ -n $refilter ]]; then obfilter="select(.${ep_defaultfield[$epidx]}|test(\"^$refilter$\"" if [[ $ignorecase -eq 1 ]]; then obfilter="${obfilter}; \"i\"))" else obfilter="${obfilter}))" fi else obfilter="." fi [[ -n $refilter ]] && [[ $quiet -eq 0 ]] && info "${what} filter: ^$refilter$" debug "refilter is $refilter" debug "obfilter is $obfilter" # determine jq filter based on object name jqfilter="" jqscript="" awkscript="" if [[ $cmd == "list" ]]; then if [[ $outmode == "verbose" ]]; then if [[ -z ${ep_fields[$epidx]} ]]; then error "Verbose mode not supported for the '$api_endpoint' endpoint" return 1 else #bitswewant=" [ .name, .address, .status, .spm.status ]" basefilter=$(getbasefilter $epidx "$refilter") #basefilter="([\"DC\", \"Node \",\"IP \",\"Status\",\"SPM\"] ), (.$jq_obj[] | [\"_LOCATION_\", .name, .address, .status, .spm.status]) | @csv" fi elif [[ $outmode == "namesonly" ]]; then basefilter=".[] | $obfilter | .${ep_defaultfield[$epidx]}" debug "(list) basefilter is $basefilter" if [[ $GLOBALCMD == "action" ]]; then basefilter="${basefilter} + \",\" + .${ep_idfield[$epidx]}" if [[ ${ep_name[$epidx]} == "nodes" ]]; then basefilter="${basefilter} + \",\" + (.console|tostring)" fi fi elif [[ $outmode == "raw" ]]; then [[ $obfilter != "." ]] && basefilter=".[] | $obfilter" fi elif [[ $cmd == "net" ]]; then jqscript="$SCRIPTDIR/diagram.jq" #awkscript="$SCRIPTDIR/detail.awk" elif [[ $cmd == "show" ]]; then if [[ $outmode == "verbose" ]]; then error "Verbose mode not supported for show command yet" return 1 elif [[ $outmode == "raw" ]]; then error "Raw mode not supported for show command yet - try 'list' instead" else # namesonly #basefilter=".$jq_obj[] | select(.name|test(\"${arg_array[1]}\"))" basefilter=".[] | $obfilter" #basefilter=".[]" jqscript="$SCRIPTDIR/${jq_obj}.jq" awkscript="$SCRIPTDIR/detail.awk" debug "jqscript is $jqscript" debug "awkscript is $awkscript" if [[ ! -z $jqscript && ! -e $jqscript ]]; then error "'show' not yet implemented for object type '$jq_obj'" return 1 fi fi fi locnum=1 if [[ -z $outfile ]]; then pids="" notify "Obtaining data from gns API" for loc in $curlocs; do [[ ! -z $basefilter ]] && jqfilter=${basefilter/_LOCATION_/${loc}} [[ $VERBOSE -eq 1 ]] && debug "jqfilter is $jqfilter" [[ $VERBOSE -eq 1 ]] && debug "jqscript is $jqscript" [[ $VERBOSE -eq 1 ]] && debug "awkscript is $awkscript" #echo "jqfilter is:" >/dev/stderr #echo "$jqfilter" >/dev/stderr debug " starting getdata $loc $epidx $opts" getdata_loc $loc $epidx $opts > "$TMPDIR/get.$loc" 2>"$TMPDIR/err.$loc" & pids="$pids $!" locnum=$((locnum + 1)) done wait $pids debug all pids finished if [[ $outmode == "raw" ]]; then all=$(cat "$TMPDIR"/get.* 2>/dev/null) else all=$(cat "$TMPDIR"/get.* 2>/dev/null | tr -d : | egrep -v 'DC|Server' | sed '/^[[:space:]]*$/d') fi if [[ -z $all ]]; then fail cat "$TMPDIR"/err.* rm -f "$TMPDIR"/get.* rm -f "$TMPDIR"/err.* return 1 fi ok if [[ $errordebug -eq 1 ]]; then if [[ $( ls "$TMPDIR"/err.* | wc -l | bc) -gt 0 ]]; then cat "$TMPDIR"/err.* >/dev/stderr fi fi #debug "combined results: [$all]" end=$(($($GDATE +%s%N)/1000)) lastqsecs=$(echo "scale=2; ($end - $start) / 1000000;" | bc) start=$(($($GDATE +%s%N)/1000)) rescount=$(printf %d $(echo -n "$all" | wc -c)) if [[ $rescount -eq 0 ]]; then echo -e " ${BOLD}${YELLOW}(no results)${PLAIN}" echo rm -f $TMPDIR/get.* rm -f $TMPDIR/err.* elif [[ $outmode == "verbose" && $cmd == "list" ]]; then # totals # get col numbers which we need to total heading=$(cat $TMPDIR/get.* | head -1 | tr ',' ' ') debug "heading is [$heading]" cols=( $heading ) wantcols="" for x in ${!cols[@]}; do debug " checking ${cols[$x]}" if [[ ${cols[$x]} =~ CPUs|RAM|VMs ]]; then wantcols="$wantcols,$((x + 1))" fi done debug "wantcols is [$wantcols]" if [[ ! -z $wantcols ]]; then wantcols="$wantcols," # generate totals line cat $TMPDIR/get.* | column -t -s, | awk -vCOLS="${wantcols}" '{ gsub(" GB", "_GB"); if (index($0,"Server") == 1 && maxf == "") { maxf = NF } else { split(COLS, c, ","); for (x in c) { if (c[x]) { this = $(c[x]); rv=gsub("_GB","",this); if (rv!=0) { isgb[c[x]]=1; } tot[c[x]] += this; } } } gsub("_GB"," GB"); } END { line=""; for (i=1; i<=maxf; i++) { if (i==1) { str="TOTAL:"} else if (index(COLS, "," i ",")) { str = tot[i]; } else { str=" "} line = line sprintf("%s%s,",str, isgb[i] ? " GB" : ""); } printf("%s\n",line); }' > "$TOTFILE" ndatalines=$(printf %d $(cat $TMPDIR/get.* | wc -l)) else cp /dev/null "$TOTFILE" ndatalines=-1 fi # print combined data (with only one heading row) and totals #everything=$(cat $TMPDIR/get.* "$TOTFILE" | column -t -s,) everything=$(cat $TMPDIR/get.* "$TOTFILE") notify "Processing data" fullres=$(csv_to_table $ndatalines "$everything") ok && echo 1>&2 echo "$fullres" debug "fullres is: [$fullres]" rm -f $TOTFILE elif [[ $cmd == "net" ]]; then local dotscript dev port x repl local updated_result="" # replace uuids with names fullres=$(cat $TMPDIR/get.*) while read -r line ; do if [[ $line =~ .*,.* ]]; then dev[0]=$(echo "$line" | awk -F, '{ print $1 }') port[0]=$(echo "$line" | awk -F, '{ print $2 }') dev[1]=$(echo "$line" | awk -F, '{ print $3 }') port[1]=$(echo "$line" | awk -F, '{ print $4 }') for x in ${!dev[@]}; do repl="$(uuid_to_name ${dev[$x]})" if [[ $? -eq 0 ]]; then dev[$x]="${repl}" fi done #updated_result="${updated_result} \"${dev[0]}\":\"${port[0]}\" -- \"${dev[1]}\":\"${port[1]}\"\n" updated_result="${updated_result} \"${dev[0]}\" -- \"${dev[1]}\"[label=\"${dev[0]}:${port[0]}\\\n${dev[1]}:${port[1]}\"]\n" fi done <<< "$fullres" dotscript="digraph {\n$(echo "$updated_result")\n}" debug "dotscript:\n${dotscript}" if [[ -n $GRAPHEASY ]]; then updated_result="${UNDERLINE}Network diagram for ${BOLD}$curproj${PLAIN}" updated_result="$updated_result\n\n$(echo -e "$dotscript" | $GRAPHEASY --from=dot --as_ascii)" else error "graph-easy not installed - try 'sudo cpan Graph::Easy then use -g to specify path." fi else # ie. show fullres=$(cat $TMPDIR/get.*) if [[ $outmode == "raw" ]]; then echo "$fullres" else local updated_result="" # Replace UUIDS with names from cache # and colourise certain words greenwords="true up enabled" redwords="false down paused uninitialized disabled" yellowwords="migrating" uuidcol="$PURPLE" while read -r line ; do key=${line%%:*} val=${line##*:} val=$(echo "$val" | sed -e 's/^.* //') if [[ $key =~ [A-Za-z]+\ UUID ]]; then repl="$(uuid_to_name ${val})" if [[ $? -eq 0 ]]; then line="${line/${val}/$repl} (${uuidcol}${val}${PLAIN})" else line="${line/$val/${uuidcol}${val}${PLAIN}}" fi #if [[ -n $repl ]]; then # #line=$(echo "${line}" | sed -e 's/[A-Za-z]\+ UUID/UUID/') # line="${line/${val}/$repl}" #fi elif [[ $key =~ UUID ]]; then # colourise the entire key line="${line/${val}/${uuidcol}${val}$PLAIN}" fi updated_result="${updated_result}${line}\n" done <<< "$fullres" fi #cat $TMPDIR/get.* fi updated_result=$(echo "$updated_result" | "$GSED" "$ADD_COLOURS") echo -e "$updated_result" rm -f $TMPDIR/get.* else # serial cp /dev/null "$outfile" notify "Obtaining data from gns API into file" for loc in $curlocs; do jqfilter=${basefilter/_LOCATION_/${loc}} getdata_loc $loc $epidx $opts >> "$outfile" locnum=$((locnum + 1)) done if [[ $errordebug -eq 1 ]]; then if [[ $( ls "$TMPDIR"/err.* | wc -l | bc) -gt 0 ]]; then cat "$TMPDIR"/err.* >/dev/stderr fi fi ok #if [[ -e ${HEADINGFILE} ]]; then # datawithheading=$(cat ${HEADINGFILE} ${outfile}) # echo "${datawithheading}" > "$outfile" # rm -f $HEADINGFILE #fi info $(printf "%d line(s) written to %s" $(cat "$outfile" | wc -l) "$outfile" ) fi #end=$(($(gdate +%s%N)/1000)) #lastqsecs=$(echo "scale=2; ($end - $start) / 1000000;" | bc) end=$(($($GDATE +%s%N)/1000)) lastprocsecs=$(echo "scale=2; ($end - $start) / 1000000;" | bc) } function listcommands() { local x wantadm if [[ $1 == "ADMIN" ]]; then wantadm=1 prefix='\' else wantadm=0 prefix="" fi for x in ${!cmd_name[@]}; do if [[ ${cmd_adm[$x]} -eq $wantadm ]]; then printf " %-10s %s\n" "${prefix}${cmd_name[$x]}" "${cmd_desc[$x]}" if [[ $wantadm -eq 0 && ${cmd_name[$x]} == "help" ]]; then # show that there is admin help available printf " %-10s %s\n" "\\help" "List admin commands." fi fi done } function listcommandaliases() { local x format format=" %-12s|%-24s|%-36s${PLAIN}\n" printf "${UNDERLINE}${BOLD}$format" "Alias" "Command" "Default Args" for x in ${!cmdalias_src[@]}; do printf "$format" "${cmdalias_src[$x]}" "${cmdalias_dst[$x]}" "${cmdalias_defaultarg[$x]}" done } function generate_random_string() { local segmentlen=5 x str="" thissegment for x in {1..5}; do thissegment=$(LC_CTYPE=C tr -dc a-z0-9 < /dev/urandom | fold -w ${segmentlen} | head -n 1) [[ -n $str ]] && str="${str} " str="${str}${thissegment}" done echo "$str" } function expand_cmdalias() { # expand_cmdalias "cmd" newcmd_var newargs_var "args" local origcmd newcmd x firstword rest defarg changed=0 args local newarg_varname newargs local newcmd_varname local repl_cmd repl_args origcmd="$1" shift newcmd_varname="$1" shift newarg_varname="$1" shift args="$*" newargs="$args" # default newcmd="$origcmd" # default for x in ${!cmdalias_src[@]} ; do if [[ ${cmdalias_src[$x]} == $origcmd ]]; then debug " matched" # Matched # cmd is FIRST WORD of _dst. rest goes before args. repl_cmd="${cmdalias_dst[$x]%% *}" repl_args="${cmdalias_dst[$x]#* }" newcmd="$repl_cmd" if [[ -n $args ]]; then newargs="$repl_args $args" else debug "using default arg $defarg" defarg=${cmdalias_defaultarg[$x]} defarg=${defarg/_CURPROJECT_/$curproj} debug " default arg is: [$defarg]" newargs="$repl_args $defarg" fi changed=1 break fi done eval "$newcmd_varname=\"$newcmd\"" eval "$newarg_varname=\"$newargs\"" if [[ $changed -eq 1 ]]; then debug "expanded [$origcmd] to [$cmd], args is [$newargs]" return 0 fi return 1 } function getnodetypeforadd() { local nt nt=$(echo "$1" | tr 'A-Z' 'a-z' | tr ' ' '_') echo "$nt" } # populates globals: # ndcs # nobs # dc_ess # ob_ess # data # allobs # alluids # o_arr # ou_arr function validate_action_obs() { # whattolist actionfilter showerroropt multiple_obs_allowed [retvar_selobname] [retvar_selobuuid] local whattolist actionfilter showerroropt local selobname="" selobuuid="" local retvar_obname retvar_obuuid multiallowed whattolist="$1" actionfilter="$2" showerroropt="$3" multiallowed="$4" retvar_obname="$5" retvar_obuuid="$6" debug "whattolist=$whattolist" debug "actionfilter=$actionfilter" debug "showerroropt=$showerroropt" debug "multiallowed=$multiallowed" debug "retvar_obname=$retvar_obname" debug "retvar_obuuid=$retvar_obuuid" getdata ${whattolist} list "-f${actionfilter}.*" $showerroropt -c -s -q >"$TMPFILE" rv=$? debug "actionfilter=$actionfilter" if [[ $rv -ne 0 ]]; then error "Can't find any ${whattolist}s matching '${BOLD}$actionfilter'$PLAIN$RED." elif [[ ! -e $TMPFILE ]]; then error "No matching ${whattolist}s found." elif grep -q "no results" $TMPFILE; then error "No ${whattolist}s found matching '$obname'." elif [[ ! -e $TMPFILE ]]; then error "Results file does not exist." else ok data=$(cat "$TMPFILE" | egrep -v "^$") ndcs=$( printf %d $(echo "$data" | awk -F, '{ print $1 }' | sort -u | wc -l)) nobs=$( printf %d $(echo "$data" | awk -F, '{ print $2 }' | sort -u | wc -l)) [[ $ndcs -eq 1 ]] && dc_ess="" || dc_ess="s" [[ $nobs -eq 1 ]] && ob_ess="" || ob_ess="s" allobs=$(echo "$data" | awk -F, '{ print $2 }' | sort -u) alluuids=$(echo "$data" | awk -F, '{ print $3 }' | sort -u) debug "allobs is $allobs" debug "alluuids is $alluuids" o_arr=($allobs) ou_arr=($alluuids) if [[ $nobs -gt 1 && $multiallowed -eq 0 ]]; then debug "actionfilter=$actionfilter" notify_nodots "'${BOLD}${actionfilter}${PLAIN}${PURPLE}' matched multiple ${whattolist}s" narrowdown "$whattolist" "$allobs" "$alluuids" selobname selobuuid || rv=1 else selobname="$allobs" selobuuid="$alluuids" fi fi [[ -n $retvar_obname ]] && eval "$retvar_obname=\"$selobname\"" [[ -n $retvar_obuuid ]] && eval "$retvar_obuuid=\"$selobuuid\"" rm -f "$TMPFILE" return $rv } function narrowdown() { # whattolist "allobs" "all_uuids" retvar_newnodetype retvar_newnodetype_uuid local whattolist allobs o_arr alluuids ou_arr local o n rv=0 local newnodetype="" newnodetype_uuid="" local retvar_newnodetype retvar_newnodetype_uuid whattolist="$1" allobs="$2" alluuids="$3" retvar_newnodetype="$4" retvar_newnodetype_uuid="$5" o_arr=($allobs) ou_arr=($alluuids) while [[ -z $newnodetype ]]; do echo n=1 while read -r o; do printf "%3d. %s\n" $n "$o" n=$((n + 1)) done <<<"$allobs" echo getstr ":" "" "Select one (q to abort)" if [[ -n $retstr ]]; then if [[ $retstr == "q" ]]; then rv=1 break elif [[ $retstr =~ ^[0-9]*$ ]]; then if [[ $retstr -le 0 || $retstr -ge $n ]]; then error "Invalid selection" else newnodetype="${o_arr[$((retstr - 1))]}" newnodetype_uuid="${ou_arr[$((retstr - 1))]}" fi else local matched=0 x allmatches="" this thisuuid for x in ${!o_arr[@]}; do this="${o_arr[$x]}" thisuuid="${ou_arr[$x]}" shopt -s nocasematch if [[ ${this} =~ $retstr ]]; then newnodetype="$this" newnodetype_uuid="$thisuuid" allmatches="$allmatches [$this]" matched=$((matched + 1)) fi shopt -u nocasematch done if [[ $matched -eq 0 ]]; then error "'$retstr' doesn't match any choice" newnodetype="" elif [[ $matched -gt 1 ]]; then error "'$retstr' matched multiple choices: $allmatches" newnodetype="" fi fi fi done eval "$retvar_newnodetype=\"$newnodetype\"" eval "$retvar_newnodetype_uuid=\"$newnodetype_uuid\"" return $rv } function processcmd() { local cmd arg newarg rv newlocs x err admin idx opts pipe gotargs local whattolist actionname="" actionfilter="" local showerror=0 showerroropt="" local epidx endpoint origendpoint builtin newname local newnodex=0 newnodey=0 local dev devuuid port portnum adapnum local BUILTINMODELS="Cloud|VPCS|NAT|Frame Relay switch|Ethernet hub|Ethernet switch" local oldname olduuid cmd=$1 shift arg="$*" shift [[ -z ${cmd:0:1} ]] && return 0 [[ ${cmd:0:1} == "#" ]] && return 0 if [[ ${cmd:0:1} == \\ ]]; then cmd=${cmd:1} admin=1 else admin=0 fi idx=$(getcmdidx "$cmd" $admin) rv=$? if [[ $rv -ne 0 ]]; then # check aliases debug "pre replacedargs is [$args]" expand_cmdalias "$cmd" replacedcmd replacedargs "$arg" debug "post replacedargs is [$replacedargs]" cmd="$replacedcmd" arg="$replacedargs" idx=$(getcmdidx "$cmd" $admin) rv=$? fi if [[ $rv -ne 0 ]]; then if [[ $admin -eq 1 ]]; then error "Invalid admin command '\\$cmd'" else error "Invalid command '$cmd'" fi return 1 fi # replace command aliases with base command name cmd=${cmd_name[$idx]} GLOBALCMD="$cmd" # strip pipe suffix if [[ $arg == *\|* ]]; then pipe=${arg#*|} arg=${arg%%|*} else pipe="cat" fi # strip options from arguments declare -a opts newarg="" for x in $arg; do if [[ ${x:0:1} == - ]]; then opts+=($x) else [[ -z $newarg ]] && newarg="$x" || newarg="$newarg $x" fi done arg="$newarg" arg_array=( $arg ) if arraycontains opts "-h"; then showcmdhelp "$cmd" return 0 fi if arraycontains opts "-e"; then showerror=1 showerroropt="-e" fi gotargs=$( printf %d $(wc -w <<< "$arg")) if [[ $gotargs -lt ${cmd_minargs[$idx]} || $gotargs -gt ${cmd_maxargs[$idx]} ]]; then error "${cmd_name[$idx]} requires ${cmd_needargs[$idx]} arguments (got $gotargs)" return 1 fi obname="" # global if [[ $cmd == "show" ]]; then endpoint=${arg_array[0]} && unset 'arg_array[0]' whattolist=${endpoint} [[ ${#arg_array[@]} -ge 1 ]] && opts+=("-f${arg_array[@]}") elif [[ $cmd == "list" ]]; then endpoint=${arg_array[0]} && unset 'arg_array[0]' whattolist=${endpoint} [[ ${#arg_array[@]} -ge 1 ]] && opts+=("-f${arg_array[@]}") elif [[ $cmd == "net" ]]; then whattolist="links" opts+="-e" elif [[ $cmd == "action" ]]; then endpoint=${arg_array[0]} && unset 'arg_array[0]' origendpoint="$endpoint" whattolist=${endpoint} actionname=${arg_array[1]} && unset 'arg_array[1]' if [[ $actionname == "add" ]]; then newname=${arg_array[2]} if [[ -z $newname ]]; then error "Name of new $whattolist not provided." return 1 fi if [[ $endpoint == "node" ]]; then newnodetype="${arg_array[3]}" # eg. VPCS, Cisco IOS, etc # is it a builtin appliance? if [[ $newnodetype =~ ^($BUILTINMODELS)$ ]]; then debug "adding a gns3 built-in node ($newnodetype)" builtin=1 whattolist="model" actionfilter="-f.*${newnodetype}.*" else debug "adding a gns3 appliance ($newnodetype)" builtin=0 # we will lookup the template ID of "nodetype" later... whattolist="model" actionfilter="-f.*${newnodetype}.*" fi elif [[ $endpoint == "link" ]]; then local x idx failed=0 idx=2 for x in 0 1; do dev[$x]=${arg_array[$idx]}; idx=$((idx + 1)) port[$x]=${arg_array[$idx]}; idx=$((idx + 1)) # replace * with .* port[$x]=$(echo "${port[$x]}" | sed 's#[^\.]\*#\.\*#g') if [[ -z ${dev[$x]} || -z ${port[$x]} ]]; then failed=1 fi done if [[ $failed -eq 1 ]]; then error "usage: action link add adev aport zdev zport" return 1 fi fi elif [[ $actionname == "mv" ]]; then oldname=${arg_array[2]} newname=${arg_array[3]} actionfilter="-f.*${oldname}.*" else # start/stop/etc if [[ ${#arg_array[@]} -ge 1 ]]; then actionfilter="-f${arg_array[@]}" obname="${arg_array[@]}" else error "Name of $whattolist not provided." return 1 fi fi fi rv=1 if [[ $cmd == "help" && -n $arg ]]; then showcmdhelp "$arg" rv=$? elif [[ $admin -eq 1 ]]; then case ${cmd} in ls) listservers rv=$? ;; lp) listprojects rv=$? ;; p) setproject "${arg}" rv=$? [[ $rv -eq 0 ]] && start_recache ;; c) start_recache rv=$? [[ $rv -ne 0 ]] && error "UUID re-cache is already in progress (PID $recache_pid)" ;; sc) if [[ -z ${arg} ]]; then for x in ${!uuid_id[@]}; do echo "UUID: ${uuid_id[$x]} -> ${uuid_name[$x]}" done else echo " UUID ${arg} -> $(uuid_to_name ${arg})" fi ;; q) DONE=1 ;; la) listcommandaliases ;; help) listcommands ADMIN ;; *) error "Admin command '$cmd' not implemented" rv=1 ;; esac else case ${cmd} in list|show|net) getdata ${whattolist} $cmd ${opts[@]} >"$TMPFILE" rv=$? [[ -e $TMPFILE ]] && cat "$TMPFILE" | $pipe if [[ $rv -eq 0 ]]; then local timestr timestr=$(printf "query time: %s seconds" "$lastqsecs") [[ -n $lastprocsecs ]] && timestr="${timestr}$(printf ", processing time: %s secs" "$lastprocsecs")" echo info "$timestr" fi rm -f "$TMPFILE" ;; action) # TODO: ooremove any output format opts validate_action ${endpoint} $actionname if [[ $? -ne 0 ]]; then error "'$actionname' is not a valid action for ${endpoint}s" return 1 fi epidx=$(getepidx $endpoint) if [[ $? -ne 0 ]]; then error "'$endpoint' is not a valid endpoint" return 1 fi if [[ -n $whattolist ]]; then local ndcs nobs data confirm=0 # Get a list of objects to operate on # ie. turn regexp into a list of dcs and obnames first echo if [[ $actionname == "connect" || $actionname == "mv" ]]; then validate_action_obs ${whattolist} "$actionfilter" "$showerroropt" $NOMULTI || return $? if [[ $nobs -gt 1 ]]; then local allobs=$(echo "$data" | awk -F, '{ print $2 }' | sort -u | tr '\n' ',') error "Can't run '$actionname' with multiple objects (matched: ${allobs%,})" return 1 else confirm=1 fi elif [[ $actionname == "add" ]]; then if [[ $endpoint == "node" ]]; then validate_action_obs ${whattolist} "$actionfilter" "$showerroropt" $NOMULTI newnodetype newnodetype_uuid if [[ -z $newnodetype ]]; then confirm=0 else debug "newnodetype is $newnodetype" debug "newnodeuuid is $newnodetype_uuid" notify_nodots "Adding a new ${BOLD}$newnodetype${PLAIN}${PURPLE} named $BOLD$newname${PLAIN}" confirm=1 fi elif [[ $endpoint == "link" ]]; then local x json adaplist portnumlist portlist nports newn newu jqs local jqs_name jqs_num for x in 0 1; do local letter portlower [[ $x -eq 0 ]] && letter=A || letter=Z debug "about to validate link $x" validate_action_obs "node" "${dev[$x]}" "$showerroropt" $NOMULTI newn newu || return $? dev[$x]="$newn" devuuid[$x]="$newu" debug "${letter}-node ${dev[$x]} is OK (${dev[$x]})" debug "getting node $x ports" json=$(getdata nodes list -q -e -r "-f${dev[$x]}") portlower=$(echo ${port[$x]} | tr 'A-Z' 'a-z') jqs=".ports[] | select(.name|ascii_downcase|test(\".*${portlower}.*\"))" jqs_name="${jqs} | .short_name" jqs_num="${jqs} | .port_number" jqs_adapnum="${jqs} | .adapter_number" portlist=$(echo "$json" | jq -r "$jqs_name") portnumlist=$(echo "$json" | jq -r "$jqs_num") adapnumlist=$(echo "$json" | jq -r "$jqs_adapnum") debug "portlist is $portlist" debug "portnumlist is $portnumlist" debug "adapnumlist is $adapnumlist" #debug "raw port data is\n$json" nports=$(wc -l <<< "$portlist" | bc) if [[ -z $portlist || $nports -eq 0 ]]; then error "${letter}-node ${BOLD}${dev[$x]}$PLAIN$RED has no ports matching '${BOLD}${port[$x]}$PLAIN$RED'." elif [[ $nports -gt 1 ]]; then error "${letter}-node ${BOLD}${dev[$x]}$PLAIN$RED has multiple ports matching '${BOLD}${port[$x]}$PLAIN$RED':" echo -e "${RED}$portlist${PLAIN}" | sed -e 's/^/ /' else port[$x]="$portlist" portnum[$x]="$portnumlist" adapnum[$x]="$adapnumlist" debug "${letter}-node ${dev[$x]} port ${port[$x]} is OK (${dev[$x]})" fi done notify_nodots "Adding a link from ${BOLD}${dev[0]} ${port[0]}${PLAIN}${PURPLE} to ${BOLD}${dev[1]} ${port[1]}${PLAIN}" confirm=1 fi else # action is not add/connect validate_action_obs ${whattolist} "$actionfilter" "$showerroropt" $MULTI || return $? echo -e "${PURPLE}About to run '${BOLD}$actionname${PLAIN}${PURPLE}' on ${BOLD}${nobs}${PLAIN}${PURPLE} ${whattolist}${ob_ess} on ${BOLD}${ndcs}${PLAIN}${PURPLE} server${dc_ess}:${PLAIN}" echo "$data" | awk -F, "BEGIN {lastdc=\"\"} { if (\$1 != lastdc) { print \" ${YELLOW}- ${BOLD}\" \$1 \"${PLAIN}\"; lastdc=\$1; } print \" ${YELLOW}- \" \$2 \"${PLAIN}\"}" echo if [[ $ndcs -le 1 ]]; then getyn n "Really proceed" [[ $? -eq 0 ]] && confirm=1 || confirm=0 else local confirmcode entered_string confirmcode=$(generate_random_string) notify_nodots "Confirmation code is: ${BOLD}${confirmcode}${PLAIN}" getstr ":" "n" "Enter the above code to proceed, anything else will abort" entered_string="$retstr" [[ $entered_string == $confirmcode ]] && confirm=1 || confirm=0 fi fi else # we dont need to do a data query newnodetype_uuid="" confirm=1 fi if [[ $confirm -eq 1 ]]; then if [[ $actionname == "add" ]]; then local postdata if [[ $endpoint == "node" ]]; then # We use alternate values for the actiontarget string here: # loc=servername (normal) # ob=name of new ob to add # obuuid=uuid of model # extrainfo=curl post data in the form of key1:val1^key2:val2... # the endpoint we call depends on whether # we are adding a gns3 builtin node or an appliance if [[ $builtin -eq 1 ]]; then local nodetypeforadd # gns3 wants the nodetype in the post data and in a # special format. nodetypeforadd=$(getnodetypeforadd $newnodetype) postdata="name:$newname^node_type:$nodetypeforadd^compute_id:local^template_id:$newnodetype_uuid" # ...and NOT in the URL newnodetype="" newnodetype_uuid="" else # ie. use an appliance template postdata="x:$newnodex^y:$newnodey" endpoint="model" fi actiontargets="$curlocs,$newname,$newnodetype_uuid,$postdata" #actiontargets="${actiontargets},$postdata" elif [[ $endpoint == "link" ]]; then #POST 'http://localhost:3080/v2/projects/bfb83f16-dab4-445a-a572-d7cc1801222e/links' -d '{"nodes": [{"adapter_number": 0, "label": {"text": "Text", "x": 42, "y": 0}, "node_id": "d3601934-e39a-4865-9cf5-3e46e1fe24e2", "port_number": 3}, {"adapter_number": 0, "node_id": "d3601934-e39a-4865-9cf5-3e46e1fe24e2", "port_number": 4}]}' RAWJSONPOSTDATA="{\"nodes\": [" for n in 0 1; do RAWJSONPOSTDATA="${RAWJSONPOSTDATA}{\"node_id\": \"${devuuid[$n]}\", \"adapter_number\": ${adapnum[$n]}, \"port_number\": ${portnum[$n]}}" [[ $n -eq 0 ]] && RAWJSONPOSTDATA="${RAWJSONPOSTDATA}," done RAWJSONPOSTDATA="${RAWJSONPOSTDATA}]}" echo -e "${YELLOW}${BOLD}rawjson is:\n${RAWJSONPOSTDATA}${PLAIN}" >/dev/stderr actiontargets="$curlocs,,," fi elif [[ $actionname == "mv" ]]; then oldname=$(echo "$data" | awk -F, '{ print $2 }' | sort -u) olduuid=$(echo "$data" | awk -F, '{ print $3 }' | sort -u) actiontargets="$curlocs,$oldname,$olduuid,name:$newname" else actiontargets=$(echo "$data") fi if [[ $actionname == "connect" ]]; then local devname sevname srvport devname=$(echo "$data" | awk -F, '{ print $2 }') srvname=$(echo "$data" | awk -F, '{ print $1 }') srvport=$(echo "$data" | awk -F, '{ print $4 }') notify_nodots "Connecting to $devname ($srvname:$srvport)" opts+=("-F") else notify "Submitting actions to gns API" fi rm -f "$TMPFILE" debug "about to call runaction with:" debug " endpoint = $endpoint" debug " obtype = $endpoint" debug " actionname = $actionname" debug " actiontargets = $actiontargets" debug " opts = ${opts[@]}" debug " outputfile = ${TMPFILE}" runaction ${endpoint} $actionname "$actiontargets" ${opts[@]} >"$TMPFILE" rv=$? if [[ $actionname == "add" && $origendpoint == "node" && $rv -eq 0 && $builtin -eq 0 ]]; then local fail=0 # we now need to rename the newly created object - gns3 # will always generate a name based on the template debug "json is $JSON_RESULTS" json_extract "$JSON_RESULTS" .name oldname .node_id olduuid if [[ $? -eq 0 ]]; then runaction ${origendpoint} mv "$curlocs,$oldname,$olduuid,name:$newname^" -F [[ $? -ne 0 ]] && fail=1 else fail=1 fi if [[ $fail -eq 1 ]]; then warn "Failed to rename new object to '$newname' - please check." fi fi if [[ $actionname != "connect" ]]; then ok [[ -e $TMPFILE ]] && cat "$TMPFILE" | $pipe echo info $(printf "action submission time: %s seconds" "$lastqsecs") fi fi # end if confirm == 1 rm -f "$TMPFILE" ;; exit) DONE=1 ;; help) listcommands NOADMIN ;; *) error "Command '$cmd' not implemented" rv=1 ;; esac fi return $rv } function setproject() { # setproject [project_name] local newproj x err rv idx newproj="${1}" err=0 if [[ -z $newproj ]]; then newproj=${proj_name[0]} if [[ -z $newproj ]]; then error "Could not set default project - none found on server!" return 1 fi fi idx=$(getprojidx ${newproj}) if [[ $? -ne 0 ]]; then error "invalid project '$newproj'" err=1 fi curprojid=${proj_id[$idx]} if [[ $err -eq 0 ]]; then curproj="${newproj}" info "Project set to: $curproj ($curprojid)" rv=0 else rv=1 fi return $rv } function checkfor() { # checkfor name [pkg_name] local what pkgname local hw os ok=0 confirm=0 local alt="" rv globvar="" local realpath="" what=$1 pkg_name=${2:-$what} os=$(uname -s) hw=$(uname -m) if [[ $hw == "aarch64" ]]; then # Android Termux alias which="command -v" fi if [[ $what == "gdate" ]]; then alt=date globvar=GDATE elif [[ $what == "gsed" ]]; then alt=sed globvar=GSED elif [[ $what == "graph-easy" ]]; then globvar=GRAPHEASY fi realpath=$(which $what 2>&1) rv=$? if [[ $rv -ne 0 ]]; then if [[ -n $alt ]]; then $alt --version 2>/dev/null | grep -q GNU 2>/dev/null if [[ $? -eq 0 ]]; then realpath=$(which $alt 2>/dev/null) rv=0 fi fi fi if [[ $rv -eq 0 ]]; then if [[ -n $globvar ]]; then debug "found $what at $realpath - have set \$$globvar" eval "$globvar=\"$realpath\"" else debug "found $what at $realpath - no globvar" fi ok=1 else error "$what binary not found in \$PATH." getyn n "Try to install it?" [[ $? -eq 0 ]] && confirm=1 || confirm=0 if [[ $confirm -eq 1 ]]; then case $os in Darwin) brew install $pkg_name && ok=1 ;; Linux) if [[ -f /etc/debian_version ]]; then apt-get install $pkg_name && ok=1 elif [[ -f /etc/redhat_release ]]; then apt-get install $pkg_name && ok=1 elif [[ $hw == "aarch64" ]]; then apt install $pkg_name && ok=1 else error "Don't know how to install stuff on OS '$os' HW '$hw'" fi ;; *) error "Don't know how to install stuff on OS '$os' HW '$hw'" ;; esac [[ $ok -eq 1 ]] && info "Successfully installed $pkg_name" || error "Failed to install $pkg_name" fi fi which $what >/dev/null 2>&1 rv=$? [[ $rv -eq 0 && -n $globvar ]] && eval "$globvar=\"$realpath\"" return $rv } function killtmux() { tmux kill-session -t "$1" >/dev/null 2>&1 } function cleanup() { local x echo notify "Killing remaining tmux sessions" for x in ${proj_name[@]}; do killtmux "$x" done ok notify "Cleaning up temporary files" [[ -n $TMPDIR && -d $TMPDIR ]] && rm -f "$TMPDIR/*" ok } function processarg() { case "$i" in g) GRAPHEASY="${OPTARG}" if [[ ! -x "$GRAPHEASY" ]]; then error "Provided graph-easy binary not found or not executable ($GRAPHEASY)" exit 1 fi ;; h) usage; exit 1; ;; I) scriptstoinline exit $?; ;; i) initauth; exit $?; ;; p) DEFPROJECT="${OPTARG}" ;; P) PROFILING=1 ;; v) VERBOSE=1 info verbose mode ;; *) error "invalid argument: $i"; usage; ;; esac } function loadservers() { local sname shost sport if [[ -e $SRVFILE ]]; then notify "Loading servers from $SRVFILE" while read -r f ; do sname=$(echo "$f" | awk -F: '{ print $1 }') shost=$(echo "$f" | awk -F: '{ print $2 }') sport=$(echo "$f" | awk -F: '{ print $3 }') if [[ -z $sname || -z $shost || -z $sport ]]; then fail error "Bad line in server file: $f" return 1 fi addloc $sname $shost $sport done < <(egrep -v "(^#|^$)" $SRVFILE) fi ok "Got $nlocs servers" return 0 } BOLD="\033[1m" PLAIN="\033[0m" ITALIC="\033[3m" UNDERLINE="\033[4m" RED="\033[31m" YELLOW="\033[33m" GREEN="\033[32m" BLUE="\033[34m" PURPLE="\033[35m" CYAN="\033[36m" LINK="$BLUE$UNDERLINE" UNDERLINE_PRINTED=$(echo -en "$UNDERLINE") UNDERLINE_LENGTH=${#UNDERLINE_PRINTED} NOMULTI=0 MULTI=1 NOQUOTES=0 QUOTES=1 REALLYNOQUOTES=-1 CURLERRORSTRINGS="(^40.:|error|status.*40.)" # Generate sed script to add colours to certain words UUIDCHAR="[0-9a-f]" UUID_REGEXP=${UUIDCHAR}\{8\}-${UUIDCHAR}\{4\}-${UUIDCHAR}\{4\}-${UUIDCHAR}\{4\}-${UUIDCHAR}\{12\} WORDCOLOURS=( "${GREEN}" "${RED}" "${YELLOW}" ) UUIDCOL="$PURPLE" COLOURED_WORDS=( "true\|up\|enabled\|complete\|finished\|started\|opened" "false\|down\|paused\|uninitialized\|disabled\|failed\|stopped\|closed" "migrating\|powering_up\|project_closed\|unknown" ) ADD_COLOURS="" for colidx in ${!WORDCOLOURS[@]}; do col=${WORDCOLOURS[$colidx]} ADD_COLOURS="${ADD_COLOURS}s/\b\(${COLOURED_WORDS[$colidx]}\)\b/\\${col}\\1\\${PLAIN}/g;" done ADD_COLOURS="${ADD_COLOURS}s/\(${UUID_REGEXP}\)/${UUIDCOL}\\1${PLAIN}/g" nlocs=0 neps=0 # addendpoint ep_name ep_apiendpoint ep_jqobjectname idfieldname [defaultfieldname] addendpoint projects projects project project_id name addepalias projects p addeptitles projects "Project " "UUID" "Status " addepfields projects ".name" ".project_id" ".status" addepactions projects open close addendpoint nodes projects/_CURPROJECT_/nodes node node_id name addepalias nodes n addeptitles nodes "Node " "Model_UUID" "Status " "Console_Port" addepfields nodes ".name" ".template_id" ".status" ".console" addepactions nodes start stop connect add del mv addendpoint links projects/_CURPROJECT_/links link link_id link_id addepalias links l addeptitles links "Type" "A-Host_UUID " "A-Port" "Z-Host_UUID " "Z-Port" "ID _" addepfields links ".link_type" ".nodes[0].node_id" ".nodes[0].label.text" ".nodes[1].node_id" ".nodes[1].label.text" ".link_id" addepactions links add del addendpoint models templates model template_id name addepalias models m addeptitles models "Name" "Category" "UUID" addepfields models ".name" ".category" ".template_id" addendpoint appliances appliances appliance template_id name addepalias appliances a addeptitles appliances "Name" "Category" "Ports" addepfields appliances ".name" ".category" ".qemu.adapters" addcmd list "List elements of a given type (eg. nodes, links, etc)" 1+ ls l addcmdusage list "object_type [regexp_filter]" "object_type can be: [${ep_name[*]}]" addcmdoption list "-n" "Don't resolve UUIDs to names" addcmdoption list "-o filename" "Send output to 'filename'" addcmdoption list "-r" "Raw mode - show raw JSON from gns API" addcmdoption list "-v" "Verbose mode - show object details" addcmd show "Show detail of a given element (eg. compute node, VM, etc)" 2 sh addcmdusage show "object_type [regexp_filter]" "object_type can be: [${ep_jqobj[*]}]" addcmdoption show "-n" "Don't resolve UUIDs to names" addcmdoption show "-o filename" "Send output to 'filename'" addcmdoption show "-r" "Raw mode - show raw JSON from gns API" addcmd net "Show network diagram (requires perl Graph::Easy)" addcmd action "Perform action on given object" 3 act do declare -a lines for x in ${!ep_jqobj[@]}; do if [[ -z ${ep_validactions[$x]} ]]; then lines+=("${ep_jqobj[$x]} actions: n/a") else lines+=("${ep_jqobj[$x]} actions: ${ep_validactions[$x]}") fi done addcmdusage action "object_type action_type regexp_filter [extrainfo]" "object_type can be: [${ep_jqobj[*]}]" "" "${lines[@]}" addcmdoption action "-f" "Force - don't ask for confirmation" addcmdalias "open" "action project open" "_CURPROJECT_" addcmdalias "close" "action project close" "_CURPROJECT_" addcmdalias "start" "action node start" "" addcmdalias "stop" "action node stop" "" addcmdalias "connect" "action node connect" "" addcmdalias "c" "action node connect" "" addcmdalias "add" "action node add" "" addcmdalias "rm" "action node del" "" addcmdalias "del" "action node del" "" addcmdalias "mv" "action node mv" "" addcmdalias "link" "action link add" "" addcmdalias "unlink" "action link del" "" addcmdalias "delink" "action link del" "" addcmd help "List regular commands" 0 "?" "h" addcmd exit "Exit from gnscli" 0 quit addcmd -a help "List admin commands" 0 "?" "h" addcmd -a la "List command aliases" 0 addcmd -a p "Select project filter (comma or space separated list)" 1 loc addcmdusage p "project_list" "project_list is a comma-separated list made up of [${loc_name[*]}]" addcmd -a lp "List GNS3 projects on current server(s)" 0 listprojects addcmd -a ls "List GNS3 servers" 0 listservers l addcmd -a c "Re-cache UUIDs for current projects" 0 cache addcmd -a sc "Show cached name for given UUID (or all UUIDs if none provided)." 0-1 showcache addcmdusage sc "[uuid]" "If UUID is not provided, all UUID-to-name mappings are shown." addcmd -a q "Exit from gnscli" 0 VERBOSE=0 GDATE="gdate" CONFDIR="$HOME/.gnscli" HISTFILE="$CONFDIR/history" TMPDIR="$CONFDIR/tmp" SCRIPTDIR="$CONFDIR/scripts" #HEADINGFILE="$CONFDIR/head.tmp" TMPFILE="$TMPDIR/temp.tmp" TOTFILE="$TMPDIR/tot.tmp" #AUTHFILE="$CONFDIR/auth" CACHEDIR="$CONFDIR/uuid_cache" CACHEFILEBASE="$CACHEDIR/mappings" RCFILE="$CONFDIR/rc" SRVFILE="$CONFDIR/servers" UUIDLENGTH=36 CACHING="" PROFILING=0 DEFPROJECT="" curproj="" nmsgq=0 recache_pid="" THISSCRIPT="$0" RCARGS="" if [[ -e $RCFILE ]]; then info "processing $RCFILE" while read -r f ; do RCARGS="$f $ALLARGS" done < <(egrep -v "(^#|^$)" $RCFILE) fi VALIDARGS="g:hiIp:Pv" while getopts "$VALIDARGS" i $RCARGS; do processarg "$i" done OPTIND=0 while getopts "$VALIDARGS" i ; do processarg "$i" done shift $((OPTIND - 1)) if [[ ! -e $TMPDIR ]]; then error "Temporary file dir $TMPDIR doesn't exist. Use -i to create it." exit 1 else rm -fr ${TMPDIR}/*.tmp rm -fr ${TMPDIR}/get.* rm -fr ${TMPDIR}/run.* fi if [[ ! -d $CONFDIR ]]; then error "Configuration directory $CONFDIR doesn't exist. Use -i to create it." exit 1 fi checkfor gdate coreutils || exit 1 checkfor jq || exit 1 checkfor gsed || exit 1 loadservers if [[ $nlocs -lt 1 ]]; then error "No servers found - check $SRVFILE" exit 1 fi alllocs=$(getalllocs) # prepopulate list of projects notify "Getting initial projectlist" oldverbose=$VERBOSE VERBOSE=1 errfile="$TMPDIR"/err output=$(getdata projects list -e -r 2>"$errfile") rv=$? debug "rv is $rv" VERBOSE=$oldverbose if [[ $rv -eq 0 ]]; then ok cat "$errfile" rm -f "$errfile" jqoutput=$(echo "$output" | jq -r '.[] | .project_id + "|" + .name' 2>"$errfile") rv=$? if [[ $rv -ne 0 || -z $jqoutput ]]; then error "$rv Got bad data from initial project list query to API" echo -e "${RED}${BOLD}Errors:${PLAIN}" echo -en "${RED}" cat "${errfile}" | sed -e 's/^/ /' echo -en "${PLAIN}" rm -f "$errfile" exit 1 fi rm -f "$errfile" for x in $jqoutput; do id=${x%|*} name=${x#*|} addproject $id $name done else fail error "Could not obtain initial project list from API" echo -e "${RED}${BOLD}Output:${PLAIN}" echo -en "${RED}" echo "${output}" | sed -e 's/^/ /' echo -en "${PLAIN}" echo -e "${RED}${BOLD}Errors:${PLAIN}" echo -en "${RED}" cat "${errfile}" | sed -e 's/^/ /' echo -en "${PLAIN}" rm -f "$errfile" exit 1 fi if [[ ${#proj_id[@]} -eq 0 ]]; then error "No projects found on server!!" exit 1 fi setproject $DEFPROJECT || exit 1 # defaults to first one [[ -e $HISTFILE ]] && history -r "$HISTFILE" loadcachefile $curlocs dumpmsgq trap cleanup EXIT echo "Unofficial GNS3 cli v${VER}" echo -e "${ITALIC}${YELLOW}Note: this script is still in development and may have bugs!${PLAIN}" if [[ $# -gt 0 ]]; then processcmd $* else DONE=0 while [[ $DONE -ne 1 ]]; do lastqsecs=0 #echo -en "${pstr}" getcmd cmd || break set -f processcmd $cmd set +f if [[ ! -z $cmd ]]; then history -s "$cmd" history -w "$HISTFILE" fi dumpmsgq # show any queued messages done fi exit 0 #START_INLINE:model.jq "UUID#" + .template_id, "Name#" + .name, "Usage#" + .usage, "Category#" + .category, "RAM#" + .ram + "MB", "Action on close#" + .on_close, "Firewall Type#" + .firewall_type, "Image#" + .hda_disk_image, "__END__" #END_INLINE:model.jq #START_INLINE:project.jq "UUID#" + .project_id, "Name#" + .name, "Status#" + .status, "Filename#" + .filename, "Path#" + .path, "Auto-Open#" + .auto_open, "Auto-Close#" + .auto_close, "Auto-Start#" + .auto_start, "__END__" #END_INLINE:project.jq #START_INLINE:detail.awk BEGIN { FS="#" nlines=0 BOLDBLUE="\033[1m\033[36m" PLAIN="\033[0m" } { key[nlines]=$1 val[nlines++]=$2 if (length($1)>max) max=length($1); } END { nvalid=0 for (i=0;i= 1) { printf( BOLDBLUE"%" max "s:" PLAIN " %s\n",key[i],val[i]); nvalid++; } } if (nvalid >= 1) printf(" \n"); } #END_INLINE:detail.awk #START_INLINE:node.jq "Hostname#" + .name, "UUID#" + .node_id, "Model UUID#" + .template_id, "Project UUID#" + .project_id, "Status#" + .status, "Port format#" + .port_name_format, "Locked#" + (.locked // "n/a"), "Console port#" + ((.console|tostring) // "n/a"), "Port count#" + try (.ports[] | length) catch ("unknown"), #END_INLINE:node.jq #START_INLINE:link.jq "UUID#" + .link_id, "Type#" + .link_type, "Project UUID#" + .project_uuid, "A-Node#" + .nodes[0].node_id, "A-Port#" + .nodes[0].label.text, "Z-Node#" + .nodes[1].node_id, "Z-Port#" + .nodes[1].label.text, "Capturing#" + (.capturing|tostring), "Suspended#" + (.suspend|tostring), "__END__" #END_INLINE:link.jq #START_INLINE:diagram.jq .[] | .nodes[0].node_id + "," + .nodes[0].label.text + "," + .nodes[1].node_id + "," + .nodes[1].label.text #END_INLINE:diagram.jq