Revision 13c64ae3283502f77585175e28daef366bed6483 authored by Ian Vernon on 15 August 2019, 21:54:41 UTC, committed by Ian Vernon on 16 August 2019, 15:28:16 UTC
Signed-off by: Ian Vernon <ian@cilium.io>
1 parent 6b91141
Raw File
common.sh
#!/bin/bash
#
# Copyright 2016 The Kubernetes Authors All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Provide a default $PROG (for use in most functions that use a $PROG: prefix
: ${PROG:="common"}
export PROG

##############################################################################
# Common library of useful functions and GLOBALS.
##############################################################################

set -o errtrace

# TODO:
# - Figure out a way to share common bits with other Kubernetes sub repos
# - cleanup / function headers

##############################################################################
# COMMON CONSTANTS
#
TOOL_LIB_PATH=${TOOL_LIB_PATH:-$(dirname $(readlink -ne $BASH_SOURCE))}
TOOL_ROOT=${TOOL_ROOT:-$(readlink -ne $TOOL_LIB_PATH/..)}
PATH=$TOOL_ROOT:$PATH
LOCAL_CACHE="/tmp/buildresults-cache.$$"
# Provide a default EDITOR for those that don't have this set
: ${EDITOR:="vi"}
export PATH TOOL_ROOT TOOL_LIB_PATH EDITOR

# Pretty curses stuff for terminals
if [[ -t 1 ]]; then
  # Set some video text attributes for use in error/warning msgs.
  declare -A TPUT=([BOLD]=$(tput bold 2>/dev/null))
  TPUT+=(
  [REVERSE]=$(tput rev 2>/dev/null)
  [UNDERLINE]=$(tput smul 2>/dev/null)
  [BLINK]=$(tput blink 2>/dev/null)
  [GREEN]=${TPUT[BOLD]}$(tput setaf 2 2>/dev/null)
  [RED]=${TPUT[BOLD]}$(tput setaf 1 2>/dev/null)
  [YELLOW]=${TPUT[BOLD]}$(tput setaf 3 2>/dev/null)
  [OFF]=$(tput sgr0 2>/dev/null)
  [COLS]=$(tput cols 2>/dev/null)
  )

  # HR
  HR="$(for ((i=1;i<=${TPUT[COLS]};i++)); do echo -en '\u2500'; done)"

  # Save original TTY State
  TTY_SAVED_STATE="$(stty -g)"
else
  HR="$(for ((i=1;i<=80;i++)); do echo -en '='; done)"
fi

# Set some usable highlighted keywords for functions like logrun -s
YES="${TPUT[GREEN]}YES${TPUT[OFF]}"
OK="${TPUT[GREEN]}OK${TPUT[OFF]}"
DONE="${TPUT[GREEN]}DONE${TPUT[OFF]}"
PASSED="${TPUT[GREEN]}PASSED${TPUT[OFF]}"
FAILED="${TPUT[RED]}FAILED${TPUT[OFF]}"
FATAL="${TPUT[RED]}FATAL${TPUT[OFF]}"
NO="${TPUT[RED]}NO${TPUT[OFF]}"
WARNING="${TPUT[YELLOW]}WARNING${TPUT[OFF]}"
ATTENTION="${TPUT[YELLOW]}ATTENTION${TPUT[OFF]}"
MOCK="${TPUT[YELLOW]}MOCK${TPUT[OFF]}"
FOUND="${TPUT[GREEN]}FOUND${TPUT[OFF]}"
NOTFOUND="${TPUT[YELLOW]}NOT FOUND${TPUT[OFF]}"

# Ensure USER is set
USER=${USER:-$LOGNAME}

# Set a PID for use throughout.
export PID=$$

# Save original cmd-line.
ORIG_CMDLINE="$*"

PROGSTATE=/tmp/$PROG-runstate

