A simple todo list for your shell

This simple script lists today’s tasks every time you log in (or create a new window in your screen).

# Just add me to your .bash_profile:
# [ -x ~/.bash_today ] && . ~/.bash_today
 
TODAYD="${HOME}/.today.d"
 
if [ ! -d "${TODAYD}/today" ]; then
        mkdir -p "${TODAYD}/today"
fi
 
TODAY=$(date +%F)
 
function toDay() {
        local DAY=${1};         shift;
        local CARD=${1};        shift;
        local TODO=${*}
        echo "${TODAYD}/${DAY}/${CARD}: ${TODO}"
        (
                if [ ! -d "${TODAYD}/${DAY}" ]; then
                        mkdir -p "${TODAYD}/${DAY}"
                fi
        ) && echo "${TODO} [${TODAY}]" >> "${TODAYD}/${DAY}/${CARD}"
}
 
echo -n "Today is $(date +%A), ${TODAY}"
 
if [ -d "${TODAYD}/${TODAY}" ] && STUFF=$(ls "${TODAYD}/${TODAY}") && [ -n "${STUFF}" ]; then
        for CARD in ${STUFF}; do
                (cat "${TODAYD}/${TODAY}/${CARD}" >> "$TODAYD/today/${CARD}") &&
                        rm -f "${TODAYD}/${TODAY}/${CARD}"
        done
        rmdir "${TODAYD}/${TODAY}"
fi
 
if [ -d "${TODAYD}/today" ] && STUFF=$(ls -X "${TODAYD}/today/") && [ -n "${STUFF}" ]; then
        echo \ and here\'s what to do:
        for CARD in ${STUFF}; do
                echo -n "${CARD}) "
                cat "${TODAYD}/today/${CARD}"
        done
else
        echo , don\'t you know what to do? =\)
fi

Adding a new task is as easy as typing something like that:

toDay 2015-13-13 universe.999 Save the universe!

On the 13’th day of the 13’th month a new task will be added with priority 999 and on that day you may see:

Today is Saturday, 2015-13-13 and here's what to do:
flowers.001) Water the flowers [2015-04-20]
goodness.013) Do something good [2000-02-02]
universe.999) Save the universe! [2015-01-01]

Get updates from GitHub: https://gist.github.com/melnik13/200673d429c828e8a860.

Maintenance works without drama: how to inform visitors when the server has been shut down

Sometimes you need to shut some web-services down for a while to do some maintenance works. Of course, you have warned all users about the scheduled downtime period, but what if it’s just impossible to inform all potential victims, e.g. every potential visitor of some web-site that won’t be working? It’s a good idea to take care of those people (or bots) and to let them know what’s going on and when their websites is up. It’s easy to make a web-server showing some maintenance notice, but what if you need to shut the server down completely? If it’s a single server, not a cluster, what visitors will see when you shut it down? They will wait for the page to be loaded, but a message saying that the connection is timed out will be the only reward for their patience. It would be much better to let them know that you’re apologizing for inconveniences, working hard to fix the issue and have some prognosis on when it’ll be okay.

So, let’s involve some spare web-server (supposedly, you have some, don’t you?) and make it showing some calming message for users and visitors of their web-sites. It’s quite easy when you have a spare web-server connected to the same network or vlan, there are just few simple steps that needed to be performed.

Step 1: creating the announcement page

Maybe one of your colleagues has already wrote an announcement and published it where your users can read it, didn’t they? If yes – let’s take the text for the notice from there, if not – let’s write the message by ourselves. Of course, as we need it to be formatted properly, we should add some HTML-tags.

