Category Archives: Linux

The Linux Category actually encompasses *BSD, RH, Fedora, Ubuntu, and the like.

Gnome popos Linux

I was reflecting on my tech career just last night and I thought it best to give a bit of background.

I grew up on Macs – a Macintosh SE with an 8MHz processor, 1MB RAM, a 720KB floppy drive, and a 20MB SCSI 25-pin hard disk drive. Running OS 6.

From there we acquired a PowerMac 7100/80AV. Had a 80MHz processor, 16MB RAM, 700MB 50-pin SCSI hard disk drive, and a 1.4MB FDD. Oh and a 2x CDROM – I fondly remember the sounds this would make when trying to load Myst. We also successfully upgraded this to 24MB RAM and replaced the 700MB HDD with a 2.1GB version.

The first internet-connected Mac was next: the PowerMac G3 minitower. This featured a 233MHz processor, 32MB RAM, and a 4GB HDD. The CDROM was a 24x, and the FDD was still there. Ours came with a 100MB Zip drive too. We upgraded to 64MB RAM, added a 12x CD Burner, and replaced the HDD with a 20GB eventually. This came with OS 8, and we attempted to load OSX beta and it was Slow AF.

A buddy and I decided we wanted to try our hands at Linux – I acquired an AMD K6-2 350MHz with 32MB RAM and a 40GB HDD. Playing around with ISA and PCI network cards was fun (10/100). We originally ran Redhat as an internet router, but when the install broke (my fault, but that’s how I learned breaking/fixing), I replaced with Slackware Linux instead.

At this time I started College and I got 2 computers – the first was a PowerMac G4 dual 450Mhz with 128MB RAM, 30GB HDD, and an external Firewire 12x CDBurner. This also had a 100MB Zip Drive. Eventually upgraded to 3x 80GB HDD in RAID5 along with 512MB RAM. Came with OS9 which I upgraded to OSX. The second was a custom built AMD – Asus Board with an AMD Athlon Thunderbird running at 1.4GHz, 40GB HDD, 24x CD Burner, and 128MB RAM – I believe I upgraded it to 256MB at some point. Ran Windows 2000 on this.

I bought a used white iBook 500MHz 64MB RAM 20GB Drive somewhere along the way – it was pretty slow even for the time.

It gets a bit hazy here since I started building PC’s for family and friends.

I bought a used HP Laptop – maybe like an N810 or something? It was back when HP/Compaq merged.

I got a couple laptops for free – gateway tablet and a EeePC (Asus netbook).

ANYWAY, since I’m getting wordy and not actually accomplishing anything, I wanted to say this was now the 3rd time in my career that I’ve attempted to go “Full Linux” on my work computer. The first time ended poorly when I kept breaking the installation (Ubuntu 8.04), the second time I had a systemboard die, although I was cheating on that – Running Linux MX with virtualbox running Win10.

Now I’m running PopOS. I have a VM of Win10 just in case, but overall I’ve been happy as a clam just using the linux OS. Slack, Cisco VPN, RDP, Browser.. it just works for me.

The only problem I had – being my first GNOME GUI – was the lack of a task bar at the bottom. Easy fix:

https://extensions.gnome.org/extension/1160/dash-to-panel/

Tinypilot KVM

I decided to get a tinypilot kvm device for testing purposes – it’s actually pretty neat. Sure I could have saved a few bucks by building it myself, but this way I save time and support someone else’s great ideas.

Anyway, to update the device, SSH to it (DNS name is generally tinypilot)

Login with tinypilot/flyingsopi

Run

/opt/tinypilot/scripts/upgrade && sudo reboot

Enable SNMP on ESXi

Inheriting 3 different companies’ worth of Virtual infrastructure is sometimes a giant PITA. This holds true especially when the 3 different companies all had multiple “admin’s” working at any given time without established standards for naming, setup, configuration, or even maintenance.

Some of the newly-installed VMWare equipment wasn’t showing up on my Nagios monitoring board. First step is to make sure that snmp is setup and actually running (hint, it was not).