###############################################################################
# Define logecho() function to display to both log and stdout.
# As this is widely used and to reduce clutter, we forgo the common:: prefix
# Options can be -n or -p or -np/-pn.
# @optparam -p Add $PROG: prefix to stdout
# @optparam -r Exclude log prefix (used to output status' like $OK $FAILED)
# @optparam -n no newline (just like echo -n)
# @param a string to echo to stdout
logecho () {
  local log_prefix="$PROG::${FUNCNAME[1]:-"main"}(): "
  local prefix
  # Dynamically set fmtlen
  local fmtlen=$((${TPUT[COLS]:-"80"}))
  local n
  local raw=0
  #local -a sed_pat=()

  while [[ "$#" -gt 0 ]]; do
    case "$1" in
      -r) raw=1; shift ;;
      -n) n="-n"; shift ;;
      -p) prefix="$PROG: "; ((fmtlen+=${#prefix})); shift ;;
       *) break ;;
    esac
  done

  if ((raw)) || [[ -z "$*" ]]; then
    # Clean log_prefix for blank lines
    log_prefix=""
  #else
    # Increase fmtlen to account for control characters
    #((fmtlen+=$(echo "$*" grep -o '[[:cntrl:]]' |wc -l)))
    #sed_pat=(-e '2,${s}/^/ ... /g')
  fi

  # Allow widespread use of logecho without having to
  # determine if $LOGFILE exists first.
  [[ -f $LOGFILE ]] || LOGFILE="/dev/null"
  (
  # If -n is set, do not provide autoformatting or you lose the -n effect
  # Use of -n should only be used on short status lines anyway.
  if ((raw)) || [[ $n == "-n" ]]; then
    echo -e $n "$log_prefix$*"
  else
    # Add FUNCNAME to line prefix, but strip it from visible output
    # Useful for viewing log detail
    echo -e "$*" | fmt -$fmtlen | sed -e "1s,^,$log_prefix,g" "${sed_pat[@]}"
  fi
  ) | tee -a "$LOGFILE" |sed "s,^$log_prefix,$prefix,g"
}

###############################################################################
# logrun() function to run commands to both log and stdout.
# As this is widely used and to reduce clutter, we forgo the common:: prefix
#
# The calling function is added to the line prefix.
# NOTE: All optparam's for logrun() (obviously) must preceed the command string
# @optparam -v Run verbosely
# @optparam -s Provide a $OK or $FAILED status from running command
# @optparam -m MOCK command by printing out command line rather than running it.
# @optparam -r Retry attempts. Integer arg follows -r (Ex. -r 2)
#              Typically used together with -v to show retry attempts.
# @param a command string
# GLOBALS used in this function:
# * LOGFILE (Set by common::logfileinit()), if set, gets full command output
# * FLAGS_verbose (Set by caller - defaults to false), if true, full output to stdout
logrun () {
  local mock=0
  local status=0
  local arg
  local retries=0
  local try
  local retry_string
  local scope="::${FUNCNAME[1]:-main}()"
  local ret
  local verbose=0

  while [[ "$#" -gt 0 ]]; do
    case "$1" in
      -v) verbose=1; shift ;;
      -s) status=1; shift ;;
      -m) mock=1; shift ;;
      -r) retries=$2; shift 2;;
       *) break ;;
    esac
  done

  for ((try=0; try<=$retries; try++)); do
    if [[ $try -gt 0 ]]; then
      if ((verbose)) || ((FLAGS_verbose)); then
        # if global FLAGS_verbose, be very verbose
        logecho "Retry #$try..."
      elif ((status)); then
        # if we're reporting a status (-v), then just ...
        logecho -n "."
      fi
      # Add some minimal wait between retries assuming we're retrying due to
      # something resolvable by waiting 'just a bit'
      sleep 2
    fi

    # if no args, take stdin
    if (($#==0)); then
      if ((verbose)) || ((FLAGS_verbose)); then
        tee -a $LOGFILE
      else
        tee -a $LOGFILE &>/dev/null
      fi
      ret=$?
    elif [[ -f "$LOGFILE" ]]; then
      printf "\n$PROG$scope: %s\n" "$*" >> $LOGFILE

      if ((mock)); then
        logecho "($MOCK)"
        logecho "(CMD): $@"
        return 0
      fi

      # Special case "cd" which cannot be run through a pipe (subshell)
      if (! ((FLAGS_verbose)) && ! ((verbose)) ) || [[ "$1" == "cd" ]]; then
        "${@:-:}" >> $LOGFILE 2>&1
      else
        printf "\n$PROG$scope: %s\n" "$*"
        "${@:-:}" 2>&1 | tee -a $LOGFILE
      fi
    ret=${PIPESTATUS[0]}
    else
      if ((mock)); then
        logecho "($MOCK)"
        logecho "(CMD): $@"
        return 0
      fi

      if ((verbose)) || ((FLAGS_verbose)); then
        printf "\n$PROG$scope: %s\n" "$*"
        "${@:-:}"
      else
        "${@:-:}" &>/dev/null
      fi
      ret=${PIPESTATUS[0]}
    fi

    [[ "$ret" = 0 ]] && break
  done

  [[ -n "$retries" && $try > 0 ]] && retry_string=" (retry #$try)"

  if ((status)); then
    [[ "$ret" = 0 ]] && logecho -r "$OK$retry_string"
    [[ "$ret" != 0 ]] && logecho -r "$FAILED"
  fi

  return $ret
}

###############################################################################
# common::timestamp() Capture block timings and display them
# The calling function is added to the line prefix.
# NOTE: All optparam's for logrun() (obviously) must preceed the command string
# @param begin|end|done
# @optparam section defaults to main, but can be specified to time sub sections
common::timestamp () {
  local action=$1
  local section=${2:-main}
  # convert illegal characters to (legal) underscore
  section=${section//[-\.:\/]/_}
  local start_var="${section}start_seconds"
  local end_var="${section}end_seconds"
  local elapsed
  local d
  local h
  local m
  local s
  local prettyd
  local prettyh
  local prettym
  local prettys
  local pretty

  case $action in
  begin)

    # Get time(date) for display and calc.
    eval $start_var=$(date '+%s')

    # Print BEGIN message for $PROG.
    echo "$PROG: BEGIN $section on ${HOSTNAME%%.*} $(date)"

    if [[ $section == "main" ]]; then
      echo
    fi
    ;;
  end|done)
    # Check for "START" values before calcing.
    if [[ -z ${!start_var} ]]; then
      #display_time="EE:EE:EE - 'end' run without 'begin' in this scope or sourced script using common::timestamp"
      return 1
    fi

    # Get time(date) for display and calc.
    eval $end_var=$(date '+%s')

    elapsed=$(( ${!end_var} - ${!start_var} ))
    d=$(( elapsed / 86400 ))
    h=$(( (elapsed % 86400) / 3600 ))
    m=$(( (elapsed % 3600) / 60 ))
    s=$(( elapsed % 60 ))
    (($d>0)) && local prettyd="${d}d"
    (($h>0)) && local prettyh="${h}h"
    (($m>0)) && local prettym="${m}m"
    prettys="${s}s"
    pretty="$prettyd$prettyh$prettym$prettys"

    [[ $section == "main" ]] && echo
    echo "$PROG: DONE $section on ${HOSTNAME%%.*} $(date) in $pretty"
    ;;
  esac
}