<!doctype html>
<title>Site Maintenance</title>
<style>
  body { text-align: center; padding: 150px; }
  h1 { font-size: 50px; }
  body { font: 20px Helvetica, sans-serif; color: #333; }
  article { display: block; text-align: left; width: 650px; margin: 0 auto; }
  a{ color: #dc8100; text-decoration: none; }
  a:hover { color: #333; text-decoration: none; }
</style>

<article>
    <h1>We&rsquo;ll be back soon!</h1>
    <div>
        <p>Sorry for the inconvenience but we&rsquo;re performing some planned maintenance at the moment. If you need to you can always contact our support team by sending a message to <a href="mailto:support@tuchacloud.com"></a> or dialing +380445835583.</p>
        <p>Works can take some time, but we'll bring this website up before 4:20 of April 20th, 2015 (GMT)!</p>
        <p>&mdash; The Team of <a href="https://tuchacloud.com/">TuchaCloud</a></p>
    </div>
</article>

Lots of thanks to Pitch for the sample!)

Let’s put this page somewhere, for example to /var/www/vhosts/13.13.13.13/maintenance.html

Step 2: configuring nginx

No, I won’t believe you don’t know how to install nginx on your operating system. So let’s suppose you’ve done it already and we only need to add the following server stanza to its configuration:

server {
  listen  13.13.13.13:80 default_server;

  access_log /var/log/nginx/13.13.13.13-maintenance-access.log;
  error_log  /var/log/nginx/13.13.13.13-maintenance-error.log;

  location ~ .* {
    return 503;
  }

  error_page 503 /maintenance.html;

  location = /maintenance.html {
    charset utf-8; # in case you'd like to use it
    root /var/www/vhosts/13.13.13.13/;
    break;
  }
}

Step 3: interception

If the spare server is in the same IP-network with our 13.13.13.13, it’s easy to make it receive all requests intended for 13.13.13.13, just add an according alias to the network interface, for example:

ifconfig eth0:0 13.13.13.13/24

Well, that’s all! Just don’t forget to update the information on the maintenance page when you realize you won’t bring the service up in time you’ve promised! :)

How to check some host by arping without looking up the corresponding network interface?

Sometimes you have to check if some host replies on ARP “who-has” requests instead of simply pinging it. You probably need it when the host has its firewall configured to deny ICMP “echo” request (as you have probably noticed, most of hosts running Microsoft Windows Server are configured to ignore these requests) to check if the host is up. So, arping really helps, but to use it you have to look up the network interface on which you shall call for this host.

This simple script does it for you: it looks up for the corresponding network interface and calls the arping utility.

 #!/bin/sh

if [ -z "${1}" ]; then
        echo "Chocho?" >&2
        exit 1
fi

HOSTIP="${@: -1}"

ROUTE=$(ip route get "${HOSTIP}")
        if [ -z "${ROUTE}" ]; then
        echo "Can't find the route to this host" >&2
        exit 1
fi

IFACE=$(echo "${ROUTE}" | sed -rn "s/^.*dev (eth.[0-9\.]+)[[:space:]]+.*$/\1/gp")
        if [ -z "${IFACE}" ]; then
        echo "Can't find the interface of this host" >&2
        exit 1
fi

exec arping -I "${IFACE}" ${@} 

Just run it with any parameters you’d like to pass to arping, it will add the “-I” parameter and execute arping.

If I ever update this script, you can get the fresh version from GitHub: https://gist.github.com/melnik13/fb2a05d4084a6c039ccb.

P.S. I’d also suggest you to use arping by Thomas Habets – it doesn’t require you to indicate the interface, as it determines it by itself.

How to restart some service when server’s load average goes high as fuck

This post’s name and script’s comments are pretty descriptive, right? :)

#!/bin/bash
#
# Just put it to your crontab:
#
# * * * * * root /root/bin/lalala.sh --la1 50 --la5 20 --laf 10 --service httpd --kill --process httpd
#
# It will kill all httpd processes and restart httpd service when LA1 becomes greater or equal to 50, LA5 - to 20 and LA15 - to 10
#

OPTS=$(getopt -n $(basename ${0}) -o 1:5:f:s:kp: -l la1:,la5:,laf:,service:,kill,process: -- "${@}")
if [ "${?}" -ne 0 ]; then
        echo "Can't get options" >&2
        exit 1
fi
eval set -- "${OPTS}"
while :; do
        case "${1}" in
                -1|--la1)
                        THR1="${2}"; shift 2;;
                -5|--la5)
                        THR5="${2}"; shift 2;;
                -f|--laf)
                        THR15="${2}"; shift 2;;
                -s|--service)
                        SERVICE="${2}"; shift 2;;
                -k|--kill)
                        KILL="1"; shift 1;;
                -p|--process)
                        PROCESS="${2}"; shift 2;;
                --)
                        shift; break;;
                *)
                        echo "Can't recognize the option: ${1}"; exit 1;;
        esac
