Tag archieven: shell

Alles Python

Ik heb al een hele tijd niets geschreven. Na een stapel avonden de vorige webserver (LAMP-stack) vervangen te hebben van CentOS 7 naar AlmaLinux 9 (daar lees je dit nu op) ben ik toch weer begonnen met alles moet in python. Een paar kleine ’tooltjes’, of wel scripts, omzetten en weer actiever zijn in deze scripting taal.

Vanuit mijn vorige werk bij een grote service/cloud provider had ik altijd de neiging om veel bash/shell scriptjes te maken. Of one-liners, omdat het kon. Dat was in de tijd van CentOS4/5, toen Python nog een beetje gek aanvoelde. Nu bij mij huidige werkgever (ook een cloud provider) probeer ik iedereen ook te bewegen handig te worden met basis tools uit de GNU-stal. Maar met een duidelijke kanttekening. Moet je meer dan 6 ofzo regels in bash/shell schrijven, dan ben je eigenlijk al te ver. Bij de derde zoekopdracht op Internet moet je dus ook al bedenken dat je te ver gaat.

Python kan zoveel out-of-the box al. De eigen modules die je kan importeren dekken een heleboel. Denk aan sys, os, re, argparse, math en nog veel meer. Natuurlijk, er zijn gevallen waarbij het niets oplevert. Of toch wel? Ik ben van mening dat het altijd helpt. Het houd je actief bezig met de Python scripting taal.

Een aantal voorbeeld

Ik had bijvoorbeeld een klein scriptje gemaakt dat de load van het linux systeem toonde. Ja met uptime zie je het ook, maar ergens dacht ik; dat kan korter. De werkelijke getallen komen uit een bestand /proc/loadavg en dat is dus eigenlijk kort te maken met een alias of letterlijk een bash-script in bijvoorbeeld /usr/local/bin/

#!/usr/bin/env bash
awk '{OFS=", "; print $1, $2, $3}' /proc/loadavg

Maar ja, dit is eenvoudig in Python te doen

#!/usr/bin/env python3
with open('/proc/loadavg', 'r') as f:
    line = f.read().strip().split()
output = ', '.join(line[:3])
print(f'{output}')

Een ander voorbeeld. Ik had voorheen een eigen RPM repository voor CentOS 7. Hierin zaten, jawel, 2 packages. Een heet telebot, de andere postqmon. De telebot package is plat gezegd een bash-script dat een /etc/telebot.conf verwacht met daarin een Telegram bot token en chat id. Daarna doet het, als alle checks gedaan zijn, een POST met cURL naar de Telegram API. En eerder al, in hetzelfde gedachtegoed, heb ik een Python repository online gezet (Devpi) met daarin het equivalent van de bash telebot.

De tegenhanger in Python is wat uitgebreider. Deze kijkt niet alleen naar /etc/telebot.conf maar ook naar lokale ‘user’ config file locaties zoals ~/.config/. Dit is ook uitbreidbaar. Nu is het stuk configuratie file uitlezen met de re.findall() misschien wat omslachtig. Het kan ook met confparse , die ‘.ini’-like files ook parsed. En alles draait als Class, is abstract en het houd je Python kennis weer up-to-date.

Een one-liner die te gek word maar misschien nog net kan?

for USER in $(grep -vP '/sbin/nologin|/bin/sync|/sbin/shutdown|/sbin/halt' /etc/passwd | cut -d ':' -f1); do sudo passwd -S $USER | grep -oP 'LK' 2>&1 > /dev/null && printf "%s is locked\n" $USER || printf "%s login active\n" $USER; done

Natuurlijk houd niemand je tegen met een text file als dit, een soort Cisco vlan-database, in vlans.txt:

vlan 123
vlan 456
vlan 789
vlan 1234
vlan 2345

En dan omzetten naar Juniper

for v in $(awk '{print $2}' vlans.txt); do printf "set vlans vlan%s vlan-id %s\n" $v $v; done

Output;

set vlans vlan123 vlan-id 123
set vlans vlan456 vlan-id 456
set vlans vlan789 vlan-id 789
set vlans vlan1234 vlan-id 1234
set vlans vlan2345 vlan-id 2345

Maar het kan ook in Python!

with open('vlans.txt', 'r') as f:
    lines = f.readlines()
for line in lines:
    line_parts = line.split()
    vlan_id = linepart[1]
    print(f'set vlans vlan{vlan_id} vlan-id {vlan_id}')