I ended up at this auvik support site – and, if it wasn’t so expensive (our budget is about 1/3 of what it “should be” – I would be running this for monitoring of our infrastructure. Think of it as cloud prtg. https://support.auvik.com/hc/en-us/articles/206311526-How-to-enable-SNMP-on-a-VMware-ESXi-hypervisor#topic_esx6

Since we’re running ESXi 6.5 and 6.7 currently:

  • SSH to your ESXi box using the root account
  • esxcli system snmp set -r
  • esxcli system snmp set -c YOURCOMMUNITY
  • esxcli system snmp set -p 161
  • esxcli system snmp set -L "City, State, Country"
  • esxcli system snmp set -C noc@domain.tld
  • esxcli system snmp set -e yes

Or, if you’re using ESXi 7:

  • SSH to your ESXi box using the root account
  • esxcli system snmp set --communities YOURCOMMUNITY
  • esxcli system snmp set --enable true

Nagios Add User

Yes, I’m a creature of habit. I started using Nagios back in 2005 and it was awful. I mean it “worked”, but I had no idea what I was doing. Reinstalling, installing, configuring, new jobs… each time I was learning from my previous mistakes and making it better. It’s now at the point I can perform most of the work without actually referencing anything else. But the point of this blog is for my own notes, so here goes.

  • SSH to your nagios server. I use putty from my primary Windows desktop.
  • Switch to root
    • sudo su -
  • Create the new web user account and password
    • htpasswd /usr/local/nagios/etc/htpasswd.users MYNEWUSER
  • Enter the password twice

Sometimes you’ll receive an error message about “You do not have permission to view information for any of the services you requested”. So we’ll have to edit the cgi.cfg.

  • SSH to your nagios server
  • sudo su -
  • nano /usr/local/nagios/etc/cgi.cfg
  • Add the MYNEWUSER wherever necessary
    • authorized_for_system_information=nagiosadmin,userhere,MYNEWUSER

BTRFS Snapshot Replication

This post is going to be a little bit different in terms of BTRFS and replication. The “normal” way is you use snapshots for backup purposes – or – if you use a snapshot to clone the data, you end up copying the data from the snapshot to the new location. My needs are a little bit different, so I can use the data (read-only) from the snapshot directly.

In my scenario I have a source-of-truth server with 2.3TB of data and over 800,000 files that has daily changes (additions mostly) to the data. This data change is only moderate – about 500 files/1GB per day – but the actual changes occur throughout the business day rather than at a single point of time. The original spec was to RSYNC changes to remote sites daily for the purposes of backups and distribution points, but this was quickly changed to “can we have this run every hour?” by the consumers of the data. Client systems connect to the closest server available and rsync data based on their xml payloads (not including that configuration in this post as it is out of scope).

Unfortunately RSYNC is terribly slow when copying from a single directory with many files. It gets even worse as the copy travels over SSH to geographically disconnected sites; the latency at the best site is 40ms and at the worst is over 150ms. Because this is critical data, it was deemed necessary to have MD5 checksums on each file to guarantee the distribution points are identical to that of the source of truth server. The change of data being only 500 files/1GB had little impact on the 12-20 hours it would take at our worst site.

Enter BTRFS. Yes, I know that ZFS offers this as well, but the BTRFS is slightly more native on Linux than ZFS is, and I only need the checksum (scrub) and snapshot abilities for my file systems as the data is not compressible or deduplication friendly.

Snapshot

I actually downloaded some BTRFS friendly scripts from github: https://github.com/nachoparker/btrfs-snp

chmod +x btrfs-snp
mv btrfs-snp /usr/local/sbin

btrfs-snp /mnt/btrfs/ hourly 2 3600

Sync to Remote Server

Once again, the BTRFS friendly script from github: https://github.com/nachoparker/btrfs-sync

chmod +x btrfs-sync
mv btfrs-sync /usr/local/sbin

btrfs-sync -d -v /mnt/btrfs/.snapshots/ root@10.10.3.21:/mnt/btrfs/snapshot/

My Script

In my case I needed to then utilize the data for clients to connect via RSYNC, which means the data had to be in a specific spot already advertised to those clients. Enter sym links! Here is my full script.

#ping make sure the device responds
ping -c 3 10.130.20.200

#create snapshot named 'hourly', delete any more than 2 snaps, no less than 3600 seconds old
#btrfs-snp /mnt/btrfs/ hourly 2 3600

#TESTING PURPOSES create snapshot named 'hourly', delete any more than 2 snaps, no less than 600 seconds old
btrfs-snp /mnt/btrfs/ hourly 2 600

#send the snapshot to the other system (requires authkeys ssh setup)
btrfs-sync -d -v /mnt/btrfs/.snapshots/ root@10.10.3.21:/mnt/btrfs/snapshot/

#now run the following commands on the remote system - need testing
## as this may cause rsync to fail if a player is currently loading
ssh root@10.10.3.21 'latestdir=$(ls -rt /mnt/btrfs/snapshot | tail -1) && rm /mnt/btrfs/data && ln -s /mnt/btrfs/snapshot/$latestdir/ /mnt/btrfs/data'

Other Scripts

In case they remove from github, Figured I’d put them here:

#!/bin/bash

#
# Simple script that synchronizes BTRFS snapshots locally or through SSH.
# Features compression, retention policy and automatic incremental sync
#
# Usage:
#  btrfs-sync [options] <src> [<src>...] [[user@]host:]<dir>
#
#  -k|--keep NUM     keep only last <NUM> sync'ed snapshots
#  -d|--delete       delete snapshots in <dst> that don't exist in <src>
#  -z|--xz           use xz     compression. Saves bandwidth, but uses one CPU
#  -Z|--pbzip2       use pbzip2 compression. Saves bandwidth, but uses all CPUs
#  -q|--quiet        don't display progress
#  -v|--verbose      display more information
#  -h|--help         show usage
#
# <src> can either be a single snapshot, or a folder containing snapshots
# <user> requires privileged permissions at <host> for the 'btrfs' command
#
# Cron example: daily synchronization over the internet, keep only last 50
#
# cat > /etc/cron.daily/btrfs-sync <<EOF
# #!/bin/bash
# /usr/local/sbin/btrfs-sync -q -k50 -z /home user@host:/path/to/snaps
# EOF
# chmod +x /etc/cron.daily/btrfs-sync
#
# Copyleft 2018 by Ignacio Nunez Hernanz <nacho _a_t_ ownyourbits _d_o_t_ com>
# GPL licensed (see end of file) * Use at your own risk!
#
# More at https://ownyourbits.com
#

set -e -o pipefail

# help
print_usage() {
  echo "Usage:
  $BIN [options] [[user@]host:]<src> [<src>...] [[user@]host:]<dir>

  -k|--keep NUM     keep only last <NUM> sync'ed snapshots
  -d|--delete       delete snapshots in <dst> that don't exist in <src>
  -z|--xz           use xz     compression. Saves bandwidth, but uses one CPU
  -Z|--pbzip2       use pbzip2 compression. Saves bandwidth, but uses all CPUs
  -p|--port         SSH port. Default 22
  -q|--quiet        don't display progress
  -v|--verbose      display more information
  -h|--help         show usage

<src> can either be a single snapshot, or a folder containing snapshots
<user> requires privileged permissions at <host> for the 'btrfs' command

Cron example: daily synchronization over the internet, keep only last 50

cat > /etc/cron.daily/btrfs-sync <<EOF
#!/bin/bash
/usr/local/sbin/btrfs-sync -q -k50 -z /home user@host:/path/to/snaps
EOF
chmod +x /etc/cron.daily/btrfs-sync
"
}

echov() { if [[ "$VERBOSE" == 1 ]]; then echo "$@"; fi }

#----------------------------------------------------------------------------------------------------------

# preliminary checks
BIN="${0##*/}"
[[ $# -lt 2      ]] && { print_usage                                ; exit 1; }
[[ ${EUID} -ne 0 ]] && { echo "Must be run as root. Try 'sudo $BIN'"; exit 1; }

# parse arguments
KEEP=0
PORT=22
ZIP=cat PIZ=cat
SILENT=">/dev/null"

OPTS=$( getopt -o hqzZk:p:dv -l quiet -l help -l xz -l pbzip2 -l keep: -l port: -l delete -l verbose -- "$@" 2>/dev/null )
[[ $? -ne 0 ]] && { echo "error parsing arguments"; exit 1; }
eval set -- "$OPTS"

while true; do
  case "$1" in
    -h|--help   ) print_usage; exit  0 ;;
    -q|--quiet  ) QUIET=1    ; shift 1 ;;
    -d|--delete ) DELETE=1   ; shift 1 ;;
    -k|--keep   ) KEEP=$2    ; shift 2 ;;
    -p|--port   ) PORT=$2    ; shift 2 ;;
    -z|--xz     ) ZIP=xz     PIZ=( xz     -d ); shift 1 ;;
    -Z|--pbzip2 ) ZIP=pbzip2 PIZ=( pbzip2 -d ); shift 1 ;;
    -v|--verbose) SILENT=""  VERBOSE=1        ; shift 1 ;;
    --)                shift;  break   ;;
  esac
