Skip to content

Instantly share code, notes, and snippets.

@pavel-kirienko
Last active April 6, 2021 02:32
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pavel-kirienko/99be748c68dd04c4175444c219b6e7c3 to your computer and use it in GitHub Desktop.
Save pavel-kirienko/99be748c68dd04c4175444c219b6e7c3 to your computer and use it in GitHub Desktop.
Web interface for a backup storage server (CGI bash script, usable with any web server that supports CGI)
#!/bin/bash
#
# Backup server web interface (CGI script).
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Copyright (C) 2017, Pavel Kirienko <pavel.kirienko@zubax.com>.
#
BACKUP_DIR=/opt/backup/
MAX_TIME_SINCE_MODIFICATION=$((3600*24*8))
PAGE_REFRESH_INTERVAL=$((60*30))
function find_newest_file_in_dir()
{
ls -t $1/* | head -1
}
function print_file_modification_timestamp()
{
stat -c %Y "$1"
}
function print_human_readable_duration()
{
((d=${1}/86400))
((h=(${1}%86400)/3600))
printf "%d days %d hours\n" $d $h
}
function print_human_readable_file_size()
{
ls -lah "$1" | awk -F " " {'print $5'}
}
current_timestamp=`date +%s`
echo -en "Content-type: text/html\r\n"
echo -en "\r\n"
cat << EOF
<html>
<head>
<meta http-equiv="refresh" content="${PAGE_REFRESH_INTERVAL}" >
<title>Backup storage status</title>
<style>
body {
background: #fff;
color: #000;
font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;
font-style: normal;
font-variant: normal;
line-height: 120%;
}
pre, tt {
font-family: 'Lucida Console', Monaco, monospace;
background-color: #eee;
}
table {
font-size: 120%;
border-collapse: collapse;
width: 100%;
}
table pre, table tt {
background-color: inherit;
}
th, td {
text-align: left;
border-bottom: 1px solid #ccc;
padding: 0.5em 0.1em;
}
.status-ok { background-color: #cfc; color: #050; }
.status-warning { background-color: #ffc; color: #550; }
.status-error { background-color: #fcc; color: #500; }
.banner {
padding: 1em;
text-align: center;
font-size: 200%;
font-weight: 600;
}
.uppercase {
text-transform: uppercase;
}
</style>
</head>
<body>
<h1>Backup storage status</h1>
<p>
This page displays the status of the backups collected from the Zubax infrastructure.
Each component must back up itself with the following maximum interval:
`print_human_readable_duration $MAX_TIME_SINCE_MODIFICATION`.
If the latest backup is older than the specified amount of time, an error will be displayed.
This page will be refreshing itself automatically.
At the bottom you will find some examples showing how automatic backups can be organized.
</p>
<table>
<thead>
<tr>
<th>Directory</th>
<th>Newest file</th>
<th>Size</th>
<th>Age</th>
<th>Status</th>
</tr>
</thead>
<tbody>
EOF
overall_status=ok
overall_status_text='OK'
for dir in `echo $BACKUP_DIR/*/ | sort`
do
newest_file=`find_newest_file_in_dir $dir`
modification_ts=`print_file_modification_timestamp $newest_file`
since_modification=$((current_timestamp-modification_ts))
if [ $since_modification -gt $MAX_TIME_SINCE_MODIFICATION ]
then
overall_status=error
overall_status_text="Outdated"
status=error
status_text="Outdated"
else
status=ok
status_text="Up to date"
fi
cat << EOF
<tr class="status-$status">
<td><pre>`readlink -m $dir`</pre></td>
<td><pre>`basename $newest_file`</pre></td>
<td>`print_human_readable_file_size $newest_file`</td>
<td>`print_human_readable_duration $since_modification`</td>
<td class="uppercase">$status_text</td>
</tr>
EOF
done
cat << EOF
</tbody>
</table>
<p class="banner uppercase status-$overall_status">Backup status: $overall_status_text</p>
<h2>Server status</h2>
<pre>`uptime`</pre>
<pre>`df -h`</pre>
<h2>Backup directory contents</h2>
<pre>`ls -alh $BACKUP_DIR/*`</pre>
EOF
cat <<'END_OF_EXAMPLES'
<h2>Examples</h2>
<p>
Put the following (edited as necessary) into a cron job, e.g. /etc/cron.daily/backup_my_stuff:
</p>
<pre>
#!/bin/sh
script=$(cat <<'END_OF_REMOTE_SCRIPT'
# Exit on error, expand all commands
set -e && set -x &&
# Configuration parameters (do not use colons in the name template, they break things)
SERVICE_NAME='licensing.zubax.com' &&
ARCHIVE_NAME_TEMPLATE='%Y-%m-%d-%H%M%S' &&
# Move to the target directory, create it if missing
mkdir -p /opt/backup/${SERVICE_NAME}/ && cd $_ &&
# We need the subsequent mv to be atomic. File move can be atomic only if the file is moved within
# the same filesystem, therefore, we MUST keep the temp file in the same directory rather than, say,
# /tmp, because /tmp can easily be a separate file system.
# We make the temp files hidden in order to make them not show up in the backup status web panel.
# Use of the week day and the hour in the name of the temp file ensures that in the WORST case we'll
# get 7*24=168 garbage files in the back-up. That would require that every single attempt to transfer
# the backup should fail, which is exceedingly unlikely.
# Addition of the archive name template to the temp file name allows the remote application to back
# up itself in different ways concurrently. For example, the same application may want to make
# persistent daily snapshots as well as hourly updated non-persistent backups (in which case the
# archive name template would be just a plain string, e.g. 'latest').
tempfile=".${ARCHIVE_NAME_TEMPLATE}.`date +%A-%H`.incomplete" &&
# Perform the transfer now from the remote server into the temp file.
# This is the end of the input pipe, by the way.
cat - > $tempfile
# Check the received archive integrity; abort and clean up if it is not valid.
if ! tar tf $tempfile &> /dev/null; then
echo "BACKUP ARCHIVE IS NOT VALID"
rm -rf $tempfile
exit 1
fi
# Once the archive is transferred and verified, commit it to the backup storage
mv -f $tempfile `date +${ARCHIVE_NAME_TEMPLATE}`.tar.gz
END_OF_REMOTE_SCRIPT
)
# Files and directories to be backed up are specified here:
tar czf - /my-stuff/* |\
ssh uploader@backup.zubax.com "$script"
</pre>
<p>
The above would create a new file for every backup, which is fine for compact mission-critical stuff.
The backup server itself may get rid of very old data automatically, the application that is being
backed up should not concern itself with that.
</p>
<p>
However, if the amount of data being backed up is large (several GB and more),
then weekday named backups might be a good idea, because that would facilitate automatic rotation.
In order to achieve that, replace the archive name template string to %A.
Refer to the date command manual to learn more about the formats.
</p>
<p>
Note that it is not allowed to simply pipe the data from the remote server into a backup file,
because the archive may end up being corrupted, and there would be no way to detect that.
Furthermore, with automatic rotation in place, all downloaded archives may end up corrupted,
and therefore, when the last file is overwritten, there would be no valid backup at all!
</p>
</body>
</html>
END_OF_EXAMPLES
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment