Categorie archieven: Scripts

Allerhande scripts

Scheduling jobs

Vanuit de Linux wereld bestaat de term ‘cron’. Deze term komt vanuit het oude Grieks en het betekend tijd. Vanuit oudsher was op Linux de mogelijkheid om op een paar verschillende manieren een scheduled job te starten, zijnde een table (vandaar crontab):

  • user cron
  • /etc/crontab
  • /etc/cron.d/<bestand>
  • /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly

Op deze verschillende plekken, met uitzondering van de eerste, eenvoudig qua syntax een job starten

# minute hour day month day-of-week username command/script
1 12 * * 1 j.doe /opt/myscript.py

Dus elke maandag om 1 minuut over 12 uur word onder de naam j.doe het script uitgevoerd. Het enige verschil met de user cron (crontab -e (edit) of crontab -l (list) is dat de username niet opgevoerd hoeft te worden omdat het in die userspace opereert.

Sinds al een lange poos is de Linux wereld over aan het stappen naar systemd (nouja, is al overgestapt). Deze architectuur en manier van werken is in diverse opzichten een flinke verbetering van de oude init.d scripts/services en dus crontab. Nu is dat geen nieuwtje maar het gebeurt gewoon te makkelijk om “even een crontabje” te zetten.

Systemd staat je toe zelf eenvoudig services te schrijven maar ook targets én timers. De timer functie zoom ik iets meer op in, daar ik vind het een veel betere plek is voor scheduled jobs. Hier komen namelijk ook meer voordelen bij, te denken aan logging/auditing, controle van de jobs, error afhandeling en complexe situaties om pas te starten als ‘iets’ beschikbaar is, of nadat een ander proces succesvol is gestart. De oude cron methode is meer als UDP op het netwerk: fire and forget.

Een voorbeeld is een soort disk-check die elk half uur de vrije ruimte opvraagt en boven, zeg, 75% gebruik een email uitstuurt naar de beheerder. Dit kan eenvoudig in een cron maar we maken er nu een service van.

/usr/lib/systemd/system/diskcheck.service

[Unit]
Description=Disk space check

[Service]
ExecStart=/usr/local/bin/syschk -d

De timer binnen systemd. Deze naam moet overeenkomen met de service.

/usr/lib/systemd/system/diskcheck.timer

[Unit]
Description=Disk space check every 30 minutes

[Timer]
OnCalendar=*:0/30
Persistent=true

[Install]
WantedBy=timers.target

En deze timer schakelen we in: systemctl daemon-reload && systemctl enable diskcheck.timer

Om te zien of alles goed werkt kan je eenvoudig een paar commando’s uitvoeren:

  • systemctl status diskcheck.service
  • systemctl status diskcheck.timer

En zo heb je dus een heel simpel iets als nog heel simpel gemaakt binnen de huidige systemd wereld met voordelen van logging en uiteraard als je wil dat de service uit gezet moet worden:

  • systemctl disable diskcheck.timer

Een andere optie is een service te maken die alleen actief is bij opstarten (on-boot). Dit voorbeeld start een script (met 2 command line opties) dat pas start als het netwerk online is én postfix gestart is. Waarom? Als de ‘syschk’ iets afwijkends detecteert moet het een email kunnen uitsturen.

[Unit]
Description=HouseKeeping at boot
After=network-online.target
After=postfix.service

[Service]
Type=simple
ExecStart=/usr/local/bin/syschk -bp

[Install]
WantedBy=multi-user.target

Met name de After regels zijn in dit geval waarmee het gestuurd word.

Als je besluit dat het een background daemon of iets dergelijk moet worden, dan kan dat ook op deze manier maar dan moet je in jouw proces of script wel het altijd aan laten. Denk aan een lowlevel ‘while true’ loop (maar denk aan je cpu cylcles ;).

Het nakijken van statussen en logs is bijvoorbeeld met eerder genoemde systemctl commando. Echter is er ook vanuit het Linux eco systeem een andere optie: journalctl.

  • journalctl -eu diskcheck.service

De opties zijn flink en de gekozen 2 zijn eenvoudig de eerste om mee te beginnen. De ‘-e’ is pager-end, of wel het laatste stukje en de ‘-u’ is de betreffende unit. In dit voorbeeld de eerder gemaakte diskcheck.service.

Uiteraard zijn er veel meer opties met journalctl om in te zoomen op bijv. tijdsvakken en nog meer. Zie ook de ‘manpages’.

Zo makkelijk als het lijkt om een service, met timer of niet, te maken is, een veelal vergeten aspect, natuurlijk wel even denken aan veiligheid. De systemd services kunnen en mogen veel, ‘out-of-the-box’. Dat is voor een lokaal servertje thuis niet direct een probleem maar een webserver of waar gevoelige data verwerkt kan worden is dan een interessant object. Als je daar, op welke manier dan ook, gebruik kan maken van zwakheden dan is dat als kwaadwillende top. Ik ga hier in dit artikel niet dieper op in maar verdiep gerust even in de materie:

https://linux-audit.com/systemd/systemd-features-to-secure-units-and-services

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?

Netconf maar dan EasZy

In netwerk land zijn diverse manieren om je netwerk device te beheren. Old-skool via CLI, via SNMP, via een GUI of zelfs een API. Het kan redelijk alle kanten op.

De meest praktische manier in hedendaagse tijd is middels Netconf. Dit communiceert via RPC calls en gebruikt XML voor communicatie standaarden.

Hoewel er zat devices zijn met allemaal hun eigen nukken, netconf praten ze. Alleen niet in een uniforme manier. Dat is op zich wel recht te breien maar dan met een eigen soort ‘API’ in Python. Hoewel daar al packages zijn (zoals Napalm) is het goed om zelf ook eens te kijken hoe je het voor elkaar krijgt om de denkwijze goed te kunnen begrijpen. Vanuit presentaties van een core python developer (R. Hettinger) is de denkwijze dat een Class in Python eigenlijk als een API zou moeten werken. Dat klinkt goed. Waarom zou je meerdere wegen per type device bewandelen als het ook abstracter kan.

Laten we vast stellen dat er een netwerk is met een aantal devices, elke met hun eigen afwijking. Je wilt vanuit jouw programma eigenlijk van al deze devices:

  1. uniform verbinden
  2. data op 1 manier opvragen
  3. data op 1 manier terug krijgen

In dit voorbeeld ga ik dieper in op een Juniper omgeving maar de basis is hetzelfde. Belangrijkste is om de strategie vooraf te bepalen.

De strategie is als volgt:

  1. Uniform verbinden
    • Router? Switch? Firewall? Allen kunnen geclassificeerd worden als ‘JuniperDevice’
      • Parent Class: verbind met device, error afhandeling, context manager
    • Per device type onderscheid maken om specifieke zaken te behandelen
      • Child Class: definieer data bronnen als properties
  2. Data op 1 manier opvragen
    • Interfaces? Mac adressen? Vlans? De methode zou uniek moeten zijn
      • Opvragen via middels Yaml templates
  3. Data op 1 manier terug krijgen
    • Vaste data structuur
      • Teruggeven van XML object

Laten we de parent class gaan starten:

class JuniperDevice:
    """Parent Class to connect in a single way to a netconf-enabled Juniper device."""
    vendor = 'Juniper'

    def __init__(self, **kwargs):

Meer is niet nodig om de platte basis neer te zetten. Maar dit doet nog niets. In de ‘init’ fase moeten o.a. hostname, credentials en meer opgetuigd worden. Ik begin met een 2-tal basis zaken. Een Juniper device, met name EX en SRX willen nog wel eens verschillen in type (ELS of niet). Daarnaast wil ik een hostname hebben, via het keyword argument ‘hostname’:

        self.facts = None
        self.hostname = kwargs.get('hostname', None)
        if not isinstance(self.hostname, str):
            raise TypeError("No hostname provided or not a string")

Hiermee is nog geen verbinding gestart. Dat gaat via de packages vanuit PyEZ (pip install junos-eznc). Dit gaan we invoeren, halen direct de ‘facts’ erbij voor bepaling ELS of niet, en wat error afhandeling. Belangrijk is het via pyez opstarten van de connectie middels de Device instance:

from jnpr.junos import Device
from jnpr.junos import exception
...
    def __init__(self, **kwargs):
        ...
        try:
            self.device = Device(host=self.hostname, user='johndoe', password='my_very_secret')
            self.device.open()
            self.facts = self.device.facts
            try:
                if self.facts['switch_style'] == 'VLAN_L2NG':
                    self.is_els_type = True
            except KeyError:
                raise RuntimeError(f'Can not determine switch_style for {self.hostname}')
        except (exception.ConnectRefusedError, exception.ConnectTimeoutError, exception.ConnectUnknownHostError,
                exception.ProbeError, exception.ConnectError):
            raise ConnectionError

We zouden nu deze class kunnen aanroepen, zoals ‘dev = Juniperdevice()’ maar dat is nog niet alles. Het kan maar wat handigheid inbouwen heeft de voorkeur. Dus we kunnen de class zo opbouwen dat het ook automatisch de connectie sluit. De class zal dus ook een context manager worden. Hiervoor zijn 2 class methodes nodig (of in python: dunder) genaamd ‘enter’ en ‘exit’:

...
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.device.close()
        return True

Laten we naast deze ook een property invoeren in de parent class, voor het gemak. De ‘facts’ hebben we tenslotte al opgehaald, dus kunnen we die ook terug geven (of opgemaakt presenteren, jouw keuze):

    @property
    def get_facts(self):
        return self.facts

Met al deze stukjes is in principe de parent class voor nu klaar. Er zal voor het mooie nog een dunder ‘__repr__’ in mogen en een paar andere zaken maar het gaat om het grotere geheel.

Nu de parent class gedefinieerd is en we op een uniforme manier kunnen verbinden naar een Juniper netconf-enabled device. We zouden in het hoofd programma dus kunnen starten met:

def main():
    with JuniperDevice(hostname='coolswitch') as dev:
        print(dev.facts)

Nu kunnen we de diepte in en specifiekere Juniper devices inrichten. We starten een nieuwe class maar met een inherit van JuniperDevice:

class JuniperSwitch(JuniperDevice):
    """Child Class to present data from a JuniperDevice"""
    type = 'Switch'

Nu zouden we in het hoofd programma dus ook dit kunnen doen:

def main():
    with JuniperSwitch(hostname='coolswitch') as dev:
        print(dev.facts)
        print(f'I am a {dev.vendor} and I am a {dev.type}')

Maar dat is niet alles. We zouden nu dan ook de stap kunnen maken om een RPC call te maken en dat gaan we via een YAML template doen.

Dit doen we voor nu om het behapbaar te houden (niet via automatisch ingeladen .yml files en dergelijke in package modules verstopt) even in de class file zelf.

We voegen een aantal imports toe en plaatsen een YAML template. Daarna maken in de JuniperSwitch class een property aan. Deze property vraagt in dit geval een lijst op van Ethernet Ports op en geeft deze terug.

from jnpr.junos.factory.factory_loader import FactoryLoader
import yaml

etherports_yaml = """
---
EtherPortsTable:
    rpc: get-interface-information
    item: physical-interface
    key: name
    view: EtherPortsTableView

EtherPortsTableView:
    fields:
        name: name
        admin_status: admin-status
        oper_status: oper-status
"""

globals().update(FactoryLoader().load(yaml.load(etherports_yaml, Loader=yaml.FullLoader)))

class JuniperSwitch(JuniperDevice):
    ...
    @property
    def eth_ports_table(self):
        return EtherPortsTable(self.device).get()

Hiermee zouden we dus al kunnen zeggen dat we kunnen verbinden en een overzicht kunnen raadplegen vanuit de switch met ethernet poorten. Het hoofdprogramma zou dan dit kunnen doen:

def main():
    with JuniperSwitch(hostname='coolswitch') as dev:
        print(dev.facts)
        print(f'I am a {dev.vendor} and I am a {dev.type}')
        for port in dev.eth_ports_table:
            print(f'{port.name}')

Het XML object wat vanuit de property method terug komt heeft een aantal velden die we kunnen aanspreken. Deze zijn gelijk aan die in de YAML template. Dus met de ‘for’ loop kunen we dus port.name opvragen maar ook port.oper_status.

Stel dat je een set aan Juniper EX switches hebt die wel en geen ELS type zijn, dan kan je differentiatie maken tussen deze. Enige wat moet is 2 verschillende YAML templates inladen (1 voor ELS, 1 voor normaal) en dan in de property hierop de juiste opvragen en terug geven (even zonder YAML template):

class JuniperSwitch(JuniperDevice):
    ...
    @property
    def mac_table(self):
        if self.is_els_type:
            return ElsEtherSwTable(self.device).get()
        return EtherSwTable(self.device).get()

Een Juniper Router toevoegen met zijn eigen ‘views’ is dus dan redelijk easy. Een extra property toevoegen is een kwestie van een YAML template en terug geven. Je kan dus een soort API maken met alle mogelijke views. Daarna kan je in je hoofd progamma met deze informatie aan de slag.

Het geheel voor nu zou er dus zo uitzien (zonder bovengenoemde mac_table property:

from jnpr.junos import Device
from jnpr.junos import exception
from jnpr.junos.factory.factory_loader import FactoryLoader
import yaml

etherports_yaml = """
---
EtherPortsTable:
    rpc: get-interface-information
    item: physical-interface
    key: name
    view: EtherPortsTableView

EtherPortsTableView:
    fields:
        name: name
        admin_status: admin-status
        oper_status: oper-status
"""

globals().update(FactoryLoader().load(yaml.load(etherports_yaml, Loader=yaml.FullLoader)))


class JuniperDevice:
    """Parent Class to connect in a single way to a netconf-enabled Juniper device."""
    vendor = 'Juniper'

    def __init__(self, **kwargs):
        self.facts = None
        self.hostname = kwargs.get('hostname', None)
        if not isinstance(self.hostname, str):
            raise TypeError("No hostname provided or not a string")
        try:
            self.device = Device(host=self.hostname, user='johndoe', password='my_very_secret')
            self.device.open()
            self.facts = self.device.facts
            try:
                if self.facts['switch_style'] == 'VLAN_L2NG':
                    self.is_els_type = True
            except KeyError:
                raise RuntimeError(f'Can not determine switch_style for {self.hostname}')
        except (exception.ConnectRefusedError, exception.ConnectTimeoutError, exception.ConnectUnknownHostError,
                exception.ProbeError, exception.ConnectError):
            raise ConnectionError

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.device.close()
        return True

    @property
    def get_facts(self):
        return self.facts


class JuniperSwitch(JuniperDevice):
    """Child Class to present data from a JuniperDevice"""
    type = 'Switch'

    @property
    def eth_ports_table(self):
        return EtherPortsTable(self.device).get()


def main():
    with JuniperSwitch(hostname='coolswitch') as dev:
        print(dev.facts)
        print(f'I am a {dev.vendor} and I am a {dev.type}')
        for port in dev.eth_ports_table:
            print(f'{port.name}')


if __name__ == '__main__':
    main()

Het zo abstract mogelijk maken is redelijk simpel. Als je uiteindelijk de herhaaldelijke patronen gaat zien en je dus de onderdelen (JuniperDevice vs JuniperSwitch) goed uitdenkt. Vaak kom je tot deze inzichten als je eenmaal verder gaat zoals in de alinea hierboven ook uitgeduid word.

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.