#!/bin/bash # 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 curprojectis 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="" 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() { if [[ $VERBOSE -eq 1 ]]; then echo -e "${PURPLE}${BOLD}${FUNCNAME[1]}(): ${PLAIN}${PURPLE}$*${PLAIN}" 1>&2 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 " -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 runcurlaction() { # location api_endpoint(ovname) obname ovmethod local thisapi thisurl curlres rv #local thisauthdom u p thisauthstr local loc api_endpoint obname ovmethod #local data="" local data="" loc="$1" api_endpoint="$2" obname="$3" ovmethod="$4" thisapi=$(get $loc api) [[ -z $thisapi ]] && echo "cant find api for '$loc'" && return 1 #thisauthdom=$(get $loc authdomain) thisurl="$thisapi/$api_endpoint/$obname/$ovmethod" thisurl=${thisurl/_CURPROJECT_/$curprojid} #u=$(head -1 "$AUTHFILE") #p=$(tail -1 "$AUTHFILE") #thisauthstr="$u@$thisauthdom:$p" #unset u #unset p [[ $VERBOSE -eq 1 ]] && debug "curl -XPOST -sk --header 'Content-type: application/xml' --header 'Accept: application/json' -d \"$data\" $thisurl" curlres=$(curl -XPOST -sk --header 'Content-type: application/xml' --header 'Accept: application/json' -d "$data" $thisurl) rv=$? [[ $rv -ne 0 ]] && error "curl 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";; #migrate) echo "migrate";; *) echo rv=1; ;; esac return $rv } function runaction_loc() { # runaction_loc syd|etc api_endpoint action_name ob_uuid ob_name [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 rv=0 loc="$1" epidx="$2" actionname="$3" ob="$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 ) # 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 '$action'" return 1 fi debug "location: ${loc}" debug "api endpoint: ${api_endpoint}" debug "jq obj: ${jq_obj}" debug "actionname: ${actionname} (gns method: $method)" debug "obname: ${ob}" debug "options: ${opts}" curlres=$(runcurlaction $loc $api_endpoint $ob $method ) if [[ $? -ne 0 ]]; then rv=1 fi debug "curlres is [$curlres]" echo "$curlres" fi return $rv } 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" if [[ $outmode == "raw" ]]; then [[ ! -z $curlres ]] && echo "$curlres" || rv=2 elif [[ $outmode == "verbose" ]]; then #if [[ $locnum -eq 1 ]]; then # # bold+underline the heading, and store in a temp file # echo "$curlres" | jq -r "$jqfilter" 2>/dev/null | tr -d '"' | awk '{ if (NR == 1) { printf "\033[1m\033[4m" $0 "\033[0m\n" } }' >$HEADINGFILE #fi debug "jqfilter is $jqfilter" debug "r1 pre jqfilter is $curlres" r1=$(echo "$curlres" | jq -r "$jqfilter" 2>/dev/null | tr -d '"') debug "r1 post jqfilter is $r1" #if [[ ! -z $regexpfilter ]]; then # [[ $VERBOSE -eq 1 ]] && debug "r1 pre regexpfilter is $r1" # r1=$(echo "$r1" | awk -v filter="$regexpfilter" '(NR==1){ print } (NR !=1 && (match($0,filter))) { print }') #fi [[ ! -z $r1 ]] && echo "$r1" || rv=2 else # namesonly r1=$(echo "$curlres") echo "$r1" >/tmp/out if [[ ! -z $jqfilter ]]; then debug "jqfilter is $jqfilter" debug "r1 pre jqfilter is $r1" r1=$(echo "$r1" | jq -r "$jqfilter" 2>/dev/null) debug "r1 post jqfilter is $r1" fi if [[ ! -z $jqscript ]]; then debug "jqscript is $jqscript" debug "r1 pre jqscript is $r1" r1=$(echo "$r1" | jq -rf "$jqscript") fi # if [[ ! -z $regexpfilter ]]; then # debug "r1 pre regexpfilter is $r1" # r1=$(echo "$r1" | awk -v filter="$regexpfilter" '(NR==1){ print } (NR !=1 && (match($0,filter))) { print }') # fi if [[ ! -z $awkscript ]]; then debug "r1 pre awkscript is $r1" r1=$(echo "$r1" | awk -f "$awkscript") fi 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,$thisgrid,/") r1=$(echo "$r1" | egrep -v "^$" | sed "s/^/$loc,/") else #r1=$(echo "$r1" | egrep -v "^$" | sed "s/^/$(echo -e "$BOLD")\[$thisgrid\] $(echo -e "$PLAIN")/") r1=$(echo "$r1" | egrep -v "^$") fi fi debug "final r1 is [$r1]" [[ -n $r1 ]] && echo "$r1" | sed 's/__END__://' || rv=2 fi #echo "$curlres" | jq -r "$jqfilter" | sed "s/^/$(echo -e "$BOLD")\[$thisgrid\] $(echo -e "$PLAIN")/" 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(.name|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="defaultvalue" [[ $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 uuidcolumn uuidcolumns thisuuid repl line local col colidx profile_start ndatalines="$1" shift 1 msg="$*" # table=$(echo -e "$msg" | column -t -s, | awk -v ndatalines=$ndatalines '{ if (match($0, "DC|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 ) { printf "\033[4m" $0 "\033[0m\n"} else if (!hdgline || index($0,"TOTAL") == 1) { print } if (!hdgline) { pfw=$1; } }') table=$(echo -e "$msg" | column -t -s, | 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 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 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" 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 if [[ $foreground -eq 1 ]]; then # 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) runaction_loc $loc $epidx $actionname $obuuid $ob $extrainfo $opts > "$TMPDIR/run,$loc,$ob" else pids="" while read -r line ; do loc=$(echo "$line" | cut -d, -f1) ob=$(echo "$line" | cut -d, -f2) obuuid=$(echo "$line" | cut -d, -f3) extrainfo=$(echo "$line" | cut -d, -f4) 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} allgoodresults="Server,${objecttype},Job Status" allbadresults="Server,${objecttype},Detail,Reason,Status" 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" else ACTIONRES_MSG[$n]=$(cat ${thisfile}) fi if [[ ${ACTIONRES_MSG[$n]} == *detail*reason* ]]; then goterror=1 elif [[ ${ACTIONRES_MSG[$n]} == *rror* ]]; 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 "^$") 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}" fullres_bad=$(csv_to_table $(($errs + 1)) "$allbadresults") echo "$fullres_bad" 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" 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 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 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 "-n" && usecache=0 || usecache=1 arraycontains opts_a "-e" && errordebug=1 debug "opts is $opts" outfile=$(getarrayopt opts_a o) refilter=$(getarrayopt opts_a f) [[ $refilter == "*" ]] && refilter=".*" [[ -n $refilter ]] && obfilter="select(.name|test(\"^$refilter$\"))" || obfilter="." [[ -n $refilter ]] && [[ $quiet -eq 0 ]] && info "${what} filter: ^$refilter$" # Allow standard globs rather than regexp globs debug "refilter is $refilter" # 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 fi elif [[ $cmd == "show" ]]; then if [[ $outmode == "verbose" ]]; then error "Verbose mode not supported for show command yet" return 1 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 all=$(cat "$TMPDIR"/get.* 2>/dev/null | tr -d : | egrep -v 'DC|Server' | sed '/^[[:space:]]*$/d') 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 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" updated_result="" 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 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 processcmd() { local cmd arg newarg rv newlocs x err admin idx opts pipe gotargs local whattolist actionname="" actionfilter="" local showerror=0 showerroropt="" local epidx 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 whattolist=${arg_array[0]} && unset 'arg_array[0]' [[ ${#arg_array[@]} -ge 1 ]] && opts+=("-f${arg_array[@]}") elif [[ $cmd == "list" ]]; then whattolist=${arg_array[0]} && unset 'arg_array[0]' [[ ${#arg_array[@]} -ge 1 ]] && opts+=("-f${arg_array[@]}") elif [[ $cmd == "action" ]]; then whattolist=${arg_array[0]} && unset 'arg_array[0]' actionname=${arg_array[1]} && unset 'arg_array[1]' if [[ ${#arg_array[@]} -ge 1 ]]; then actionfilter="-f${arg_array[@]}" obname="${arg_array[@]}" else error "Name of $whattolist not provided." return 1 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=$? start_recache_if_needed ;; 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 ;; help) listcommands ADMIN ;; *) error "Admin command '$cmd' not implemented" rv=1 ;; esac else case ${cmd} in list|show) 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 ${whattolist} $actionname if [[ $? -ne 0 ]]; then error "'$actionname' is not a valid action for ${whattolist}s" return 1 fi epidx=$(getepidx $whattolist) if [[ $? -ne 0 ]]; then error "'$whattolist' is not a valid endpoint" return 1 fi # When getting a list of what to operate on, we need # to get the ID as well as the name, and the ID field # depends on the obejct type. #action_idfield=${ep_idfield[$epidx]} # Get a list of objects to operate on # ie. turn regexp into a list of dcs and obnames first getdata ${whattolist} list $actionfilter $showerroropt -s -q >"$TMPFILE" rv=$? if [[ $rv -ne 0 ]]; then error "Query for matching objects failed." rv=1 elif [[ ! -e $TMPFILE ]]; then error "No matching objects found." rv=1 elif grep -q "no results" $TMPFILE; then error "No ${whattolist}s found matching '$obname'." rv=1 elif [[ ! -e $TMPFILE ]]; then error "Results file does not exist." rv=1 else local ndcs nobs data confirm=0 ok debug "tmpfile contents: [$(cat $TMPFILE)]" echo data=$(cat "$TMPFILE" | egrep -v "^$") # how many servers? ndcs=$( printf %d $(echo "$data" | awk -F, '{ print $1 }' | sort -u | wc -l)) [[ $ndcs -eq 1 ]] && dc_ess="" || dc_ess="s" nobs=$( printf %d $(echo "$data" | awk -F, '{ print $2 }' | sort -u | wc -l)) [[ $nobs -eq 1 ]] && ob_ess="" || ob_ess="s" if [[ $actionname == "connect" ]]; then if [[ $nobs -gt 1 ]]; then error "Can't run '$actionname' with multiple objects" return 1 else confirm=1 fi else 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 if [[ $confirm -eq 1 ]]; then actiontargets=$(echo "$data") 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 " whattolist = $whattolist" debug " actionname = $actionname" debug " actiontargets = $actiontargets" debug " opts = ${opts[@]}" debug " outputfile = ${TMPFILE}" runaction ${whattolist} $actionname "$actiontargets" ${opts[@]} >"$TMPFILE" rv=$? if [[ $actionname != "connect" ]]; then ok [[ -e $TMPFILE ]] && cat "$TMPFILE" | $pipe echo info $(printf "action submission time: %s seconds" "$lastqsecs") fi fi fi 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 os ok=0 confirm=0 what=$1 pkg_name=${2:-$what} os=$(uname -s) which $what >/dev/null 2>&1 if [[ $? -eq 0 ]]; then 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 fi ;; *) echo "Don't know how to install stuff on OS '$os'" ;; esac [[ $ok -eq 1 ]] && info "Successfully installed $pkg_name" || error "Failed to install $pkg_name" fi fi which $what >/dev/null 2>&1 return $? } 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 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} # 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 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" addepfields links ".link_type" ".nodes[0].node_id" ".nodes[0].label.text" ".nodes[1].node_id" ".nodes[1].label.text" addendpoint models templates model template_id name addepalias models m addeptitles models "Name" "Category" "UUID" addepfields models ".name" ".category" ".template_id" 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 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" "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" "" addcmd help "List regular commands" 0 "?" "h" addcmd exit "Exit from gnscli" 0 quit addcmd -a help "List admin commands" 0 "?" "h" 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 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="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 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 show -e -r 2>"$errfile") 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, "Console port#" + ((.console|tostring) // "n/a"), "Locked#" + (.locked // "n/a"), "Port format#" + .port_name_format, "Port count#" + try (.ports[] | length) catch ("unknown"), "__END__" #END_INLINE:node.jq #START_INLINE:link.jq "UUID#" + .link_id, "Type#" + .link_type, "Project UUID#" + .project_uuid, "A-Node#" + .node[0].node_id, "A-Port#" + .node[0].label.text, "Z-Node#" + .node[1].node_id, "Z-Port#" + .node[1].label.text, "Capturing#" + .capturing, "Suspended#" + .suspend, "__END__" #END_INLINE:link.jq