Permalink
Cannot retrieve contributors at this time
executable file
522 lines (483 sloc)
20.2 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| #=============================================================================== | |
| # | |
| # FILE: worktop | |
| # | |
| # USAGE: ./worktop <args> | |
| # | |
| # DESCRIPTION: This was supposed to be a simple shell script for Gaetan of | |
| # communities.sas.com. Great example of weekend hack scope creep. | |
| # See http://boem.sk/2hwvU8m for original discussion. | |
| # | |
| # OPTIONS: ./worktop -h prints available options | |
| # REQUIREMENTS: see requiredThings variable on line 126 | |
| # BUGS: Always, it's a fun game | |
| # NOTES: github.com/boemska/worktop | |
| # AUTHOR: Nikola Markovic | |
| # ORGANIZATION: Boemska (boemskats.com) | |
| # CREATED: 12/10/2016 10:27 | |
| # REVISION: v0.1 | |
| # | |
| #=============================================================================== | |
| # This program is free software: you can redistribute it and/or modify it under | |
| # the terms of the GNU General Public License as published by the Free Software | |
| # Foundation, either version 3 of the License, or (at your option) any later | |
| # version. | |
| # | |
| # This program is distributed in the hope that it will be useful, but WITHOUT | |
| # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS | |
| # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. | |
| # | |
| # You should have received a copy of the GNU General Public License along with | |
| # this program. If not, see http://www.gnu.org/licenses/. | |
| #=============================================================================== | |
| # set -o nounset # Treat unset variables as an error | |
| #------------------------------------------------------------------------------- | |
| # Colours and stuff for the prettiness later | |
| #------------------------------------------------------------------------------- | |
| Red=$(tput setaf 1) | |
| Gre=$(tput setaf 2) | |
| Yel=$(tput setaf 3) | |
| Bla=$(tput setaf 0) | |
| BRed=$(tput bold)$(tput setaf 1) | |
| BGre=$(tput bold)$(tput setaf 2) | |
| BYel=$(tput bold)$(tput setaf 3) | |
| BWhi=$(tput bold)$(tput setaf 7) | |
| BgRed=$(tput setab 1) | |
| BgGre=$(tput setab 2) | |
| BgYel=$(tput setab 3) | |
| BgWhi=$(tput setab 7) | |
| Invert=$(tput setab 7)$(tput setaf 0) | |
| RCol=$(tput sgr0) | |
| CLine=$(tput el) | |
| CDim=$(tput dim) | |
| Bce=$(tput bce) | |
| URed=$(tput smul)$(tput setaf 1) | |
| UGre=$(tput smul)$(tput setaf 2) | |
| UYel=$(tput smul)$(tput setaf 3) | |
| UWhi=$(tput smul)$(tput setaf 7) | |
| #--- FUNCTIONS --------------------------------------------------------------- | |
| # NAME: startme|finishme | |
| # DESCRIPTION: functions to initialise environment and clean it up after | |
| # PARAMETERS: start: n/a, finish: $1 is exit code, $2 is what to run b4 | |
| # RETURNS: | |
| #------------------------------------------------------------------------------- | |
| function startme { | |
| tput smcup | |
| tput civis | |
| tput clear | |
| } | |
| function finishme { | |
| # read -p 'whats going on' | |
| # back to reality | |
| tput rmcup | |
| tput cnorm | |
| # run the optional $2 function if you want | |
| $2 | |
| # exit with $1 | |
| exit $1 | |
| } | |
| #--- FUNCTION ---------------------------------------------------------------- | |
| # NAME: printHelp | |
| # DESCRIPTION: A function to print the help text. Simple one. | |
| # PARAMETERS: n/a | |
| # RETURNS: console output | |
| #------------------------------------------------------------------------------- | |
| printHelp() { | |
| printf "\n\n${Yel}worktop.sh${RCol} - a bash at helping you stay on top of your SAS Work directories.\n" | |
| printf " Copyright(c) 2016 Nikola Markovic @boemskats\n\n" | |
| printf "Disclaimer: You really shouldn't rely on shell scripts for this kind of thing.\n" | |
| printf " For a more robust cross-platform enterprise monitoring solution\n" | |
| printf " you should consider ${BGre}boemskats.com/esm${RCol}. It's much better. \n\n" | |
| printf "Usage [optional args]: \n\n ${Yel}$(basename "$1") -d <sasworkdir> \n" | |
| printf " [-n <directory sizing interval (seconds)>]\n" | |
| printf " [-g <alert/highlight limit (K/M/G)>] \n" | |
| printf " [-i <directory where metaindex host file is located>] \n" | |
| printf " [-s (run as sudo if your user can't read all work dirs)]\n" | |
| printf " [-l <target logfile to write stuff to>]${RCol}\n\n" | |
| printf "Example (size every 7 minutes, highlight anything larger than 70GB): " | |
| printf "\n\n ${Yel}$(basename "$0") -d /data/saswork -n 420 -l 70\n\n${RCol}" | |
| } # ---------- end of function printHelp ------------ | |
| #--- FUNCTION ---------------------------------------------------------------- | |
| # NAME: checkEverything | |
| # DESCRIPTION: This checks prereqs and parses up command line params | |
| # PARAMETERS: $@ - everything from command line | |
| # RETURNS: Doesn't exit if everything is ok | |
| #------------------------------------------------------------------------------- | |
| checkEverything() { | |
| # initialise default values for below | |
| WORKDIR=. | |
| INTERVAL=30 | |
| WORKLIMIT=1048576 # 1gb default | |
| LOGFILELOC=/dev/null | |
| SUDOAS= | |
| # commands to check for | |
| requiredThings=("tput" "date" "man" "getopts" "basename" "hostname" | |
| "date" "du" "sort" "find" "basename" "cut" "xargs" "read" "echo" "tr") | |
| # check that the arguments we have are correct and stuff | |
| while getopts ":d:n:l:i:g:hs" option; do | |
| case "${option}" in | |
| d) | |
| WORKDIR=${OPTARG};; | |
| n) | |
| INTERVAL=${OPTARG};; | |
| g) | |
| # LIMITARG=${OPTARG} | |
| # LIMITMED=$(numfmt --from=si ${LIMITARG} 2>&1) | |
| # if [[ $? > 0 ]]; then | |
| # printerror "$LIMITMED." | |
| # printscr "Remember, -g needs SI units. Look at man numfmt maybe." | |
| # exit 2 | |
| # fi | |
| # # WORKLIMIT=$(( $LIMITMED / 1000 ));; | |
| # WORKLIMIT=$( | |
| # | |
| # ) | |
| # THIS REPLACES NUMFMT... looks like coreutils only got it recently | |
| local SUFFIXES=(K M G T P E Z Y) | |
| local MULTIPLIER=1 | |
| shopt -s nocasematch | |
| local MATCHED=0 | |
| for SUFFIX in "${SUFFIXES[@]}"; do | |
| REGEX="^([0-9]+)(${SUFFIX}i?B?)?\$" | |
| if [[ ${OPTARG} =~ $REGEX ]]; then | |
| WORKLIMIT=$((${BASH_REMATCH[1]} * MULTIPLIER)) | |
| MATCHED=1 | |
| fi | |
| ((MULTIPLIER *= 1024)) | |
| done | |
| if [[ $MATCHED -eq 0 ]]; then | |
| printerror "Invalid size format ${OPTARG}" | |
| printscr "\nSome examples of valid size threshold formats:\n" | |
| printscr " worktop -g 12G" | |
| printscr " worktop -g 23TB" | |
| printscr " worktop -g 31mB" | |
| printscr "\netc... you get the idea." | |
| printscr "\nPlease ensure you use a valid size format." | |
| exit 2 | |
| fi | |
| shopt -u nocasematch | |
| ;; | |
| i) | |
| METAFILEDIR=${OPTARG};; | |
| l) | |
| LOGFILELOC=${OPTARG};; | |
| h) | |
| printHelp | |
| exit 0;; | |
| s) | |
| sudo -v | |
| if [ $? == 0 ]; then | |
| SUDOAS=sudo | |
| else | |
| printerror "Sudoing unsuccessful :(" | |
| printscr "\n\nThe -s argument was passed, but you failed to sudo." | |
| printscr "\n${Yel}worktop${RCol} will run in non-superuser mode." | |
| read -p "Hit Enter to continue." | |
| fi;; | |
| \?) | |
| printerror "Invalid option -$OPTARG. " | |
| printscr "\nFor help with this script, type ${Yel}$(basename "$0") -h${RCol}\n" | |
| exit 2;; | |
| :) | |
| printerror "Option -$OPTARG requires an argument" | |
| printscr "\nFor help with this script, type ${Yel}$(basename "$0") -h${RCol}\n" | |
| exit 2;; | |
| esac | |
| done | |
| # check existence of our main parameter and verify that it is a valid directory | |
| if [ "$WORKDIR" == "" ]; then | |
| printerror "Work directory not specified." | |
| printscr "\nFor help with this script, type ${Yel}$(basename "$0") -h${RCol}\n" | |
| exit 2 | |
| fi | |
| if [ ! -d "$WORKDIR" ]; then | |
| printerror "The specified work directory ${Red}$WORKDIR${RCol} does not exist." | |
| printscr "\nFor help with this script, type ${Yel}$(basename "$0") -h${RCol}\n" | |
| exit 2 | |
| fi | |
| # now check that all bits we need are there | |
| local thingsNotFound=0; | |
| for thing in "${requiredThings[@]}"; do | |
| command -v $thing >/dev/null 2>&1 || { | |
| printerror "Command ${Yel}${thing}${RCol} not found or unavailable." ; | |
| thingsNotFound=$(( $thingsNotFound + 1)) | |
| } | |
| done | |
| if [[ $thingsNotFound -ne 0 ]]; then | |
| printscr "Aborting." | |
| exit 1 | |
| fi | |
| # this can't really go anywhere else i guess | |
| NOTQUITELIMIT=$(( $WORKLIMIT / 2 )) | |
| } # ---------- end of function checkEverything ------ | |
| #--- FUNCTION ---------------------------------------------------------------- | |
| # NAME: resizeFrame | |
| # DESCRIPTION: Function to recalculate terminal size dependent vars | |
| # every time that the size of the terminal is chnaged. | |
| # PARAMETERS: n/a | |
| # RETURNS: | |
| # SETS: tlines, tcolumns, colNpos, colNlen | |
| #------------------------------------------------------------------------------- | |
| function resizeFrame { | |
| tlines=$(tput lines) | |
| tcolumns=$(tput cols) | |
| colpad=1 | |
| col1pos=0 | |
| col2pos=$(( $col1pos + 10 )) | |
| col3pos=$(( $col2pos + 8 )) | |
| col4pos=$(( $col3pos + $tcolumns / 6 )) | |
| col5pos=$(( $col4pos + 20 )) | |
| col1len=$(( $col2pos - $col1pos - $colpad )) | |
| col2len=$(( $col3pos - $col2pos - $colpad )) | |
| col3len=$(( $col4pos - $col3pos - $colpad )) | |
| col4len=$(( $col5pos - $col4pos - $colpad )) | |
| col5len=$(( $tcolumns - ( $col5pos + 1) - $colpad )) | |
| # redraw header and column headers | |
| headerrow=3 | |
| headerbg=$(tput cup ${headerrow} 0)${BgWhi}${Bla}${Bce}${CLine} | |
| headertxt="$(tput cup ${headerrow} ${col1pos})SIZE" | |
| headertxt+="$(tput cup ${headerrow} ${col2pos})PID" | |
| headertxt+="$(tput cup ${headerrow} ${col3pos})METAUSER" | |
| headertxt+="$(tput cup ${headerrow} ${col4pos})HOST" | |
| headertxt+="$(tput cup ${headerrow} ${col5pos})TEMPORARY DIRECTORY" | |
| headertxt+="${RCol}" | |
| dirstoshow=$(( $tlines - $headerrow )) | |
| printf "$headerbg" | |
| printf "$headertxt" | |
| drawtopbit | |
| } # ---------- end of function resizeFrame ---------- | |
| #--- FUNCTIONS --------------------------------------------------------------- | |
| # NAME: print[left|middle|right|status|substatus] | |
| # DESCRIPTION: Some utility functions for printing stuff in places | |
| # PARAMETERS: $1 is Message, $2 is Row for l/m/r/sub, BgColor for status | |
| #------------------------------------------------------------------------------- | |
| printright() { | |
| message=$1 | |
| messpos=$(( ${tcolumns} - ${#message} - 2 )) | |
| printf "$(tput cup $2 ${messpos}) ${message}" | |
| } | |
| printmiddle() { | |
| message=$1 | |
| messpos=$(( (${tcolumns} / 2) - (${#message} / 2) - 1 )) | |
| printf "$(tput cup $2 ${messpos}) ${message}" | |
| } | |
| printleft() { | |
| printf "$(tput cup $2 0)$1" | |
| } | |
| printstatus() { | |
| # limit status message to 20 chars | |
| local statusmessage=${1:0:20} | |
| # clear any previous statii | |
| local clearpos=$(( ${tcolumns} - 21 )) | |
| printf "$(tput cup 0 $clearpos)$(tput el)" | |
| local messpos=$(( ${tcolumns} - ${#statusmessage} - 1 )) | |
| printf "$(tput cup 0 ${messpos}) ${Bla}${2}${statusmessage}${RCol}" | |
| } | |
| printsubstatus() { | |
| # limit substatus message to 30 chars | |
| local statusmessage=${1:0:30} | |
| # clear any previous statii | |
| local clearpos=$(( ${tcolumns} - 31 )) | |
| printf "$(tput cup $2 $clearpos)$(tput el)" | |
| local messpos=$(( ${tcolumns} - ${#statusmessage} - 1 )) | |
| printf "$(tput cup $2 ${messpos}) ${statusmessage}${RCol}" | |
| } | |
| #--- FUNCTIONS --------------------------------------------------------------- | |
| # NAME: print[scr|log|error] | |
| # DESCRIPTION: Some more utility functions for printing stuff... zzz | |
| # scr prints to the screen, | |
| # log to the log, | |
| # error sticks a big red error in front and prints to both | |
| # PARAMETERS: $1 is whatever is to be printed | |
| #------------------------------------------------------------------------------- | |
| printscr() { printf "$@\n"; } | |
| printlog() { echo -e "$(date +%F,%T%t)$@" >> $LOGFILELOC; } | |
| printerror() { printscr "${Red}ERROR:${RCol} $@"; printlog "ERROR: $@"; } | |
| #--- FUNCTION ---------------------------------------------------------------- | |
| # NAME: drawtopbit | |
| # DESCRIPTION: Draws the header bits above the fold... the top bits | |
| # PARAMETERS: n/a | |
| #------------------------------------------------------------------------------- | |
| drawtopbit () { | |
| printleft "${CLine}" 0 | |
| printleft "${CLine}" 1 | |
| printleft "${CLine}" 2 | |
| printmiddle "WORKtop" 0 | |
| printmiddle "(kind of like top for SASWORK)" 1 | |
| printmiddle "©2016 boemskats.com" 2 | |
| printleft "${BYel}$WORKDIR${RCol} on ${BYel}$(hostname -s)${RCol}" | |
| local prettyhard=$(echo ${WORKLIMIT} | awk '{ sum=$1 ; | |
| hum[1024**3]="Tb"; | |
| hum[1024**2]="Gb"; | |
| hum[1024]="Mb"; | |
| for (x=1024**3; x>=1024; x/=1024) | |
| { if (sum>=x) | |
| { printf "%.1f %s\n",sum/x,hum[x];break } | |
| } | |
| } | |
| ') | |
| local prettysoft=$(echo ${NOTQUITELIMIT} | awk '{ sum=$1 ; | |
| hum[1024**3]="Tb"; | |
| hum[1024**2]="Gb"; | |
| hum[1024]="Mb"; | |
| for (x=1024**3; x>=1024; x/=1024) | |
| { if (sum>=x) | |
| { printf "%.1f %s\n",sum/x,hum[x];break } | |
| } | |
| } | |
| ') | |
| # local prettyhard=$(numfmt --to=si --from=si ${WORKLIMIT}K) | |
| # local prettysoft=$(numfmt --to=si --from=si ${NOTQUITELIMIT}K) | |
| printleft "Highlights @ ${Red}${prettyhard}${RCol} and ${Yel}${prettysoft}${RCol} " 1 | |
| printleft "Hit ${BGre}R${RCol} to update, ${BGre}Q${RCol} to quit" 2 | |
| } # ---------- end of function drawtopbit ---------- | |
| #--- FUNCTION ---------------------------------------------------------------- | |
| # NAME: calculate | |
| # DESCRIPTION: Do all of the calculationing things, over and over forever | |
| # (runs du, matches dirs, draws on screen) | |
| # PARAMETERS: n/a | |
| #------------------------------------------------------------------------------- | |
| function calculate { | |
| printlog "Starting periodic sizing..." | |
| # offset=1 | |
| # keep doing this for ever and ever | |
| while true ; do | |
| # variable cleardown and reinit | |
| unset rowShade col1vals col2vals col3vals col4vals col5vals timermsg active | |
| declare -a rowShade col1vals col2vals col3vals col4vals col5vals active | |
| timerstart=$(date +%s%3N) | |
| printstatus "sizing directory" "${BgRed}" | |
| # basically the entire script right here | |
| let i=0 | |
| while IFS=$'\n' read -r line ; do | |
| works[i]="$line" | |
| ((++i)) | |
| done < <($SUDOAS du -k --max-depth=0 $WORKDIR/*/ 2>/dev/null | sort -n -r) | |
| timerend=$(date +%s%3N) | |
| sizetime=$(( $timerend - $timerstart )) | |
| timermsg="${sizetime}ms to read the disk" | |
| printsubstatus "$timermsg" 1 | |
| timerstart=$(date +%s%3N) | |
| printstatus "calculating" "${BgYel}" | |
| # load any metaindexes that may exist | |
| if [ $(find $METAFILEDIR/workmeta* 2>/dev/null | wc -l) -gt 0 ]; then | |
| for metafile in $METAFILEDIR/workmeta*; do | |
| let i=0 | |
| while IFS=$'\n' read -r line ; do | |
| raw[i]="$line\n" | |
| ((++i)) | |
| done < $metafile | |
| fileshort=$(basename $metafile) | |
| nodename=$(echo ${fileshort:9:99} | tr . _ ) | |
| for (( CNTR=0; CNTR<${#raw[@]} ; CNTR+=1 )); do | |
| # strip new line character from the string as we assign to $line | |
| line=${raw[$CNTR]%'\n'} | |
| thisPid=${line% *} | |
| thisuser=${line#$thisPid } | |
| # removes - from hostname otherwise the variable assigment fails | |
| declare lookuphash__${nodename/-/}_${thisPid}="${thisuser}" | |
| done | |
| done | |
| fi | |
| # go through the results and parse them up | |
| for (( CNTR=0; CNTR<${#works[@]} && CNTR<$dirstoshow; CNTR+=1 )); do | |
| thisrow=$(( $CNTR + headerrow + 1 )) | |
| thisSize=$(echo ${works[$CNTR]} | cut -d ' ' -f 1) | |
| # thisPrettySize=$(numfmt --to=si --from=si --padding=6 ${thisSize}K) | |
| local thisPrettySize=$(echo ${thisSize} | | |
| awk '{ sum=$1 ; | |
| hum[1024**3]="Tb"; | |
| hum[1024**2]="Gb"; | |
| hum[1024]="Mb"; | |
| for (x=1024**3; x>=1024; x/=1024) | |
| { if (sum>=x) | |
| { printf "%.1f %s\n",sum/x,hum[x];break } | |
| } | |
| } | |
| ') | |
| thisDir=$(echo ${works[$CNTR]} | cut -d ' ' -f 2 | xargs basename) | |
| # only do this for legit saswork formatted dirs so that bash can dehex | |
| local dirsub="${thisDir:0:8}" | |
| if [[ "${dirsub}" == "SAS_work" || "${dirsub}" == "SAS_util" ]] ; then | |
| thisHex=$(echo "${thisDir}" | cut -d "_" -f 2 | cut -b 10-) | |
| thisHost=$(echo "${thisDir}" | cut -d "_" -f 3 | tr . _ ) | |
| thisPid=$(( 16#${thisHex} )) | |
| # check meta user lookup hash | |
| var=lookuphash__${thisHost/-/}_${thisPid} | |
| # swap the _ back to . for FQDN hosts for display purposes | |
| thisHost=$(echo "${thisHost}" | tr _ .) | |
| if [ -n "${!var}" ] ; then | |
| thisUser="${!var}" | |
| else | |
| thisUser="-" | |
| fi | |
| else | |
| thisHex="-" | |
| thisHost="-" | |
| thisPid="-" | |
| thisUser="-" | |
| fi | |
| #check if the pid is active. 0 = alive on host | |
| ps -p $thisPid 2>&1 >/dev/null | |
| active=$? | |
| # apply highlighting | |
| if [[ $active == 0 ]]; then | |
| if [[ ${thisSize} -ge $WORKLIMIT ]] ; then | |
| rowShade[$CNTR]="${Red}" | |
| elif [[ ${thisSize} -ge $NOTQUITELIMIT ]] ; then | |
| rowShade[$CNTR]="${Yel}" | |
| else | |
| rowShade[$CNTR]="${RCol}" | |
| fi | |
| else | |
| if [[ ${thisSize} -ge $WORKLIMIT ]] ; then | |
| rowShade[$CNTR]="${URed}" | |
| elif [[ ${thisSize} -ge $NOTQUITELIMIT ]] ; then | |
| rowShade[$CNTR]="${UYel}" | |
| else | |
| rowShade[$CNTR]="${UWhi}" | |
| fi | |
| fi | |
| col1vals[$CNTR]="$(tput cup ${thisrow} ${col1pos})${thisPrettySize:0:${col1len}}" | |
| col2vals[$CNTR]="$(tput cup ${thisrow} ${col2pos})${thisPid:0:${col2len}}" | |
| col3vals[$CNTR]="$(tput cup ${thisrow} ${col3pos})${thisUser:0:${col3len}}" | |
| col4vals[$CNTR]="$(tput cup ${thisrow} ${col4pos})${thisHost:0:${col4len}}" | |
| col5vals[$CNTR]="$(tput cup ${thisrow} ${col5pos})${CDim}${thisDir:0:${col5len}}" | |
| done | |
| timerend=$(date +%s%3N) | |
| theresttime=$(( $timerend - $timerstart )) | |
| timermsg="${theresttime}ms to do the rest" | |
| printsubstatus "$timermsg" 2 | |
| printlog "Sized in ${sizetime}ms, matched in ${theresttime}ms" | |
| for (( CNTR=0; CNTR<${#works[@]} && CNTR<$dirstoshow; CNTR+=1 )); do | |
| printf "${rowShade[$CNTR]}${col1vals[$CNTR]}${CLine}${col2vals[$CNTR]}${col3vals[$CNTR]}${col4vals[$CNTR]}${col5vals[$CNTR]}${RCol}" | |
| done | |
| nextupdate=$INTERVAL | |
| while [ $nextupdate -gt 0 ]; do | |
| printstatus "Updating in ${nextupdate}s" "${BgGre}" | |
| read -rsn1 -t 1 keyaction | |
| if [ "$keyaction" == "r" -o "$keyaction" == "R" ]; then | |
| nextupdate=1; | |
| elif [ "$keyaction" == "q" -o "$keyaction" == "Q" ]; then | |
| finishme 0 | |
| fi | |
| nextupdate=$(($nextupdate-1)) | |
| done | |
| done | |
| } # ---------- end of function recalculate ---------- | |
| #------------------------------------------------------------------------------- | |
| # Right, that's it. I want to run, be freeee | |
| #------------------------------------------------------------------------------- | |
| #- but first just make sure everything is ok obvs ------------------------------ | |
| checkEverything $@ | |
| startme | |
| resizeFrame | |
| #- oh and make sure we handle resizes and kill signals ------------------------- | |
| trap resizeFrame WINCH | |
| trap finishme EXIT | |
| #- woo ok ok here we go -------------------------------------------------------- | |
| calculate | |
| #------------------------------------------------------------------------------- | |
| # fin | |
| #------------------------------------------------------------------------------- |