done
if [ -z "${SERVICE}" ]; then
        echo "Can't understand what service shall I restart" >&2
        exit 1
fi
if [ "${KILL}" ] && [ -z "${PROCESS}" ]; then
        echo "Can't understand what process shall I kill" >&2
        exit 1
fi

LA=$(  uptime | sed -r 's/^.*load average: ([0-9]+)(\.[0-9]+)?, ([0-9]+)(\.[0-9]+)?, ([0-9]+)(\.[0-9]+)?$/\1\t\3\t\5/g' )
LA1=$( echo "${LA}" | cut -f1)
LA5=$( echo "${LA}" | cut -f2)
LA15=$(echo "${LA}" | cut -f3)

if
        [ -n "${THR1}"  ] && [ -n "${LA1}"  ] && [ "${LA1}"  -ge "${THR1}"  ] &&
        [ -n "${THR5}"  ] && [ -n "${LA5}"  ] && [ "${LA5}"  -ge "${THR5}"  ] &&
        [ -n "${THR15}" ] && [ -n "${LA15}" ] && [ "${LA15}" -ge "${THR15}" ];
then
        if [ "${KILL}" ]; then
                while PIDS=$(pidof ${PROCESS}); do
                        echo "Slaying ${PIDS}..."
                        kill -9 ${PIDS}
                        sleep 1
                done
                sleep 10
        else
                service "${SERVICE}" restart
        fi
fi

Of course, you can get all further updates of this script from GitHub: https://gist.github.com/melnik13/04225e4e60a8d66f9ab6.

Now you can sleep tight in the night! :)

How to run commands on different servers with different login credentials

Wouldn’t it be lovely if you could run some command on a bunch of servers?

Of course, you can! Most of us would do something like that:

for HOST in host1 host2 host3; do { ssh "${HOST}" "echo ZALOOPA > /var/tmp/zaloopa"; } done

But what if there are dozens of servers with different usernames to log-in with, different SSH-keys to use and even different ports to connect to? It would be quite convenient to use some conficuration file with all these login-details:

#
# The format is following:
#     hostname:username:port:ssh-key #comments #tags
# All parameters besides of the first are optional
#
# Regular servers
host1
host2
host3
# The servers where we shall use an alternative username to log in
host4:user
# Servers where we shall use an alternative port to log in
host5::30022
host6:user:30022
# Servers where we shall use an alternative SSH-key to log in
host7:::~/.ssh/id_rsa.zaloopa
host8:user::~/.ssh/id_rsa.zaloopa
host9:user:30666:~/.ssh/id_rsa.zaloopa
# Servers we need to be found by some hashtags (so we can run the script with the -g parameter: "-g '#huerga'")
host10                                  #huerga
host11:user                             #huerga
host12:user:30022                       #huerga #zaebis #pizdets
host13:user:30022:~/.ssh/id_rsa.zaloppa #huerga #pizdets

No problem! Grab this simple script to have it possible:

#!/bin/bash
 
OPTS=$(getopt -n $(basename ${0}) -o c:g:y -l command:,grep-criterias:yes -- "${@}")
if [ "${?}" -ne 0 ]; then
        echo "Can't get options" >&2
        exit 1
fi
eval set -- "${OPTS}"
while :; do
        case "${1}" in
                -c|--command)
                        COMMAND="${2}"; shift 2;;
                -g|--grep-criterias)
                        CRITERIAS="${2}"; shift 2;;
                -y|--yes)
                        SURE="Y"; shift 1;;
                --)
                        shift; break;;
                *)
                        echo "Can't recognize the option: ${1}"; exit 1;;
        esac
done
 
TARGETS=$(cat ~/etc/servers | grep -E "${CRITERIAS}" | sed -e 's/[[:space:]]*#.*$//' -e '/^$/d')
 
if [ -z "${COMMAND}" ]; then
        echo "The command is undefined" >&2
        exit 1
fi
 