En zo zijn er vast nog meer gevallen.

Conclusie

Alles naar Python. En het liefst naar >3.8 of nieuwer. Niet alles in bash/shell-scripts proberen. Ja, voor one-liners blijft het krachtig, dat kan ik niet ontkennen. Maar hou het dan bij one-liners. Een snelle actie om een paar filteringen te doen op een text file? Awk/grep/sed/cut zijn je vrienden.

Maar moet je het in een for-loop doen, dan kom je vast tot het punt dat je het niet meer knap kan lezen en je fouten in syntax moet oplossen. Dan moet je, als je een editor als VIM opent eigenlijk al afvragen; moet ik het niet gelijk in Python doen?

Dan is het antwoord ja! De ervaring leert, dat als je al langer dan 5 minuten bezig bent je one-liner met een loop te corrigeren en knap te krijgen, dat je al te lang bezig bent: stop dan.

Probeer jezelf te bewegen om alles in Python te doen, hoe klein het misschien ook is. Eenmaal wat meer thuis in de basis, kan je verder naar grotere projecten, wellicht met Classes, wellicht met eigen modules die je importeert?

Automatisch lichten aanzetten met behulp van een API

Ooit, een hele tijd terug heb ik een APC rack pdu (ap7921) in huis gehaald om 2 dingen te doen. Het meten van het totale verbruik van de mediahoek én het in/uitschakelen van lichten of andere zaken. Dat kan prima met deze pdu, want er zit netwerk op en dus ook via SNMP aan te sturen. Via een php webpagina (die ik met mijn mobile device kan openen) kan ik al heel lang de belangrijkste outlets schakelen en ik heb ook al een tijd een cronjob op een server draaien die de lichten schakelen. Ja, ook de kerstboom hoewel het stekkerblok op die outlet nu onder de bank ligt voor algemeen gebruik. De subwoofer was voor het gemak, tijdens een film licht uit, sub aan en geniet.

De pagina stuurt in feite ook via een ‘snmpset’ commando een 1 of 2 naar de juiste poort. Daarbij is de kleur groen ‘aan’ en rood is ‘uit’ puur voor de opmaak.

Het script in cronjob is een paar keer herschreven en kwijt geraakt door een crash van de OrangePi thuis en dus weer opnieuw gemaakt. Voor nu is het een ‘fancy’ versie want het kan uiteraard ook gewoon met praktisch 8 of zo regels. Maar dat is het mooie, je wil leren en je script verder ontwikkelen, verharden. Een geval van “good practice” dus.

#!/bin/bash#
# Switching on or off light at home
# bartjan@pc-mania.nl - 08 Mar 2018

snmp_exec="/usr/bin/snmpset"

function check_reqs {
        if [ ! -f $snmp_exec ]; then
                echo "Error: snmpset executable not found.."
                echo ""
                exit 1
        fi
}

function show_help {
        echo "Error: incorrect syntax.."
        echo "Usage: ./lights.sh [on|off]"
        echo ""
}

function switch {
        # 1st arg is outlet nr (integer)
        # 2nd arg options: on / off
        if [ $2 == "on" ]; then
                ${snmp_exec} -v2c -cpublic 192.168.1.251 1.3.6.1.4.1.318.1.1.4.4.2.1.3.${1} i 1 1>/dev/null 2>&1
        elif [ $2 == "off" ]; then
                ${snmp_exec} -v2c -cpublic 192.168.1.251 1.3.6.1.4.1.318.1.1.4.4.2.1.3.${1} i 2 1>/dev/null 2>&1
fi
}

# Start the program

check_reqs

if [ ! $1 ]; then
        show_help
        exit 1
fi


if [ $1 == "on" ]; then
        echo "Iluminate"
        switch 1 on
        switch 8 on
elif [ $1 == "off" ]; then
        echo "Deluminate"
        switch 1 off
        switch 8 off
else
        show_help
        exit 1
fi

De output op de cli is vanzelf sprekend. De cronjob zelf voert de output naar dev/null.

Nu kwam het zo naar boven dat in de herfst de tijden van de job in cron verschuiven. Elke keer dit aanpassen is suf en dat kan slimmer. Ik ben al een ruime tijd veel meer met Python bezig en dat bevalt erg goed. Ik heb een script geschreven die tijden ophaalt via een API call bij een publieke dienst aan de hand van de fysieke locatie. Hiermee kan ik de crontab van de user ‘root’ op de OrangePi weer aanpassen. Deze gebruikt deze dus de handige module ‘python-crontab‘.