done

SRC=( "${@:1:$#-1}" )
DST="${@: -1}"

# detect remote dst argument
[[ "$SRC" =~ : ]] && {
  NET_SRC="$( sed 's|:.*||' <<<"$SRC" )"
  SRC="$( sed 's|.*:||' <<<"$SRC" )"
  SSH_SRC=( ssh -p "$PORT" -o ServerAliveInterval=5 -o ConnectTimeout=1 -o BatchMode=yes "$NET_SRC" )
}

[[ "$SSH_SRC" != "" ]] && SRC_CMD=( ${SSH_SRC[@]} ) || SRC_CMD=( eval )
${SRC_CMD[@]} test -x "$SRC" &>/dev/null || {
  [[ "$SSH_SRC" != "" ]] && echo "SSH access error to $NET_SRC. Do you have passwordless login setup, and adequate permissions for $SRC?"
  [[ "$SSH_SRC" == "" ]] && echo "Access error. Do you have adequate permissions for $SRC?"
}

# detect remote dst argument
[[ "$DST" =~ : ]] && {
  NET="$( sed 's|:.*||' <<<"$DST" )"
  DST="$( sed 's|.*:||' <<<"$DST" )"
  SSH=( ssh -p "$PORT" -o ServerAliveInterval=5 -o ConnectTimeout=1 -o BatchMode=yes "$NET" )
}
[[ "$SSH" != "" ]] && DST_CMD=( ${SSH[@]} ) || DST_CMD=( eval )
${DST_CMD[@]} test -x "$DST" &>/dev/null || {
  [[ "$SSH" != "" ]] && echo "SSH access error to $NET. Do you have passwordless login setup, and adequate permissions for $DST?"
  [[ "$SSH" == "" ]] && echo "Access error. Do you have adequate permissions for $DST?"
  exit 1
}