while :; do
        echo -e "Our targets are:\n${TARGETS}" >&2
        [ "${SURE}" != "Y" ] && read -p "Are you sure you want to run '${COMMAND}'? (Y/N) " SURE
        case "${SURE}" in
                [Yy])
                        for TARGET in ${TARGETS}; do
                                HOST=$(echo "${TARGET}" | sed -r -n -e 's/^([^:]+)(:([^:]*))?(:([^:]*))?(:([^:]*))?$/\1/gp')
                                USER=$(echo "${TARGET}" | sed -r -n -e 's/^([^:]+)(:([^:]*))?(:([^:]*))?(:([^:]*))?$/\3/gp')
                                PORT=$(echo "${TARGET}" | sed -r -n -e 's/^([^:]+)(:([^:]*))?(:([^:]*))?(:([^:]*))?$/\5/gp')
                                SKEY=$(echo "${TARGET}" | sed -r -n -e 's/^([^:]+)(:([^:]*))?(:([^:]*))?(:([^:]*))?$/\7/gp')
                                [ -z "${USER}" ] && USER=$(whoami)
                                [ -z "${PORT}" ] && PORT=22
                                [ -z "${SKEY}" ] && SKEY=~/.ssh/id_rsa
                                echo "Running '${COMMAND}' on ${HOST}:${PORT}..." >&2
                                ssh -i "${SKEY}" -p "${PORT}" -l "${USER}" "${HOST}" "${COMMAND}"
                        done
                        break
                        ;;
                [Nn])
                        break
                        ;;
                *)
                        echo "So, Y or N?" >&2
                        ;;
        esac
done

Now you can do something like that:

spread.sh --command "yum -y upgrade && reboot" --grep-criteria '(#centos|#pizdos)' --yes

If I ever update this script with some new features, you could always get the latest verion from GitHub: https://gist.github.com/melnik13/be1160a5bf73e246cd5f.

Enjoy your great power, but don’t forget about great responsibility comes with it! :)

How to monitor which files are growing faster than other ones?

If you ever needed a tool to get to know which files are growing faster than other ones, you probably will be quite satisfied with this simple bash-script.

#!/bin/sh
 
# Checking the spool directory
SPOOL="/var/spool/diskhogs"
if [ ! -e "${SPOOL}" ]; then
        mkdir -p "${SPOOL}"
fi
if [ ! -d "${SPOOL}" ]; then
        echo "There are no ${SPOOL} directory" >&2
        exit 1
fi
 
if [ -z "${1}" ]; then
        DIR=.
else
        DIR="${1}"
fi
 
FILES=$(find "${DIR}" -type f)
 
TIME=$(date +%s)
if [ -z "${TIME}" ]; then
        echo "Can't determine current time" >&2
        exit 1
fi
 
for FILE in ${FILES}; do
 
        SIZE=$(ls -nl ${FILE} | awk '{ print $5 }')
        if [ -z "${SIZE}" ]; then
                echo "Can't determine size of the ${FILE} file" >&2
                continue
        fi
 
        sqlite3 "${SPOOL}/db" "INSERT INTO sizes VALUES ('${FILE}', '${TIME}', '${SIZE}');"
        if [ ${?} -ne 0 ]; then
                continue
        fi
 
done
 
for PERIOD in 60 300 600 1800 3600 86400; do
 
        TIME_WAS=$((${TIME} - ${PERIOD}))
 
        (
                echo "*** Since $(date --date="@${TIME_WAS}") (${PERIOD} seconds ago) ***"
                sqlite3 \
                        "${SPOOL}/db" \
                        "SELECT MAX(size) - MIN(size) AS mm, name
                                FROM sizes
                                WHERE time >= '${TIME_WAS}'
                                GROUP BY name
                                ORDER BY mm
                        ;"
        ) > "${SPOOL}/report_${PERIOD}"
 
done

Simply put it to your crontab:

* * * * * root /root/bin/diskhogs.sh /directory/to/monitor

Resulting reports will be put to /var/spool/diskhogs/report_*. You also can generate any reports by yourself, just use sqlite:

sqlite3 /var/spool/diskhogs/db "
    SELECT MAX(size) - MIN(size) as mm, name
        FROM sizes
        WHERE
            time >= '$(date --date='10 days ago' +%s)' AND
            name like '/var/lib/libvirt/images/%'
        GROUP BY name
        ORDER BY mm DESC
    ;"

This query will generate the report for all files in the /var/lib/libvirt/images directory for the last 10 days.

If there are any updates, you could get them from GitHub: https://gist.github.com/melnik13/7ad33c57aa33742b9854.

Happy monitoring! :)