#!/usr/bin/python
#
# @author bartjan@pc-mania.nl
# Oct-6,2018

import requests
import json
import smtplib
import sys
import time
from crontab import CronTab

link = 'https://api.sunrise-sunset.org/json?lat=51.840449&lng=-4.972762?&formatted=0'
localtime = time.localtime()

def time_of(input):
    # Get the JSON output from the API
    try:
        data = requests.get(link).text
    except Exception:
        sendmail("Failed to connect to API")
        sys.exit()
    data = json.loads(data)

    # Fetch the wanted time
    try:
        time = str(data['results'][input]).split('T')[1].split('+')[0]
    except Exception:
        sendmail("No usable data is returned, keeping old time")
        sys.exit()

    return time

def reset_crontab(minute,hour):
    cron = CronTab(user='root')

    # Check and remove the old job
    try:
        for job in cron.find_command('lights.sh on'):
            job.delete()
    except Exception:
        sendmail("Old cron job not found, stopping update")
        sys.exit()

    # Set the new job at the given time
    job = cron.new(command='/bin/bash /root/scripts/lights.sh on >/dev/null 2>&1')
    job.minute.on(minute)
    job.hour.on(hour)

    # Save job
    try:
        cron.write()
    except Exception:
        sendmail("Writing new cron failed")
        sys.exit()

def sendmail(msg):
    msg = "Subject: sunset tool warning" + "\n\n" + msg
    server = smtplib.SMTP('smtp.yourisp.org',25)
    server.sendmail('you@domain.net','johndoe@somedomain.com',msg)

###

# Get new time and split in hour/minute
time = time_of('nautical_twilight_end')
hour = time.split(':')[0]
if not localtime.tm_isdst:
    # Correct daylight savings
    hour = hour - 1
minute = time.split(':')[1]

# Update (so reset) the cronjob
reset_crontab(minute,hour)

Dit script draait sowieso ook in de crontab van dezelfde user ‘root’. Dit alleen 1x per dag om 06.00, om te job voor diezelfde dag goed te zetten. De gewenste tijd is aan te passen door de parameter “time” te veranderen. De API call geeft namelijk al deze velden terug (formatted):

{
"results":
    {
        "sunrise":"6:30:51 AM",
        "sunset":"5:44:52 PM",
        "solar_noon":"12:07:51 PM",
        "day_length":"11:14:01",
        "civil_twilight_begin":"5:56:38 AM",
        "civil_twilight_end":"6:19:05 PM",
        "nautical_twilight_begin":"5:16:49 AM",
        "nautical_twilight_end":"6:58:54 PM",
        "astronomical_twilight_begin":"4:36:17 AM",
        "astronomical_twilight_end":"7:39:26 PM"
    },
    "status":"OK"
}

Of zonder format, de toevoeging ?&formatted=0 in de url zoals ik nu gebruik (scheelt een extra stapel stappen om naar 24-uurs tijd om te zetten):

{
"results":
    {
        "sunrise":"2018-10-08T06:34:22+00:00",
        "sunset":"2018-10-08T17:40:13+00:00",
        "solar_noon":"2018-10-08T12:07:17+00:00",
        "day_length":39951,
        "civil_twilight_begin":"2018-10-08T06:00:05+00:00",
        "civil_twilight_end":"2018-10-08T18:14:30+00:00",
        "nautical_twilight_begin":"2018-10-08T05:20:18+00:00",
        "nautical_twilight_end":"2018-10-08T18:54:17+00:00",
        "astronomical_twilight_begin":"2018-10-08T04:39:57+00:00",
        "astronomical_twilight_end":"2018-10-08T19:34:38+00:00"
    },
"status":"OK"
}

Dus, nu past de cronjob zichzelf aan en zorgt dat de 2 lichten rondom het tv-meubel aangaan als het begint te schemeren en ik hoef niet meer in te loggen via SSH en crontab -e te doen en de tijd per x-dagen aan te passen.

De ‘nautical_twilight’ is een andere benadering van wanneer het donker word. Lees anders even ook deze pagina voor meer informatie.

Emergency Backup op de QNAP

In mijn thuis situatie heb ik een QNAP (TS-412)  actief waarop meerdere gebruikers hun bestanden kwijt kunnen. Nu had ik laatst het idee; wat als… Ja wat als. Wat als er brand uit breekt, of wat als de mooie polder waar ik mij in bevind weer onder water komt. Dan is het nodige kwijt.

Een disaster backup, of emergency backup is dan wel wenselijk. Maar dan geen dikke media files, geen duizenden foto’s of gedownloade installatiebestanden; nee.
Documenten. De digitalisering is redelijk toegeslagen en alle documenten van papier worden langzamerhand ingescanned of opgeslagen in een directory ‘Digiarchive’.

Hier heb ik, want zelf maken is leuker, een setup voor gebouwd en een script voor gemaakt. De setup is eenvoudig:

  • 1x VPS op de eigen vCloud omgeving in Amsterdam met 300GB storage, Centos als een NFS server.
  • 1x VPN IKE/IPsec
  • 1x QNAP met SSH access

Op de NFS server moet het bestand /etc/exports gevuld worden. Hier moet verteld worden in welke map wie en hoe bij mag. In dit geval de map /var/data/vrieskist en de host 192.168.1.100 (QNAP):

/var/data/vrieskist 192.168.1.100(rw,sync,no_root_squash,no_subtree_check)

Daarna kan het script gemaakt worden in het bestand embackup.sh . Hierin zit een dependency voor het bestand to_backup. In dit bestand kan je alle directory’s of files aangeven welke je elke maand gebackuped wil hebben.

Het script werkt als volgt:

  1. Geen input bestand? Stop
  2. Directory waarin de mount moet zijn bestaat niet? Maak aan, bij fout: Stop
  3. Mount zelf bestaat niet? Maak aan, bij fout: Stop
  4. Bestaat dezelfde maand van vorig jaar? Delete deze
  5. Bestaat deze maand van dit jaar? Dan heeft de backup al  gedraaid
  6. Kopieer de direcotory’s/bestanden
#!/bin/bash

if [ ! -f /share/Bartjan/Scripts/to_backup ]; then
	# input file does not exists, failing
	echo "No input file detected, exiting..."
	exit 1
fi
	
if [ ! -d "/mnt/dcstore/vrieskist" ]; then
	# dir does not exists, try to create
	echo "Mountpoint directory does not exists"
	echo "Trying make the mountpoint directory"
	mkdir -p /mnt/dcstore/vrieskist 2> /dev/null
	if [ $? -eq 0 ]; then
		echo "Mount point directory created"
		echo ""
	else
		echo "Cannot create mount point directory" >&2
		exit 1
	fi
elif [ -d "/mnt/dcstore/vrieskist" ]; then
	echo "Mountpoint directory exists"
fi


if ! `grep -qs /mnt/dcstore/vrieskist /proc/mounts`; then
	echo "Mountpoint directory does exists"
	echo "Trying to make the mount"
	mount -t nfs 192.168.100.14:/var/data/vrieskist /mnt/dcstore/vrieskist/ 2> /dev/null
	if [ $? -eq 0 ]; then
		echo "Mount point created"
		echo ""
	else
		echo "Cannot create mount point" >&2
		exit 1
	fi
elif `grep -qs /mnt/dcstore/vrieskist /proc/mounts`; then
	echo "Mount exists"
fi


# Looks like the critical parts are in place...

LASTYEAR=`date +%b_%Y --date="1 year ago"`
THISYEAR=`date +%b_%Y`

if [ -d "/mnt/dcstore/vrieskist/$LASTYEAR" ]; then
	echo "$LASTYEAR: it exists"
	rm -rf /mnt/dcstore/vrieskist/$LASTYEAR 2> /dev/null
	if [ $? -eq 0 ]; then
		echo "Directory $LASTYEAR purged"
		echo ""
	else
		echo "Cannot remove directory $LASTYEAR" >&2
		exit 1
	fi
else
	echo "$LASTYEAR: it does not exists"
fi

if [ -d "/mnt/dcstore/vrieskist/$THISYEAR" ]; then
	echo "$THISYEAR: it exists. Probably no need to copy.."