# Write our own trap to capture signal
common::trap () {
  local func="$1"
  shift
  local sig

  for sig; do
    trap "$func $sig" "$sig"
  done
}

common::trapclean () {
  local sig=$1
  local frame=0

  # If user ^C's at read then tty is hosed, so make it sane again.
  [[ -n "$TTY_SAVED_STATE" ]] && stty "$TTY_SAVED_STATE"

  logecho;logecho
  logecho "Signal $sig caught!"
  logecho
  logecho "Traceback (line function script):"
  while caller $frame; do
    ((frame++))
  done
  common::exit 2 "Exiting..."
}

#############################################################################
# Clean exit with an ending timestamp
# @param Exit code
common::cleanexit () {
  # Display end common::timestamp when an existing common::timestamp begin
  # was run.
  [[ -n ${mainstart_seconds} ]] && common::timestamp end
  exit ${1:-0}
}

#############################################################################
# common::cleanexit() entry point with some formatting and message printing
# @param Exit code
# @param message
common::exit () {
  local etype=${1:-0}
  shift

  [[ -n "$1" ]] && (logecho;logecho "$@";logecho)
  common::cleanexit $etype
}

#############################################################################
# Simple yes/no prompt
#
# @optparam default -n(default)/-y/-e (default to n, y or make (e)xplicit)
# @param message
common::askyorn () {
  local yorn
  local def=n
  local msg="y/N"

  case $1 in
  -y) # yes default
      def="y" msg="Y/n"
      shift
      ;;
  -e) # Explicit
      def="" msg="y/n"
      shift
      ;;
  -n) shift
      ;;
  esac

  while [[ $yorn != [yYnN] ]]; do
    logecho -n "$*? ($msg): "
    read yorn
    : ${yorn:=$def}
  done

  # Final test to set return code
  [[ $yorn == [yY] ]]
}

