github.com/imran-kn/cilium-fork@v1.6.9/contrib/release/lib/common.sh (about)

     1  #!/bin/bash
     2  #
     3  # Copyright 2016 The Kubernetes Authors All rights reserved.
     4  #
     5  # Licensed under the Apache License, Version 2.0 (the "License");
     6  # you may not use this file except in compliance with the License.
     7  # You may obtain a copy of the License at
     8  #
     9  #     http://www.apache.org/licenses/LICENSE-2.0
    10  #
    11  # Unless required by applicable law or agreed to in writing, software
    12  # distributed under the License is distributed on an "AS IS" BASIS,
    13  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14  # See the License for the specific language governing permissions and
    15  # limitations under the License.
    16  #
    17  # Provide a default $PROG (for use in most functions that use a $PROG: prefix
    18  : ${PROG:="common"}
    19  export PROG
    20  
    21  ##############################################################################
    22  # Common library of useful functions and GLOBALS.
    23  ##############################################################################
    24  
    25  set -o errtrace
    26  
    27  # TODO:
    28  # - Figure out a way to share common bits with other Kubernetes sub repos
    29  # - cleanup / function headers
    30  
    31  ##############################################################################
    32  # COMMON CONSTANTS
    33  #
    34  TOOL_LIB_PATH=${TOOL_LIB_PATH:-$(dirname $(readlink -ne $BASH_SOURCE))}
    35  TOOL_ROOT=${TOOL_ROOT:-$(readlink -ne $TOOL_LIB_PATH/..)}
    36  PATH=$TOOL_ROOT:$PATH
    37  LOCAL_CACHE="/tmp/buildresults-cache.$$"
    38  # Provide a default EDITOR for those that don't have this set
    39  : ${EDITOR:="vi"}
    40  export PATH TOOL_ROOT TOOL_LIB_PATH EDITOR
    41  
    42  # Pretty curses stuff for terminals
    43  if [[ -t 1 ]]; then
    44    # Set some video text attributes for use in error/warning msgs.
    45    declare -A TPUT=([BOLD]=$(tput bold 2>/dev/null))
    46    TPUT+=(
    47    [REVERSE]=$(tput rev 2>/dev/null)
    48    [UNDERLINE]=$(tput smul 2>/dev/null)
    49    [BLINK]=$(tput blink 2>/dev/null)
    50    [GREEN]=${TPUT[BOLD]}$(tput setaf 2 2>/dev/null)
    51    [RED]=${TPUT[BOLD]}$(tput setaf 1 2>/dev/null)
    52    [YELLOW]=${TPUT[BOLD]}$(tput setaf 3 2>/dev/null)
    53    [OFF]=$(tput sgr0 2>/dev/null)
    54    [COLS]=$(tput cols 2>/dev/null)
    55    )
    56  
    57    # HR
    58    HR="$(for ((i=1;i<=${TPUT[COLS]};i++)); do echo -en '\u2500'; done)"
    59  
    60    # Save original TTY State
    61    TTY_SAVED_STATE="$(stty -g)"
    62  else
    63    HR="$(for ((i=1;i<=80;i++)); do echo -en '='; done)"
    64  fi
    65  
    66  # Set some usable highlighted keywords for functions like logrun -s
    67  YES="${TPUT[GREEN]}YES${TPUT[OFF]}"
    68  OK="${TPUT[GREEN]}OK${TPUT[OFF]}"
    69  DONE="${TPUT[GREEN]}DONE${TPUT[OFF]}"
    70  PASSED="${TPUT[GREEN]}PASSED${TPUT[OFF]}"
    71  FAILED="${TPUT[RED]}FAILED${TPUT[OFF]}"
    72  FATAL="${TPUT[RED]}FATAL${TPUT[OFF]}"
    73  NO="${TPUT[RED]}NO${TPUT[OFF]}"
    74  WARNING="${TPUT[YELLOW]}WARNING${TPUT[OFF]}"
    75  ATTENTION="${TPUT[YELLOW]}ATTENTION${TPUT[OFF]}"
    76  MOCK="${TPUT[YELLOW]}MOCK${TPUT[OFF]}"
    77  FOUND="${TPUT[GREEN]}FOUND${TPUT[OFF]}"
    78  NOTFOUND="${TPUT[YELLOW]}NOT FOUND${TPUT[OFF]}"
    79  
    80  # Ensure USER is set
    81  USER=${USER:-$LOGNAME}
    82  
    83  # Set a PID for use throughout.
    84  export PID=$$
    85  
    86  # Save original cmd-line.
    87  ORIG_CMDLINE="$*"
    88  
    89  PROGSTATE=/tmp/$PROG-runstate
    90  
    91  ###############################################################################
    92  # Define logecho() function to display to both log and stdout.
    93  # As this is widely used and to reduce clutter, we forgo the common:: prefix
    94  # Options can be -n or -p or -np/-pn.
    95  # @optparam -p Add $PROG: prefix to stdout
    96  # @optparam -r Exclude log prefix (used to output status' like $OK $FAILED)
    97  # @optparam -n no newline (just like echo -n)
    98  # @param a string to echo to stdout
    99  logecho () {
   100    local log_prefix="$PROG::${FUNCNAME[1]:-"main"}(): "
   101    local prefix
   102    # Dynamically set fmtlen
   103    local fmtlen=$((${TPUT[COLS]:-"80"}))
   104    local n
   105    local raw=0
   106    #local -a sed_pat=()
   107  
   108    while [[ "$#" -gt 0 ]]; do
   109      case "$1" in
   110        -r) raw=1; shift ;;
   111        -n) n="-n"; shift ;;
   112        -p) prefix="$PROG: "; ((fmtlen+=${#prefix})); shift ;;
   113         *) break ;;
   114      esac
   115    done
   116  
   117    if ((raw)) || [[ -z "$*" ]]; then
   118      # Clean log_prefix for blank lines
   119      log_prefix=""
   120    #else
   121      # Increase fmtlen to account for control characters
   122      #((fmtlen+=$(echo "$*" grep -o '[[:cntrl:]]' |wc -l)))
   123      #sed_pat=(-e '2,${s}/^/ ... /g')
   124    fi
   125  
   126    # Allow widespread use of logecho without having to
   127    # determine if $LOGFILE exists first.
   128    [[ -f $LOGFILE ]] || LOGFILE="/dev/null"
   129    (
   130    # If -n is set, do not provide autoformatting or you lose the -n effect
   131    # Use of -n should only be used on short status lines anyway.
   132    if ((raw)) || [[ $n == "-n" ]]; then
   133      echo -e $n "$log_prefix$*"
   134    else
   135      # Add FUNCNAME to line prefix, but strip it from visible output
   136      # Useful for viewing log detail
   137      echo -e "$*" | fmt -$fmtlen | sed -e "1s,^,$log_prefix,g" "${sed_pat[@]}"
   138    fi
   139    ) | tee -a "$LOGFILE" |sed "s,^$log_prefix,$prefix,g"
   140  }
   141  
   142  ###############################################################################
   143  # logrun() function to run commands to both log and stdout.
   144  # As this is widely used and to reduce clutter, we forgo the common:: prefix
   145  #
   146  # The calling function is added to the line prefix.
   147  # NOTE: All optparam's for logrun() (obviously) must preceed the command string
   148  # @optparam -v Run verbosely
   149  # @optparam -s Provide a $OK or $FAILED status from running command
   150  # @optparam -m MOCK command by printing out command line rather than running it.
   151  # @optparam -r Retry attempts. Integer arg follows -r (Ex. -r 2)
   152  #              Typically used together with -v to show retry attempts.
   153  # @param a command string
   154  # GLOBALS used in this function:
   155  # * LOGFILE (Set by common::logfileinit()), if set, gets full command output
   156  # * FLAGS_verbose (Set by caller - defaults to false), if true, full output to stdout
   157  logrun () {
   158    local mock=0
   159    local status=0
   160    local arg
   161    local retries=0
   162    local try
   163    local retry_string
   164    local scope="::${FUNCNAME[1]:-main}()"
   165    local ret
   166    local verbose=0
   167  
   168    while [[ "$#" -gt 0 ]]; do
   169      case "$1" in
   170        -v) verbose=1; shift ;;
   171        -s) status=1; shift ;;
   172        -m) mock=1; shift ;;
   173        -r) retries=$2; shift 2;;
   174         *) break ;;
   175      esac
   176    done
   177  
   178    for ((try=0; try<=$retries; try++)); do
   179      if [[ $try -gt 0 ]]; then
   180        if ((verbose)) || ((FLAGS_verbose)); then
   181          # if global FLAGS_verbose, be very verbose
   182          logecho "Retry #$try..."
   183        elif ((status)); then
   184          # if we're reporting a status (-v), then just ...
   185          logecho -n "."
   186        fi
   187        # Add some minimal wait between retries assuming we're retrying due to
   188        # something resolvable by waiting 'just a bit'
   189        sleep 2
   190      fi
   191  
   192      # if no args, take stdin
   193      if (($#==0)); then
   194        if ((verbose)) || ((FLAGS_verbose)); then
   195          tee -a $LOGFILE
   196        else
   197          tee -a $LOGFILE &>/dev/null
   198        fi
   199        ret=$?
   200      elif [[ -f "$LOGFILE" ]]; then
   201        printf "\n$PROG$scope: %s\n" "$*" >> $LOGFILE
   202  
   203        if ((mock)); then
   204          logecho "($MOCK)"
   205          logecho "(CMD): $@"
   206          return 0
   207        fi
   208  
   209        # Special case "cd" which cannot be run through a pipe (subshell)
   210        if (! ((FLAGS_verbose)) && ! ((verbose)) ) || [[ "$1" == "cd" ]]; then
   211          "${@:-:}" >> $LOGFILE 2>&1
   212        else
   213          printf "\n$PROG$scope: %s\n" "$*"
   214          "${@:-:}" 2>&1 | tee -a $LOGFILE
   215        fi
   216      ret=${PIPESTATUS[0]}
   217      else
   218        if ((mock)); then
   219          logecho "($MOCK)"
   220          logecho "(CMD): $@"
   221          return 0
   222        fi
   223  
   224        if ((verbose)) || ((FLAGS_verbose)); then
   225          printf "\n$PROG$scope: %s\n" "$*"
   226          "${@:-:}"
   227        else
   228          "${@:-:}" &>/dev/null
   229        fi
   230        ret=${PIPESTATUS[0]}
   231      fi
   232  
   233      [[ "$ret" = 0 ]] && break
   234    done
   235  
   236    [[ -n "$retries" && $try > 0 ]] && retry_string=" (retry #$try)"
   237  
   238    if ((status)); then
   239      [[ "$ret" = 0 ]] && logecho -r "$OK$retry_string"
   240      [[ "$ret" != 0 ]] && logecho -r "$FAILED"
   241    fi
   242  
   243    return $ret
   244  }
   245  
   246  ###############################################################################
   247  # common::timestamp() Capture block timings and display them
   248  # The calling function is added to the line prefix.
   249  # NOTE: All optparam's for logrun() (obviously) must preceed the command string
   250  # @param begin|end|done
   251  # @optparam section defaults to main, but can be specified to time sub sections
   252  common::timestamp () {
   253    local action=$1
   254    local section=${2:-main}
   255    # convert illegal characters to (legal) underscore
   256    section=${section//[-\.:\/]/_}
   257    local start_var="${section}start_seconds"
   258    local end_var="${section}end_seconds"
   259    local elapsed
   260    local d
   261    local h
   262    local m
   263    local s
   264    local prettyd
   265    local prettyh
   266    local prettym
   267    local prettys
   268    local pretty
   269  
   270    case $action in
   271    begin)
   272  
   273      # Get time(date) for display and calc.
   274      eval $start_var=$(date '+%s')
   275  
   276      # Print BEGIN message for $PROG.
   277      echo "$PROG: BEGIN $section on ${HOSTNAME%%.*} $(date)"
   278  
   279      if [[ $section == "main" ]]; then
   280        echo
   281      fi
   282      ;;
   283    end|done)
   284      # Check for "START" values before calcing.
   285      if [[ -z ${!start_var} ]]; then
   286        #display_time="EE:EE:EE - 'end' run without 'begin' in this scope or sourced script using common::timestamp"
   287        return 1
   288      fi
   289  
   290      # Get time(date) for display and calc.
   291      eval $end_var=$(date '+%s')
   292  
   293      elapsed=$(( ${!end_var} - ${!start_var} ))
   294      d=$(( elapsed / 86400 ))
   295      h=$(( (elapsed % 86400) / 3600 ))
   296      m=$(( (elapsed % 3600) / 60 ))
   297      s=$(( elapsed % 60 ))
   298      (($d>0)) && local prettyd="${d}d"
   299      (($h>0)) && local prettyh="${h}h"
   300      (($m>0)) && local prettym="${m}m"
   301      prettys="${s}s"
   302      pretty="$prettyd$prettyh$prettym$prettys"
   303  
   304      [[ $section == "main" ]] && echo
   305      echo "$PROG: DONE $section on ${HOSTNAME%%.*} $(date) in $pretty"
   306      ;;
   307    esac
   308  }
   309  
   310  # Write our own trap to capture signal
   311  common::trap () {
   312    local func="$1"
   313    shift
   314    local sig
   315  
   316    for sig; do
   317      trap "$func $sig" "$sig"
   318    done
   319  }
   320  
   321  common::trapclean () {
   322    local sig=$1
   323    local frame=0
   324  
   325    # If user ^C's at read then tty is hosed, so make it sane again.
   326    [[ -n "$TTY_SAVED_STATE" ]] && stty "$TTY_SAVED_STATE"
   327  
   328    logecho;logecho
   329    logecho "Signal $sig caught!"
   330    logecho
   331    logecho "Traceback (line function script):"
   332    while caller $frame; do
   333      ((frame++))
   334    done
   335    common::exit 2 "Exiting..."
   336  }
   337  
   338  #############################################################################
   339  # Clean exit with an ending timestamp
   340  # @param Exit code
   341  common::cleanexit () {
   342    # Display end common::timestamp when an existing common::timestamp begin
   343    # was run.
   344    [[ -n ${mainstart_seconds} ]] && common::timestamp end
   345    exit ${1:-0}
   346  }
   347  
   348  #############################################################################
   349  # common::cleanexit() entry point with some formatting and message printing
   350  # @param Exit code
   351  # @param message
   352  common::exit () {
   353    local etype=${1:-0}
   354    shift
   355  
   356    [[ -n "$1" ]] && (logecho;logecho "$@";logecho)
   357    common::cleanexit $etype
   358  }
   359  
   360  #############################################################################
   361  # Simple yes/no prompt
   362  #
   363  # @optparam default -n(default)/-y/-e (default to n, y or make (e)xplicit)
   364  # @param message
   365  common::askyorn () {
   366    local yorn
   367    local def=n
   368    local msg="y/N"
   369  
   370    case $1 in
   371    -y) # yes default
   372        def="y" msg="Y/n"
   373        shift
   374        ;;
   375    -e) # Explicit
   376        def="" msg="y/n"
   377        shift
   378        ;;
   379    -n) shift
   380        ;;
   381    esac
   382  
   383    while [[ $yorn != [yYnN] ]]; do
   384      logecho -n "$*? ($msg): "
   385      read yorn
   386      : ${yorn:=$def}
   387    done
   388  
   389    # Final test to set return code
   390    [[ $yorn == [yY] ]]
   391  }
   392  
   393  ###############################################################################
   394  # Print step header text in a consistent way
   395  common::stepheader () {
   396    # If called with no args, assume the key is the caller's function name
   397    local msg="$*"
   398  
   399    logecho
   400    logecho -r $HR
   401    logecho "$msg"
   402    logecho -r $HR
   403    logecho
   404  }
   405  
   406  # Save a specified number of backups to a file
   407  common::rotatelog () {
   408    local file=$1
   409    local num=$2
   410    local tmpfile=/tmp/rotatelog.$PID
   411    local counter=$num
   412  
   413    # Quiet exit
   414    [[ ! -f "$file" ]] && return
   415  
   416    cp -p $file $tmpfile
   417  
   418    while ((counter>=0)); do
   419      if ((counter==num)); then
   420        rm -f $file.$counter
   421      elif ((counter==0)); then
   422        if [[ -f "$file" ]]; then
   423          next=$((counter+1))
   424          mv $file $file.$next
   425        fi
   426      else
   427        next=$((counter+1))
   428        [[ -f $file.$counter ]] && mv $file.$counter $file.$next
   429      fi
   430      ((counter==0)) && break
   431      ((counter--))
   432    done
   433  
   434    mv $tmpfile $file
   435  }
   436  
   437  # --norotate assumes you're passing in a unique LOGFILE.
   438  # $2 then indicates the number of unique filenames prefixed up to the last
   439  # dot extension that will be saved.  The rest of those files will be deleted
   440  # For example, common::logfileinit --norotate foo.log.234 100
   441  # common::logfileinit maintains up to 100 foo.log.* files.  Anything else named
   442  # foo.log.* > 100 are removed.
   443  common::logfileinit () {
   444    local nr=false
   445  
   446    if [[ "$1" == "--norotate" ]]; then
   447      local nr=true
   448      shift
   449    fi
   450    LOGFILE=${1:-$PWD/$PROG.log}
   451    local num=$2
   452  
   453    # Ensure LOG directory exists
   454    mkdir -p $(dirname $LOGFILE 2>&-)
   455  
   456    # Initialize Logfile.
   457    if ! $nr; then
   458      common::rotatelog "$LOGFILE" ${num:-3}
   459    fi
   460    # Truncate the logfile.
   461    > "$LOGFILE"
   462  
   463    echo "CMD: $PROG $ORIG_CMDLINE" >> "$LOGFILE"
   464  
   465    # with --norotate, remove the list of files that start with $PROG.log
   466    if $nr; then
   467      ls -1tr ${LOGFILE%.*}.* |head --lines=-$num |xargs rm -f
   468    fi
   469  }
   470  
   471  # An alternative that has a dependency on external program - pandoc
   472  # store markdown man pages in companion files.  Allow prog -man to still read
   473  # those and display a man page using:
   474  # pandoc -s -f markdown -t man prog.md |man -l -
   475  common::manpage () {
   476    [[ "$usage" == "yes" ]] && set -- -usage
   477    [[ "$man" == "yes" ]] && set -- -man
   478    [[ "$comments" == "yes" ]] && set -- -comments
   479  
   480    case $1 in
   481    -*usage|"-?")
   482      sed -n '/#+ SYNOPSIS/,/^#+ DESCRIPTION/p' $0 |sed '/^#+ DESCRIPTION/d' |\
   483       envsubst | sed -e 's,^#+ ,,g' -e 's,^#+$,,g'
   484      exit 1
   485      ;;
   486    -*man|-h|-*help)
   487      grep "^#+" "$0" |\
   488       sed -e 's,^#+ ,,g' -e 's,^#+$,,g' |envsubst |${PAGER:-"less"}
   489      exit 1
   490      ;;
   491    esac
   492  }
   493  
   494  ###############################################################################
   495  # General command-line parser converting -*arg="value" to $FLAGS_arg="value"
   496  # Set -name/--name booleans to FLAGS_name=1
   497  # As a convenience, flags can contain dashes or underscores, but dashes are
   498  # converted to underscores in the final FLAGS_name to conform to variable
   499  # naming standards.
   500  # Sets global array POSITIONAL_ARGV holding all non-dash command-line arguments
   501  common::namevalue () {
   502    local arg
   503    local name
   504    local value
   505    local -A arg_aliases=([v]="verbose" [n]="dryrun")
   506  
   507    for arg in "$@"; do
   508      case $arg in
   509        -*[[:alnum:]]*) # Strip off any leading - or --
   510            arg=$(printf "%s\n" $arg |sed 's/^-\{1,2\}//')
   511            # Handle global aliases
   512            arg=${arg_aliases[$arg]:-"$arg"}
   513            if [[ $arg =~ =(.*) ]]; then
   514              name=${arg%%=*}
   515              value=${arg#*=}
   516              # change -'s to _ in name for legal vars in bash
   517              eval export FLAGS_${name//-/_}=\""$value"\"
   518            else
   519              # bool=1
   520              # change -'s to _ in name for legal vars in bash
   521              eval export FLAGS_${arg//-/_}=1
   522            fi
   523            ;;
   524      *) POSITIONAL_ARGV+=("$arg")
   525         ;;
   526      esac
   527    done
   528  }
   529  
   530  ###############################################################################
   531  # Print vars in simple or pretty format with text highlighting, columnized,
   532  # logged.
   533  # Prints the shell-quoted values of all of the given variables.
   534  # Arrays and associative arrays are supported; all their elements will be
   535  # printed.
   536  # @optparam -p Pretty print the values
   537  # @param space separated list of variables
   538  common::printvars () {
   539    local var
   540    local var_str
   541    local key
   542    local tmp
   543    local pprint=0
   544    local pprintvar
   545    local pprintval
   546    local -a quoted
   547  
   548    # Pretty/format print?
   549    if [[ "$1" == "-p" ]]; then
   550      pprint=1
   551      pprintvar=$2
   552      shift 2
   553    fi
   554  
   555    for var in "$@"; do
   556      (($pprint)) && var_str=$var
   557  
   558      # if var is an array, do special tricks
   559      # bash wizardry courtesy of
   560      # http://stackoverflow.com/questions/4582137/bash-indirect-array-addressing
   561      if [[ "$(declare -p $var 2>/dev/null)" =~ ^declare\ -[aA] ]]; then
   562        tmp="$var[@]"
   563        quoted=("${!tmp}") # copy the variable
   564        for key in "${!quoted[@]}"; do
   565          # shell-quote each element
   566          quoted[$key]="$(printf %q "${quoted[$key]}")"
   567        done
   568        if (($pprint)); then
   569          logecho -r "$(printf '%-32s%s\n' "${var_str}:" "${quoted[*]}")"
   570        else
   571          printf '%s=%s\n' "$var" "${quoted[*]}"
   572        fi
   573      else
   574        if (($pprint)); then
   575          pprintval=$(eval echo \$$pprintvar)
   576          logecho -r \
   577           "$(printf '%-32s%s\n' "${var_str}:" "${!var/$pprintval\//\$$pprintvar/}")"
   578        else
   579          echo "$var=${!var}"
   580        fi
   581      fi
   582    done
   583  }
   584  
   585  
   586  ###############################################################################
   587  # Simple argc validation with a usage return
   588  # @param num - number of POSITIONAL_ARGV that should be on the command-line
   589  # return 1 if any number other than num
   590  common::argc_validate () {
   591    local args=$1
   592  
   593    # Validate number of args
   594    if ((${#POSITIONAL_ARGV[@]}>args)); then
   595      logecho
   596      logecho "Exceeded maximum argument limit of $args!"
   597      logecho
   598      $PROG -?
   599      logecho
   600      common::exit 1
   601    fi
   602  }
   603  
   604  
   605  ###############################################################################
   606  # Get the md5 hash of a file
   607  # @param file - The file
   608  # @print the md5 hash
   609  common::md5 () {
   610    local file=$1
   611  
   612    if which md5 >/dev/null 2>&1; then
   613      md5 -q "$1"
   614    else
   615      md5sum "$file" | awk '{print $1}'
   616    fi
   617  }
   618  
   619  ###############################################################################
   620  # Get the sha1 hash of a file
   621  # @param file - The file
   622  # @param algo - Algorithm 1 (default), 224, 256, 384, 512, 512224, 512256
   623  # @print the sha hash
   624  common::sha () {
   625    local file=$1
   626    local algo=${2:-1}
   627  
   628    which shasum >/dev/null 2>&1 && LANG=C shasum -a$algo $file | awk '{print $1}'
   629  }
   630  
   631  # Check for and source security layer
   632  # This function looks for an additional security layer and activates
   633  # special code paths to allow for enhanced features.
   634  # The pointer to this file is set with FLAGS_security_layer:
   635  # * --security_layer=/path/to/script_to_source
   636  # * $HOME/${PROG}rc (FLAGS_security_layer=/path/to/source)
   637  # SECURITY_LAYER global defaulted here.  Set to 1 in external source
   638  common::security_layer () {
   639    local rcfile=$HOME/.kubernetes-releaserc
   640    SECURITY_LAYER=0
   641  
   642    # Quietly attempt to source the include
   643    source $rcfile >/dev/null 2>&1 || true
   644  
   645    # If not there attempt to set it from env
   646    FLAGS_security_layer=${FLAGS_security_layer:-""}
   647  
   648    if [[ -n $FLAGS_security_layer ]]; then
   649      if [[ -r $FLAGS_security_layer ]]; then
   650        source $FLAGS_security_layer >/dev/null 2>&1
   651      else
   652        logecho "$FATAL! $FLAGS_security_layer is not readable."
   653        return 1
   654      fi
   655    elif [[ "$HOSTNAME" =~ google.com ]]; then
   656      logecho "$FATAL! Googler, this session is incomplete." \
   657              "$PROG is running with missing functionality.  See go/$PROG"
   658      return 1
   659    fi
   660  }
   661  
   662  ###############################################################################
   663  # Check PIP packages
   664  # @param package - A space separated list of PIP packages to verify exist
   665  #
   666  common::check_pip_packages () {
   667    local prereq
   668    local -a missing=()
   669  
   670    # Make sure a bunch of packages are available
   671    logecho -n "Checking required PIP packages: "
   672  
   673    for prereq in $*; do
   674      pip list | fgrep -w $prereq > /dev/null || missing+=($prereq)
   675    done
   676  
   677    if ((${#missing[@]}>0)); then
   678      logecho -r "$FAILED"
   679      logecho "PREREQ: Missing prerequisites: ${missing[@]}" \
   680              "Run the following and try again:"
   681      logecho
   682      for prereq in ${missing[@]}; do
   683        logecho "$ sudo pip install $prereq"
   684      done
   685      return 1
   686    fi
   687    logecho -r "$OK"
   688  }
   689  
   690  
   691  ###############################################################################
   692  # Check packages for a K8s release
   693  # @param package - A space separated list of packages to verify exist
   694  #
   695  common::check_packages () {
   696    local prereq
   697    local packagemgr
   698    local distro
   699    local -a missing=()
   700  
   701    # Make sure a bunch of packages are available
   702    logecho -n "Checking required system packages: "
   703  
   704    distro=$(lsb_release -si)
   705    case $distro in
   706      Fedora)
   707        packagemgr="dnf"
   708        for prereq in $*; do
   709          rpm --quiet -q $prereq 2>/dev/null || missing+=($prereq)
   710        done
   711        ;;
   712      Ubuntu)
   713        packagemgr="apt-get"
   714        for prereq in $*; do
   715          dpkg --get-selections 2>/dev/null | fgrep -qw $prereq || missing+=($prereq)
   716        done
   717        ;;
   718      *)
   719        logecho "Unsupported distribution. Only Fedora and Ubuntu are supported"
   720        return 1
   721        ;;
   722    esac
   723  
   724    if ((${#missing[@]}>0)); then
   725      logecho -r "$FAILED"
   726      logecho "PREREQ: Missing prerequisites: ${missing[@]}" \
   727              "Run the following and try again:"
   728      logecho
   729      for prereq in ${missing[@]}; do
   730        if [[ -n ${PREREQUISITE_INSTRUCTIONS[$prereq]} ]]; then
   731          logecho "# See ${PREREQUISITE_INSTRUCTIONS[$prereq]}"
   732        else
   733          logecho "$ sudo $packagemgr install $prereq"
   734        fi
   735      done
   736      return 1
   737    fi
   738    logecho -r "$OK"
   739  }
   740  
   741  
   742  ###############################################################################
   743  # Check disk space
   744  # @param disk - a path
   745  # @param threshold - int in GB
   746  #
   747  common::disk_space_check () {
   748    local disk=$1
   749    local threshold=$2
   750    local avail=$(df -BG $disk |\
   751                  sed -nr -e "s|^\S+\s+\S+\s+\S+\s+([0-9]+).*$|\1|p")
   752  
   753    logecho -n "Checking for at least $threshold GB on $disk: "
   754  
   755    if ((threshold>avail)); then
   756      logecho -r "$FAILED"
   757      logecho "AVAILABLE SPACE: $avail"
   758      logecho "THRESHOLD: $threshold"
   759      return 1
   760    else
   761      logecho -r "$OK"
   762    fi
   763  }
   764  
   765  ###############################################################################
   766  # Run a function and display time metrics
   767  # @param function - a function name to run and time
   768  common::runstep () {
   769    local function=$1
   770    local finishtime
   771    local retcode
   772  
   773    common::timestamp begin $function &>/dev/null
   774  
   775    $*
   776    retcode=$?
   777  
   778    finishtime=$(common::timestamp end $function | sed 's/.* //')
   779    logecho "${TPUT[BOLD]}>>>>>>>> $PROG::$function() finished in" \
   780            "$finishtime${TPUT[OFF]}"
   781    return $retcode
   782  }
   783  
   784  ###############################################################################
   785  # Absolutify incoming path
   786  #
   787  # @param relative or absolute path
   788  # @print absolute path
   789  common::absolute_path () {
   790    local arg=$1
   791  
   792    [[ -z "$arg" ]] && return 0
   793  
   794    [[ "$arg" =~ ^/ ]] || dir="$PWD/$arg"
   795    logecho $arg
   796  }
   797  
   798  ###############################################################################
   799  # Strip all control characters out of a text file
   800  # Useful for stripping color codes and things from text files after runs
   801  # @param file text file
   802  common::strip_control_characters () {
   803    local file=$1
   804  
   805    sed -ri -e "s/\x1B[\[(]([0-9]{1,2}(;[0-9]{1,2})?)?[m|K|B]//g" \
   806            -e 's/\o015$//g' $file
   807  }
   808  
   809  ###############################################################################
   810  # General log sanitizer
   811  # @param file text file
   812  common::sanitize_log () {
   813    local file=$1
   814  
   815    sed -i 's/[a-f0-9]\{40\}:x-oauth-basic/__SANITIZED__:x-oauth-basic/g' $file
   816  }
   817  
   818  ###############################################################################
   819  # Print a number of characters (with no newline)
   820  # @param char single character
   821  # @param num number to print
   822  common::print_n_char () {
   823    local char=$1
   824    local num=$2
   825    local sep
   826  
   827    printf -v sep '%*s' $num
   828    echo "${sep// /$char}"
   829  }
   830  
   831  ###############################################################################
   832  # Generate a (github) markdown TOC between BEGIN/END tags
   833  # @param file The file to update in place
   834  #
   835  common::mdtoc () {
   836    local file=$1
   837    local indent
   838    local anchor
   839    local heading
   840    local begin_block="<!-- BEGIN MUNGE: GENERATED_TOC -->"
   841    local end_block="<!-- END MUNGE: GENERATED_TOC -->"
   842    local tmpfile=/tmp/$PROG-cm.$$
   843  
   844    declare -A count
   845  
   846    while read level heading; do
   847      indent="$(echo $level |sed -e "s,^#,,g" -e 's,#,  ,g')"
   848      # make a valid anchor
   849      anchor=${heading,,}
   850      anchor=${anchor// /-}
   851      anchor=${anchor//[\.\?\*\,\/\[\]:=\<\>()]/}
   852      # Keep track of dups and identify
   853      if [[ -n ${count[$anchor]} ]]; then
   854        ((count[$anchor]++)) ||true
   855        anchor+="-${count[$anchor]}"
   856      else
   857        # initialize value
   858        count[$anchor]=0
   859      fi
   860      echo "${indent}- [$heading](#$anchor)"
   861    done < <(egrep "^#+ " $file) > $tmpfile
   862  
   863    # Insert new TOC
   864    sed -ri "/^$begin_block/,/^$end_block/{
   865         /^$begin_block/{
   866           n
   867           r $tmpfile
   868         }
   869         /^$end_block/!d
   870         }" $file
   871  
   872    logrun rm -f $tmpfile
   873  }
   874  
   875  ###############################################################################
   876  # Set the global GSUTIL and GCLOUD binaries
   877  # Returns:
   878  #   0 if both GSUTIL and GCLOUD are set to executables
   879  #   1 if both GSUTIL and GCLOUD are not set to executables
   880  common::set_cloud_binaries () {
   881  
   882    logecho -n "Checking/setting cloud tools: "
   883  
   884    for GSUTIL in $(which gsutil) /opt/google/google-cloud-sdk/bin/gsutil; do
   885      [[ -x $GSUTIL ]] && break
   886    done
   887  
   888    # gcloud should be in the same place
   889    GCLOUD=${GSUTIL/gsutil/gcloud}
   890  
   891    if [[ -x "$GSUTIL" && -x "$GCLOUD" ]]; then
   892      logecho -r $OK
   893      return 0
   894    else
   895      logecho -r $FAILED
   896      return 1
   897    fi
   898  }
   899  
   900  ###############################################################################
   901  # sendmail/mailer front end.
   902  # @optparam (flag) -h - Send html formatted
   903  # @param to - To
   904  # @param from - From
   905  # @param reply_to - Reply To
   906  # @param subject - Subject
   907  # @param cc - cc
   908  # @param file - file to send
   909  #
   910  common::sendmail () {
   911    local cc_arg
   912    local html=0
   913  
   914    while [[ "$#" -gt 0 ]]; do
   915      case "$1" in
   916        -h) html=1; shift ;;
   917         *) break ;;
   918      esac
   919    done
   920  
   921    local to="$1"
   922    local from="$2"
   923    local reply_to="$3"
   924    local subject="$4"
   925    local cc="$5"
   926    local file="$6"
   927  
   928    if [[ "$HOSTNAME" =~ google.com ]]; then
   929      logecho "$FAILED! sendmail unavailable at Google."
   930      return 1
   931    fi
   932  
   933    (
   934    cat <<EOF+
   935  To: "$to"
   936  From: "$from"
   937  Subject: "$subject"
   938  Cc: "$cc"
   939  Reply-To: "$reply_to"
   940  EOF+
   941    ((html)) && echo "Content-Type: text/html"
   942    cat $file
   943    ) |/usr/sbin/sendmail -t
   944  }
   945  
   946  # Stubs for security_layer functions
   947  security_layer::auth_check () {
   948    logecho "Skipping $FUNCNAME..."
   949    return 0
   950  }
   951  
   952  # Set a common::trap() to capture ^C's and other unexpected exits and do the
   953  # right thing in common::trapclean().
   954  common::trap common::trapclean ERR SIGINT SIGQUIT SIGTERM SIGHUP
   955  
   956  # parse cmdline
   957  common::namevalue "$@"
   958  
   959  # Run common::manpage to show usage and man pages
   960  common::manpage "$@"