#----------------------------------------------------------------------------------------------------------

# more checks

## don't overlap
pgrep -F  /run/btrfs-sync.pid  &>/dev/null && { echo "$BIN is already running"; exit 1; }
echo $$ > /run/btrfs-sync.pid

${DST_CMD[@]} "pgrep -f btrfs\ receive &>/dev/null" && { echo "btrfs-sync already running at destination"; exit 1; }

## src checks
echov "* Check source"
while read entry; do SRCS+=( "$entry" ); done < <(
  "${SRC_CMD[@]}" "
    for s in "${SRC[@]}"; do
      src=\"\$(cd \"\$s\" &>/dev/null && pwd)\" || { echo \"\$s not found\"; exit 1; } #abspath
      btrfs subvolume show \"\$src\" &>/dev/null && echo \"0|\$src\" || \
      for dir in \$( ls -drt \"\$src\"/* 2>/dev/null ); do
        DATE=\"\$( btrfs su sh \"\$dir\" 2>/dev/null | grep \"Creation time:\" | awk '{ print \$3, \$4 }' )\" \
        || continue   # not a subvolume
        SECS=\$( date -d \"\$DATE\" +\"%s\" )
        echo \"\$SECS|\$dir\"
      done
    done | sort -V | sed 's=.*|=='
  "
)
[[ ${#SRCS[@]} -eq 0 ]] && { echo "no BTRFS subvolumes found"; exit 1; }

## check pbzip2
[[ "$ZIP" == "pbzip2" ]] && {
    "${SRC_CMD[@]}" type pbzip2 &>/dev/null && \
    "${DST_CMD[@]}" type pbzip2 &>/dev/null || {
      echo "INFO: 'pbzip2' not installed on both ends, fallback to 'xz'"
      ZIP=xz PIZ=unxz
  }
}

## use 'pv' command if available
PV=( pv -F"time elapsed [%t] | rate %r | total size [%b]" )
[[ "$QUIET" == "1" ]] && PV=( cat ) || type pv &>/dev/null || {
  echo "INFO: install the 'pv' package in order to get a progress indicator"
  PV=( cat )
}

#----------------------------------------------------------------------------------------------------------

# sync snapshots

get_dst_snapshots() {      # sets DSTS DST_UUIDS
  local DST="$1"
  unset DSTS DST_UUIDS
  while read entry; do
    DST_UUIDS+=( "$( sed 's=|.*==' <<<"$entry" )" )
    DSTS+=(      "$( sed 's=.*|==' <<<"$entry" )" )
  done < <(
    "${DST_CMD[@]}" "
      DSTS=( \$( ls -d \"$DST\"/* 2>/dev/null ) )
      for dst in \${DSTS[@]}; do
        UUID=\$( sudo btrfs su sh \"\$dst\" 2>/dev/null | grep 'Received UUID' | awk '{ print \$3 }' )
        [[ \"\$UUID\" == \"-\" ]] || [[ \"\$UUID\" == \"\" ]] && continue
        echo \"\$UUID|\$dst\"
      done"
  )
}

choose_seed() {      # sets SEED
  local SRC="$1"

  SEED="$SEED_NEXT"
  if [[ "$SEED" == "" ]]; then
    # try to get most recent src snapshot that exists in dst to use as a seed
    local RXID_CALCULATED=0
    declare -A PATH_RXID DATE_RXID SHOWP RXIDP DATEP
    local LIST="$( "${SRC_CMD[@]}" sudo btrfs subvolume list -su "$SRC" )"
    SEED=$(
      for id in "${DST_UUIDS[@]}"; do
        # try to match by UUID
        local PATH_=$( awk "{ if ( \$14 == \"$id\" ) print \$16       }" <<<"$LIST" )
        local DATE=$(  awk "{ if ( \$14 == \"$id\" ) print \$11, \$12 }" <<<"$LIST" )

        # try to match by received UUID, only if necessary
        [[ "$PATH_" == "" ]] && {
          [[ "$RXID_CALCULATED" == "0" ]] && { # create table during the first iteration if needed
            local PATHS=( $( "${SRC_CMD[@]}" sudo btrfs su list -u "$SRC" | awk '{ print $11 }' ) )
            for p in "${PATHS[@]}"; do
              SHOWP="$( "${SRC_CMD[@]}" sudo btrfs su sh "$( dirname "$SRC" )/$( basename "$p" )" 2>/dev/null )"
              RXIDP="$( grep 'Received UUID' <<<"$SHOWP" | awk '{ print $3     }' )"
              DATEP="$( grep 'Creation time' <<<"$SHOWP" | awk '{ print $3, $4 }' )"
              [[ "$RXIDP" == "" ]] && continue
              PATH_RXID["$RXIDP"]="$p"
              DATE_RXID["$RXIDP"]="$DATEP"
            done
            RXID_CALCULATED=1
          }
          PATH_="${PATH_RXID["$id"]}"
           DATE="${DATE_RXID["$id"]}"
        }

        [[ "$PATH_" == "" ]] || [[ "$PATH_" == "$( basename "$SRC" )" ]] && continue

        local SECS=$( date -d "$DATE" +"%s" )
        echo "$SECS|$PATH_"
      done | sort -V | tail -1 | cut -f2 -d'|'
    )
  fi
}

exists_at_dst() {
  local SHOW="$( "${SRC_CMD[@]}" sudo btrfs subvolume show "$SRC" )"

  local SRC_UUID="$( grep 'UUID:' <<<"$SHOW" | head -1 | awk '{ print $2 }' )"
  grep -q "$SRC_UUID" <<<"${DST_UUIDS[@]}" && return 0;

  local SRC_RXID="$( grep 'Received UUID' <<<"$SHOW"   | awk '{ print $3 }' )"
  grep -q "^-$"       <<<"$SRC_RXID"       && return 1;
  grep -q "$SRC_RXID" <<<"${DST_UUIDS[@]}" && return 0;

  return 1
}

## sync incrementally
sync_snapshot() {
  local SRC="$1"
  "${SRC_CMD[@]}" test -d "$SRC" || return

  exists_at_dst "$SRC" && { echov "* Skip existing '$SRC'"; return 0; }

  choose_seed "$SRC"  # sets SEED

  # incremental sync argument
  [[ "$SEED" != "" ]] && {
    local SEED_PATH="$( dirname "$SRC" )/$( basename $SEED )"
    "${SRC_CMD[@]}" test -d "$SEED_PATH" &&
      local SEED_ARG=( -p "$SEED_PATH" ) || \
      echo "INFO: couldn't find $SEED_PATH. Non-incremental mode"
  }

  # do it
  echo -n "* Synchronizing '$src'"
  [[ "$SEED_ARG" != "" ]] && echov -n " using seed '$SEED'"
  echo "..."

  "${SRC_CMD[@]}" \
  sudo btrfs send -q ${SEED_ARG[@]} "$SRC" \
    | "$ZIP" \
    | "${PV[@]}" \
    | "${DST_CMD[@]}" "${PIZ[@]} | sudo btrfs receive \"$DST\" 2>&1 |(grep -v -e'^At subvol ' -e'^At snapshot '||true)" \
    || {
      "${DST_CMD[@]}" sudo btrfs subvolume delete "$DST"/"$( basename "$SRC" )" 2>/dev/null
      return 1;
    }

  # update DST list
  DSTS+=("$DST/$( basename "$SRC" )")
  DST_UUIDS+=("$SRC_UUID")
  SEED_NEXT="$SRC"
}

#----------------------------------------------------------------------------------------------------------

# sync all snapshots found in src
echov "* Check destination"
get_dst_snapshots "$DST" # sets DSTS DST_UUIDS
for src in "${SRCS[@]}"; do
  sync_snapshot "$src" && RET=0 || RET=1
  for i in $(seq 1 2); do
    [[ "$RET" != "1" ]] && break
    echo "* Retrying '$src'..."
    sync_snapshot "$src" && RET=0 || RET=1
  done
  [[ "$RET" == "1" ]] && { echo "Abort"; exit 1; }
done

#----------------------------------------------------------------------------------------------------------

# retention policy
[[ "$KEEP" != 0 ]] && \
  [[ ${#DSTS[@]} -gt $KEEP ]] && \
  echov "* Pruning old snapshots..." && \
  for (( i=0; i < $(( ${#DSTS[@]} - KEEP )); i++ )); do
    PRUNE_LIST+=( "${DSTS[$i]}" )
  done && \
  ${DST_CMD[@]} sudo btrfs subvolume delete "${PRUNE_LIST[@]}" $SILENT

# delete flag
[[ "$DELETE" == 1 ]] && \
  for dst in "${DSTS[@]}"; do
    FOUND=0
    for src in "${SRCS[@]}"; do
      [[ "$( basename $src )" == "$( basename $dst )" ]] && { FOUND=1; break; }
    done
    [[ "$FOUND" == 0 ]] && DEL_LIST+=( "$dst" )
  done
[[ "$DEL_LIST" != "" ]] && \
  echov "* Deleting non existent snapshots..." && \
  ${DST_CMD[@]} sudo btrfs subvolume delete "${DEL_LIST[@]}" $SILENT

exit 0

# License
#
# This script 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 2 of the License, or
# (at your option) any later version.
#
# This script 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 script; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place, Suite 330,


#!/bin/bash

#
# Script that creates BTRFS snapshots, manually or from cron
#
# Usage:
#          sudo btrfs-snp  <dir> (<tag>) (<limit>) (<seconds>) (<destdir>)
#
# Copyleft 2017 by Ignacio Nunez Hernanz <nacho _a_t_ ownyourbits _d_o_t_ com>
# GPL licensed (see end of file) * Use at your own risk!
#
# Based on btrfs-snap by Birger Monsen
#
# More at https://ownyourbits.com
#

function btrfs-snp()
{
  local   BIN="${0##*/}"
  local   DIR="${1}"
  local   TAG="${2:-snapshot}"
  local LIMIT="${3:-0}"
  local  TIME="${4:-0}"
  local   DST="${5:-.snapshots}"
  local MARGIN=15 # allow for some seconds of inaccuracy for cron / systemd timers

  ## usage
  [[ "$*" == "" ]] || [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]] && {
    echo "Usage: $BIN <dir> (<tag>) (<limit>) (<seconds>) (<destdir>)

  dir     │ create snapshot of <dir>
  tag     │ name the snapshot <tag>_<timestamp>
  limit   │ keep <limit> snapshots with this tag. 0 to disable
  seconds │ don't create snapshots before <seconds> have passed from last with this tag. 0 to disable
  destdir │ store snapshot in <destdir>, path absolute or relative to <dir>

Cron example: Hourly snapshot for one day, daily for one week, weekly for one month, and monthly for one year.

cat > /etc/cron.hourly/$BIN <<EOF
#!/bin/bash
/usr/local/sbin/$BIN /home hourly  24 3600
/usr/local/sbin/$BIN /home daily    7 86400
/usr/local/sbin/$BIN /home weekly   4 604800
/usr/local/sbin/$BIN /     weekly   4 604800
/usr/local/sbin/$BIN /home monthly 12 2592000
EOF
chmod +x /etc/cron.hourly/$BIN"
    return 0
  }

  ## checks
  local SNAPSHOT=${TAG}_$( date +%F_%H%M%S )

  [[ ${EUID} -ne 0  ]] && { echo "Must be run as root. Try 'sudo $BIN'"; return 1; }
  [[ -d "$SNAPSHOT" ]] && { echo "$SNAPSHOT already exists"            ; return 1; }

  mount -t btrfs | cut -d' ' -f3 | grep -q "^${DIR}$" || {
    btrfs subvolume show "$DIR" &>/dev/null || {
      echo "$DIR is not a BTRFS mountpoint or snapshot"
      return 1
    }
  }

  [[ "$DST" = /* ]] || DST="$DIR/$DST"
  mkdir -p "$DST"
  local SNAPS=( $( ls -d "$DST/${TAG}_"* 2>/dev/null ) )

  ## check time of the last snapshot for this tag
  [[ "$TIME" != 0 ]] && [[ "${#SNAPS[@]}" != 0 ]] && {
    local LATEST=$( sed -r "s|.*_(.*_.*)|\\1|;s|_([0-9]{2})([0-9]{2})([0-9]{2})| \\1:\\2:\\3|" <<< "${SNAPS[-1]}" )
    LATEST=$( date +%s -d "$LATEST" ) || return 1

    [[ $(( LATEST + TIME )) -gt $(( $( date +%s ) + MARGIN )) ]] && { echo "No new snapshot needed for $TAG in $DIR"; return 0; }
  }

  ## do it
  btrfs subvolume snapshot -r "$DIR" "$DST/$SNAPSHOT" || return 1

  ## prune older backups
  [[ "$LIMIT" != 0 ]] && \
  [[ ${#SNAPS[@]} -ge $LIMIT ]] && \
    echo "Pruning old snapshots..." && \
    for (( i=0; i <= $(( ${#SNAPS[@]} - LIMIT )); i++ )); do
      btrfs subvolume delete "${SNAPS[$i]}"
    done

  echo "snapshot $SNAPSHOT generated"
}

btrfs-snp "$@"

# License
#
# This script 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 2 of the License, or
# (at your option) any later version.
#
# This script 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 script; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place, Suite 330,
# Boston, MA  02111-1307  USA

List Ubuntu Version

When logging into a system it generally would show you the current version in a MOTD style window. This server had the MOTD changed so I needed to grab the pertinent information.

lsb_release -d
cat /etc/issue

Or on newer systems (16.04 or later)

cat /etc/os-release
hostnamectl