###############################################################################
# Print step header text in a consistent way
common::stepheader () {
  # If called with no args, assume the key is the caller's function name
  local msg="$*"

  logecho
  logecho -r $HR
  logecho "$msg"
  logecho -r $HR
  logecho
}

# Save a specified number of backups to a file
common::rotatelog () {
  local file=$1
  local num=$2
  local tmpfile=/tmp/rotatelog.$PID
  local counter=$num

  # Quiet exit
  [[ ! -f "$file" ]] && return

  cp -p $file $tmpfile

  while ((counter>=0)); do
    if ((counter==num)); then
      rm -f $file.$counter
    elif ((counter==0)); then
      if [[ -f "$file" ]]; then
        next=$((counter+1))
        mv $file $file.$next
      fi
    else
      next=$((counter+1))
      [[ -f $file.$counter ]] && mv $file.$counter $file.$next
    fi
    ((counter==0)) && break
    ((counter--))
  done

  mv $tmpfile $file
}

# --norotate assumes you're passing in a unique LOGFILE.
# $2 then indicates the number of unique filenames prefixed up to the last
# dot extension that will be saved.  The rest of those files will be deleted
# For example, common::logfileinit --norotate foo.log.234 100
# common::logfileinit maintains up to 100 foo.log.* files.  Anything else named
# foo.log.* > 100 are removed.
common::logfileinit () {
  local nr=false

  if [[ "$1" == "--norotate" ]]; then
    local nr=true
    shift
  fi
  LOGFILE=${1:-$PWD/$PROG.log}
  local num=$2

  # Ensure LOG directory exists
  mkdir -p $(dirname $LOGFILE 2>&-)

  # Initialize Logfile.
  if ! $nr; then
    common::rotatelog "$LOGFILE" ${num:-3}
  fi
  # Truncate the logfile.
  > "$LOGFILE"

  echo "CMD: $PROG $ORIG_CMDLINE" >> "$LOGFILE"

  # with --norotate, remove the list of files that start with $PROG.log
  if $nr; then
    ls -1tr ${LOGFILE%.*}.* |head --lines=-$num |xargs rm -f
  fi
}

# An alternative that has a dependency on external program - pandoc
# store markdown man pages in companion files.  Allow prog -man to still read
# those and display a man page using:
# pandoc -s -f markdown -t man prog.md |man -l -
common::manpage () {
  [[ "$usage" == "yes" ]] && set -- -usage
  [[ "$man" == "yes" ]] && set -- -man
  [[ "$comments" == "yes" ]] && set -- -comments

  case $1 in
  -*usage|"-?")
    sed -n '/#+ SYNOPSIS/,/^#+ DESCRIPTION/p' $0 |sed '/^#+ DESCRIPTION/d' |\
     envsubst | sed -e 's,^#+ ,,g' -e 's,^#+$,,g'
    exit 1
    ;;
  -*man|-h|-*help)
    grep "^#+" "$0" |\
     sed -e 's,^#+ ,,g' -e 's,^#+$,,g' |envsubst |${PAGER:-"less"}
    exit 1
    ;;
  esac
}

###############################################################################
# General command-line parser converting -*arg="value" to $FLAGS_arg="value"
# Set -name/--name booleans to FLAGS_name=1
# As a convenience, flags can contain dashes or underscores, but dashes are
# converted to underscores in the final FLAGS_name to conform to variable
# naming standards.
# Sets global array POSITIONAL_ARGV holding all non-dash command-line arguments
common::namevalue () {
  local arg
  local name
  local value
  local -A arg_aliases=([v]="verbose" [n]="dryrun")

  for arg in "$@"; do
    case $arg in
      -*[[:alnum:]]*) # Strip off any leading - or --
          arg=$(printf "%s\n" $arg |sed 's/^-\{1,2\}//')
          # Handle global aliases
          arg=${arg_aliases[$arg]:-"$arg"}
          if [[ $arg =~ =(.*) ]]; then
            name=${arg%%=*}
            value=${arg#*=}
            # change -'s to _ in name for legal vars in bash
            eval export FLAGS_${name//-/_}=\""$value"\"
          else
            # bool=1
            # change -'s to _ in name for legal vars in bash
            eval export FLAGS_${arg//-/_}=1
          fi
          ;;
    *) POSITIONAL_ARGV+=("$arg")
       ;;
    esac
  done
}

###############################################################################
# Print vars in simple or pretty format with text highlighting, columnized,
# logged.
# Prints the shell-quoted values of all of the given variables.
# Arrays and associative arrays are supported; all their elements will be
# printed.
# @optparam -p Pretty print the values
# @param space separated list of variables
common::printvars () {
  local var
  local var_str
  local key
  local tmp
  local pprint=0
  local pprintvar
  local pprintval
  local -a quoted

  # Pretty/format print?
  if [[ "$1" == "-p" ]]; then
    pprint=1
    pprintvar=$2
    shift 2
  fi

  for var in "$@"; do
    (($pprint)) && var_str=$var

    # if var is an array, do special tricks
    # bash wizardry courtesy of
    # http://stackoverflow.com/questions/4582137/bash-indirect-array-addressing
    if [[ "$(declare -p $var 2>/dev/null)" =~ ^declare\ -[aA] ]]; then
      tmp="$var[@]"
      quoted=("${!tmp}") # copy the variable
      for key in "${!quoted[@]}"; do
        # shell-quote each element
        quoted[$key]="$(printf %q "${quoted[$key]}")"
      done
      if (($pprint)); then
        logecho -r "$(printf '%-32s%s\n' "${var_str}:" "${quoted[*]}")"
      else
        printf '%s=%s\n' "$var" "${quoted[*]}"
      fi
    else
      if (($pprint)); then
        pprintval=$(eval echo \$$pprintvar)
        logecho -r \
         "$(printf '%-32s%s\n' "${var_str}:" "${!var/$pprintval\//\$$pprintvar/}")"
      else
        echo "$var=${!var}"
      fi
    fi
  done
}


###############################################################################
# Simple argc validation with a usage return
# @param num - number of POSITIONAL_ARGV that should be on the command-line
# return 1 if any number other than num
common::argc_validate () {
  local args=$1

  # Validate number of args
  if ((${#POSITIONAL_ARGV[@]}>args)); then
    logecho
    logecho "Exceeded maximum argument limit of $args!"
    logecho
    $PROG -?
    logecho
    common::exit 1
  fi
}


###############################################################################
# Get the md5 hash of a file
# @param file - The file
# @print the md5 hash
common::md5 () {
  local file=$1

  if which md5 >/dev/null 2>&1; then
    md5 -q "$1"
  else
    md5sum "$file" | awk '{print $1}'
  fi
}

###############################################################################
# Get the sha1 hash of a file
# @param file - The file
# @param algo - Algorithm 1 (default), 224, 256, 384, 512, 512224, 512256
# @print the sha hash
common::sha () {
  local file=$1
  local algo=${2:-1}

  which shasum >/dev/null 2>&1 && LANG=C shasum -a$algo $file | awk '{print $1}'
}

# Check for and source security layer
# This function looks for an additional security layer and activates
# special code paths to allow for enhanced features.
# The pointer to this file is set with FLAGS_security_layer:
# * --security_layer=/path/to/script_to_source
# * $HOME/${PROG}rc (FLAGS_security_layer=/path/to/source)
# SECURITY_LAYER global defaulted here.  Set to 1 in external source
common::security_layer () {
  local rcfile=$HOME/.kubernetes-releaserc
  SECURITY_LAYER=0

  # Quietly attempt to source the include
  source $rcfile >/dev/null 2>&1 || true

  # If not there attempt to set it from env
  FLAGS_security_layer=${FLAGS_security_layer:-""}

  if [[ -n $FLAGS_security_layer ]]; then
    if [[ -r $FLAGS_security_layer ]]; then
      source $FLAGS_security_layer >/dev/null 2>&1
    else
      logecho "$FATAL! $FLAGS_security_layer is not readable."
      return 1
    fi
  elif [[ "$HOSTNAME" =~ google.com ]]; then
    logecho "$FATAL! Googler, this session is incomplete." \
            "$PROG is running with missing functionality.  See go/$PROG"
    return 1
  fi
}

###############################################################################
# Check PIP packages
# @param package - A space separated list of PIP packages to verify exist
#
common::check_pip_packages () {
  local prereq
  local -a missing=()

  # Make sure a bunch of packages are available
  logecho -n "Checking required PIP packages: "

  for prereq in $*; do
    pip list | fgrep -w $prereq > /dev/null || missing+=($prereq)
  done

  if ((${#missing[@]}>0)); then
    logecho -r "$FAILED"
    logecho "PREREQ: Missing prerequisites: ${missing[@]}" \
            "Run the following and try again:"
    logecho
    for prereq in ${missing[@]}; do
      logecho "$ sudo pip install $prereq"
    done
    return 1
  fi
  logecho -r "$OK"
}


###############################################################################
# Check packages for a K8s release
# @param package - A space separated list of packages to verify exist
#
common::check_packages () {
  local prereq
  local packagemgr
  local distro
  local -a missing=()

  # Make sure a bunch of packages are available
  logecho -n "Checking required system packages: "

  distro=$(lsb_release -si)
  case $distro in
    Fedora)
      packagemgr="dnf"
      for prereq in $*; do
        rpm --quiet -q $prereq 2>/dev/null || missing+=($prereq)
      done
      ;;
    Ubuntu)
      packagemgr="apt-get"
      for prereq in $*; do
        dpkg --get-selections 2>/dev/null | fgrep -qw $prereq || missing+=($prereq)
      done
      ;;
    *)
      logecho "Unsupported distribution. Only Fedora and Ubuntu are supported"
      return 1
      ;;
  esac

  if ((${#missing[@]}>0)); then
    logecho -r "$FAILED"
    logecho "PREREQ: Missing prerequisites: ${missing[@]}" \
            "Run the following and try again:"
    logecho
    for prereq in ${missing[@]}; do
      if [[ -n ${PREREQUISITE_INSTRUCTIONS[$prereq]} ]]; then
        logecho "# See ${PREREQUISITE_INSTRUCTIONS[$prereq]}"
      else
        logecho "$ sudo $packagemgr install $prereq"
      fi
    done
    return 1
  fi
  logecho -r "$OK"
}


###############################################################################
# Check disk space
# @param disk - a path
# @param threshold - int in GB
#
common::disk_space_check () {
  local disk=$1
  local threshold=$2
  local avail=$(df -BG $disk |\
                sed -nr -e "s|^\S+\s+\S+\s+\S+\s+([0-9]+).*$|\1|p")

  logecho -n "Checking for at least $threshold GB on $disk: "

  if ((threshold>avail)); then
    logecho -r "$FAILED"
    logecho "AVAILABLE SPACE: $avail"
    logecho "THRESHOLD: $threshold"
    return 1
  else
    logecho -r "$OK"
  fi
}

###############################################################################
# Run a function and display time metrics
# @param function - a function name to run and time
common::runstep () {
  local function=$1
  local finishtime
  local retcode

  common::timestamp begin $function &>/dev/null

  $*
  retcode=$?

  finishtime=$(common::timestamp end $function | sed 's/.* //')
  logecho "${TPUT[BOLD]}>>>>>>>> $PROG::$function() finished in" \
          "$finishtime${TPUT[OFF]}"
  return $retcode
}

###############################################################################
# Absolutify incoming path
#
# @param relative or absolute path
# @print absolute path
common::absolute_path () {
  local arg=$1

  [[ -z "$arg" ]] && return 0

  [[ "$arg" =~ ^/ ]] || dir="$PWD/$arg"
  logecho $arg
}

###############################################################################
# Strip all control characters out of a text file
# Useful for stripping color codes and things from text files after runs
# @param file text file
common::strip_control_characters () {
  local file=$1

  sed -ri -e "s/\x1B[\[(]([0-9]{1,2}(;[0-9]{1,2})?)?[m|K|B]//g" \
          -e 's/\o015$//g' $file
}

###############################################################################
# General log sanitizer
# @param file text file
common::sanitize_log () {
  local file=$1

  sed -i 's/[a-f0-9]\{40\}:x-oauth-basic/__SANITIZED__:x-oauth-basic/g' $file
}

###############################################################################
# Print a number of characters (with no newline)
# @param char single character
# @param num number to print
common::print_n_char () {
  local char=$1
  local num=$2
  local sep

  printf -v sep '%*s' $num
  echo "${sep// /$char}"
}

###############################################################################
# Generate a (github) markdown TOC between BEGIN/END tags
# @param file The file to update in place
#
common::mdtoc () {
  local file=$1
  local indent
  local anchor
  local heading
  local begin_block="<!-- BEGIN MUNGE: GENERATED_TOC -->"
  local end_block="<!-- END MUNGE: GENERATED_TOC -->"
  local tmpfile=/tmp/$PROG-cm.$$

  declare -A count

  while read level heading; do
    indent="$(echo $level |sed -e "s,^#,,g" -e 's,#,  ,g')"
    # make a valid anchor
    anchor=${heading,,}
    anchor=${anchor// /-}
    anchor=${anchor//[\.\?\*\,\/\[\]:=\<\>()]/}
    # Keep track of dups and identify
    if [[ -n ${count[$anchor]} ]]; then
      ((count[$anchor]++)) ||true
      anchor+="-${count[$anchor]}"
    else
      # initialize value
      count[$anchor]=0
    fi
    echo "${indent}- [$heading](#$anchor)"
  done < <(egrep "^#+ " $file) > $tmpfile

  # Insert new TOC
  sed -ri "/^$begin_block/,/^$end_block/{
       /^$begin_block/{
         n
         r $tmpfile
       }
       /^$end_block/!d
       }" $file

  logrun rm -f $tmpfile
}

###############################################################################
# Set the global GSUTIL and GCLOUD binaries
# Returns:
#   0 if both GSUTIL and GCLOUD are set to executables
#   1 if both GSUTIL and GCLOUD are not set to executables
common::set_cloud_binaries () {

  logecho -n "Checking/setting cloud tools: "

  for GSUTIL in $(which gsutil) /opt/google/google-cloud-sdk/bin/gsutil; do
    [[ -x $GSUTIL ]] && break
  done

  # gcloud should be in the same place
  GCLOUD=${GSUTIL/gsutil/gcloud}

  if [[ -x "$GSUTIL" && -x "$GCLOUD" ]]; then
    logecho -r $OK
    return 0
  else
    logecho -r $FAILED
    return 1
  fi
}

###############################################################################
# sendmail/mailer front end.
# @optparam (flag) -h - Send html formatted
# @param to - To
# @param from - From
# @param reply_to - Reply To
# @param subject - Subject
# @param cc - cc
# @param file - file to send
#
common::sendmail () {
  local cc_arg
  local html=0

  while [[ "$#" -gt 0 ]]; do
    case "$1" in
      -h) html=1; shift ;;
       *) break ;;
    esac
  done

  local to="$1"
  local from="$2"
  local reply_to="$3"
  local subject="$4"
  local cc="$5"
  local file="$6"

  if [[ "$HOSTNAME" =~ google.com ]]; then
    logecho "$FAILED! sendmail unavailable at Google."
    return 1
  fi

  (
  cat <<EOF+
To: "$to"
From: "$from"
Subject: "$subject"
Cc: "$cc"
Reply-To: "$reply_to"
EOF+
  ((html)) && echo "Content-Type: text/html"
  cat $file
  ) |/usr/sbin/sendmail -t
}

# Stubs for security_layer functions
security_layer::auth_check () {
  logecho "Skipping $FUNCNAME..."
  return 0
}

# Set a common::trap() to capture ^C's and other unexpected exits and do the
# right thing in common::trapclean().
common::trap common::trapclean ERR SIGINT SIGQUIT SIGTERM SIGHUP

# parse cmdline
common::namevalue "$@"

# Run common::manpage to show usage and man pages
common::manpage "$@"
back to top