else
	echo "$THISYEAR: does not exists"
	echo "Creating target directory"
	mkdir /mnt/dcstore/vrieskist/$THISYEAR 2> /dev/null
	if [ $? -eq 0 ]; then
		echo "Target directory created"
		echo ""
	else
		echo "Cannot create target directory" >&2
		exit 1
	fi
	echo "Copying..."
	echo ""
	while read line; do
		cp -a "$line" /mnt/dcstore/vrieskist/$THISYEAR/ 2> /dev/null
	done < /share/Bartjan/Scripts/to_backup
	if [ $? -eq 0 ]; then
		echo "Copy succesfull!"
		echo ""
	else
		echo "Failure: Cannot copy source to target directory" >&2
		exit 1
	fi
fi

exit 0

De output kan zijn:

[./Scripts] # .embackup.sh
Mountpoint directory exists
Mount exists
Mar_2016: it does not exists
Mar_2017: does not exists
Creating target directory
Target directory created

Copying..
Copy succesfull!

Maar ook het kan als volgt zijn:

[./Scripts] # .embackup.sh
Mountpoint directory does not exists
Trying make the mountpoint directory
Mount point directory created

Mountpoint directory does exists
Trying to make the mount
Mount point created

Mar_2016: it does not exists
Mar_2017: it exists. Probably no need to copy..

Het doel word hiermee simpel bereikt. Dit kan vast en zeker in bestaande software pakketten of misschien veel simpeler. Dit is nu zo gedaan vanwege de automatische restore van systeem settings op de Qnap. Na een reboot zijn mount points weg, directory’s in /mnt en zo nog een paar zaken. Het staat nu in de crontab van de QNAP dat het elke maar de eerste dag even draait.

Natuurlijk, het is ook vast vanuit de QNAP store middels een te installeren programma mogelijk naar een online cloud dienst en dat is prima. Maar ik heb nu de controle over wat, hoe en ook waar.

Deel 2: Simpel backup script voor je Linux machine

Eerder heb ik geschreven over mijn backup script. Deze zou ik nog aanpassen zodat het niet faalt als er een inputfile mist omdat er bijvoorbeeld geen SQL actief zou zijn. Hierbij dus de iteratie en daarmee het volledige script.

Met een paar simpele ‘if, else’ blokken is het uitgebreid en werkt het zonder problemen. Bij totaal geen input files word er niets gedaan dan enkel een log entry, dat er geen werk is.

Ook een $var bijgevoegd “basefolder” puur voor de handigheid.

#!/bin/bash
#
# Backup script for the daily and weekly copys
# Files to fill:
#       to_backup               Normal files, webcontent, configuration dirs. Note: no wildcards like '*'
#       to_backup_sql           SQL databases. Enter only the database name
#
# 16-06-2013: created by bartjan@pc-mania.nl
# 03-09-2016: re-done script (simplified, improved, universal, use of input files)
# 13-10-2016: checks made if input files exist

# Variables to work with

if [[ "$(date +%a)" != "Sun" ]]; then
        btype="daily"
else
        btype="weekly"
fi
servername="$(hostname -s)"
log="/var/log/backuplog"
destination="vrieskist:/share/Backups/$servername/$btype"
basefolder="/root/scripts"

printf "\n========== $btype backup started at $(date) ==========\n" >> $log

# Do the backup of files, if any
if [[ -f $basefolder/to_backup ]]; then
        while read line
        do
                data="$data $line"
        done < $basefolder/to_backup
        printf "\nCopying Webcontent and Webserver configuration files...\n\n" >> $log

        rsync -avphR --delete --log-file="$log" $data $destination
else
        nofiles=true
fi

# Do the backup of databases, if any
if [[ -f $basefolder/to_backup_sql ]]; then
        printf "\nMaking the MySQL dumps...\n" >> $log
        if [ ! -d "/tmp/mysqlbackup_tmp" ]; then
                mkdir /tmp/mysqlbackup_tmp
        fi
        while read line
        do
                mysqldump --defaults-extra-file=/root/.my.cnf $line > /tmp/mysqlbackup_tmp/$line.sql
        done < $basefolder/to_backup_sql
        printf "\nCopying Databases and MySQL configuration...\n" >> $log

        rsync -avph --delete --log-file="$log" /tmp/mysqlbackup_tmp/* $destination/mysql/

        printf "\nCleanup after SQL backup...\n" >> $log
        rm -rf /tmp/mysqlbackup_tmp
else
        nosql=true
fi

if [[ ! -z $nofiles && $nosql ]]; then
        printf "\nNothing to backup apparently...\n\n" >> $log
else
        printf "\nAll done!\n\n" >> $log
fi