Proaktive Überwachung mit Grafana und Telegram

  1. Einleitung
  2. Telegram Bot erstellen
  3. Telegram Gruppe erstellen und Bot einladen
  4. Grafana konfigurieren
  5. Alerts in Grafana definieren
  6. Weiterführende Links

Einleitung

Eine proaktive Überwachung eines Servers oder einer Platform kann mit relativ einfachen Mitteln realisiert werden. Telegram bietet mit sogenannten Bots die Möglichkeit einen Chat mit Maschinen zu erstellen. In diesem Beitrag verwende ich eine Chatgruppe, in der sich menschliche Teilnehmer austauschen können. Der Bot, welcher Teil der Gruppe ist kann aber keine Nachrichten interpretieren sondern nur selber welche schicken.

Telegram Bot erstellen

Das Erstellen eines Telegram Kontos wird bewusst in diesem Artikel nicht erwähnt. Dies kann über https://web.telegram.org/ oder die Mobile App erledigt werden.

Als erstes muss der BotFather angeschrieben werden (welcher selber ebenfalls ein Bot ist) um einen Bot erstellen zu können. Dazu kann im Kontakt Suchfeld /BotFather geschrieben, der Kontakt ausgewählt und anschliessend im Chatfenster auf Start gedrückt werden.

Darauf präsentiert der BotFather auch gleich seine Interaktions Möglichkeiten. Das Erstellen des Bots läuft interaktiv ab, wir beginnen mit /newbot
Anschliessend werden wir nach dem Namen des Bots gefragt und Usernamen, welcher auf bot enden muss. In meinem Fall ist der Name DirectoryServicesAlerts und der Username DSAlerts_bot. Als Resultat erhalten wir das Token, mit dem Grafana über diesen Bot mit uns kommunizieren kann.

Telegram Gruppe erstellen und Bot einladen

Es ist von Vorteil anstelle eines direkt Chats mit 1 Person eine Gruppe in Telegram zu erstellen, in welcher der Bot Mitglied ist. Dadurch können beliebig menschliche Mitglieder hinzugefügt und entfernt werden je nach Bedarf (z.B im Pikettfall).
Durch klicken auf das Hamburgermenü neben dem Titel Telegram kann mit New Group eine neue Gruppe angelegt werden. Bei Kontakten wird der Bot mit der Suche nach dem Username des Bots (in meinem Fall /DSAlerts_bot ) hinzugefügt. Neben unserem Bot benötigen wir einmalig auch noch den IDBot (/myidbot), welcher uns die ID für die Gruppe sagen kann. Diese wird in Grafana benötigt, damit der Bot weiss in welche Gruppe er die Mitteilungen schicken soll.

Die Gruppen ID kann anschliessend im Gruppen Chat mit /getgroupid@myidbot angezeigt werden.

Anschliessend kann der IDBot wieder aus der Gruppe entfernt werden, er wird nicht mehr benötigt.

Grafana konfigurieren

In Grafana wird über das Alerting Menü ein neuer Notification channel hinzugefügt

Der Name des Channels kann frei gewählt werden. Als Type wird Telegram ausgewählt und bei den Telegram API Settings das BOT API Token, welches uns der BotFather mitgeteilt hat, eingetragen sowie bei Chat ID diejenige welche uns der IDBot im Gruppenchat geschickt hat.

Abschliessend kann das Ganze mit Send Test überprüft werden. Sollte alles korrekt konfiguriert worden sein ist eine Test Nachricht im Gruppen Chat ersichtlich.

Alerts in Grafana definieren

In Grafana kann nun auf einem Panel im Alert Tab der gewünschte Schwellwert hinzugefügt werden, ich nehme zum testen die CPU Auslastung.

Mit folgenden Powershell Einzeiler wird die CPU auf einem Test Server zu 100% ausgelastet

(1..(GWmi Win32_Processor | Select -Expand NumberOfLogicalProcessors)) | ForEach-Object { Start-Job {while(1){}} }; Read-Host "Press any key to stop cpu load test";Stop-Job *;Remove-Job *

und schon meldet der Bot den Alarm, bzw. dessen Aufhebung

ADFS OpenId Connect für Grafana

Zum Visualisieren von Metriken gehört Grafana für mich zu den Top Werkzeugen. In einem grösseren Umfeld kann die Benutzerverwaltung jedoch schnell aufwändig werden und es bietet sich meistens die Möglichkeit einen bereits vorhandenen Identity Provider zu verwenden der die zentrale Benutzer Verwaltung zur Verfügung stellt. Als etablierte Enterprise Web Anwendung verfügt Grafana über Authentifizierungs Möglichkeiten via LDAP, OpenId Connect, SAML und weitere. In einer Cloud orientierten Welt ist natürlich eine Federations basierte Authentifizierung sehr willkommen, weshalb ich die Konfiguration mit ADFS und OpenId Connect beschreibe.

Konfiguration ADFS

Als erstes wird eine neue “Application Group” hinzugefügt. Dazu kann die Vorlage “Server application accessing a web API” verwendet werden

Die Client Id muss für die Grafana Konfiguration kopiert und bei den Redirect URIs muss die öffentliche Adresse der Grafana Instanz eingetragen werden.

Den Haken bei “Generate shared secret” setzten und den erzeugten Schlüssel ebenfalls kopieren. Dieser Schlüssel wird einmalig angezeigt und danach nie wieder. Es kann aber jederzeit ein neuer erstellt werden, dabei muss allerdings auch die Grafana Konfiguration mit dem neuen Schlüssel aktualisiert werden.

Bei Identifier wird der vorher notierte Client Identifier eingetragen.

Als Access Control Policy verwende ich die 2 Faktor Authentisierung. Diese habe ich mit einem Yubikey als 2. Faktor eingerichtet (dazu erstelle ich ebenfalls einen Artikel, wie ein MFA Adapter in ADFS hinzugefügt werden kann).

Als Scope wird “openid” ausgewählt

Konfiguration Grafana

Grafana wird über eine .ini Datei konfiguriert. Im folgenden Abschnitt sind die Einstellungen aufgelistet, welche für die OpenId Connect Authentisierung via ADFS relevant sind. Die entsprechenden URL Endpoints können über die ADFS Metadaten abgefragt werden.

Invoke-RestMethod https://<ADFS-Url>/adfs/.well-known/openid-configuration
[auth]
oauth_auto_login = true

[auth.generic_oauth]                                                                                                  
enabled = true                                                                                                        
name = ADFS                                                                                                           
allow_sign_up = true                                                                                                  
client_id = 3d163a69-ee0b-*************************                                                                      
client_secret = ***********************************                                                              
scopes = openid                                                                                                       
email_attribute_name = email                                                                                          
;email_attribute_path =                                                                                               
auth_url = https://<ADFS-Url>/adfs/oauth2/authorize/                                                            
token_url = https://<ADFS-Url>/adfs/oauth2/token/                                                               
api_url = https://<ADFS-Url>/adfs/userinfo                                                                      
allowed_domains = irbe.ch                                                                                             
;team_ids =                                                                                                           
;allowed_organizations =                                                                                              
;role_attribute_path =                                                                                                
;tls_skip_verify_insecure = false                                                                                     
;tls_client_cert =                                                                                                    
;tls_client_key =                                                                                                     
;tls_client_ca = 

Beim Anmelden wird jedoch dieser Fehler erscheinen.

Der Grund ist, dass in meiner Domäne der UPN folgendermassen zusammengesetzt ist: <Benutzer>@lab<punkt>irbe<punkt>ch. Ich möchte jedoch, dass der Benutzername der Email Adresse entspricht, was in meinem Fall <Email>@irbe<punkt>ch wäre.
Dazu wird in ADFS das Scope “allatclaims” aktiviert, welches die Access Claims in dem Identitäts Token mitliefert und somit ein custom id_token ermöglicht. Zudem muss eine Claim Regel erstellt werden, welche die Email Adresse dem Token mitgibt.

Der Autologin klappt, nachdem ich mich beim ADFS Server angmeldet habe. Allerdings hat mein Benutzer nur die Grafana “Viewer” Rolle. Hier bietet es sich ebenfalls an, die Rollenzuweisung via Gruppen im Active Directory zu steuern.

Dazu erstelle ich im Active Directory 2 Gruppen Z-Grafana-Admin und Z-Grafana-Editor und füge meinen Benutzeraccount der Admin Gruppe hinzu. Auf dem ADFS Server in den Claims Rules erstelle ich 2 Regeln, welche dem Claim das Attribut Role, basierend auf der Gruppenzugehörigkeit, auf Admin oder Editor setzten.

In der grafana.ini wird die Konfiguration folgendermassen ergänzt

[auth.generic_oauth] 
role_attribute_path = role

So kann die Rollenzuweisung mit einer Gruppenzuweisung im Active Directory erfolgen

Powershell Ternary Operator

Ich finden den Ternary Operator (Vereinfachung einer if-then-else Anweisung auf einer Zeile) sehr praktisch. Er ist leserlicher als eine simple if-then-else Anweisung, welche sich über mehrere Zeilen verteilt. Viele Sprachen haben den Ternary Operator eingebaut. Nachfolgend ein Beispiel, wenn die Variable “threshold” grösser als 80 ist wird der String “critical”, ansonsten “normal” ausgegeben.

# Konstruktion
value_if_true if condition else value_if_false

# Beispiel
threshold = 79
print('critical' if threshold > 80 else 'normal')
# >> normal
// Konstruktion
condition ? value_if_true : value_if_false

// Beispiel
#include <stdio.h>

int main() {
    int threshold = 81;
    printf("%s", threshold > 80 ? "critical" : "normal");
}
// >> critical

Obschon es den Ternary Operator für Powershell nicht gibt kann sein Verhalten “nachgebaut” werden. Dafür kann die Tatsache, dass ein boolscher Wert durch eine 0 oder eine 1 repräsentiert wird in Zusammenhang mit einem Array verwendet werden. Je nach Script / Programmiersprache ist es unterschiedlich wie ein Wahr oder ein Falsch intern abgespeichert wird. Für Powershell finden wir das mit folgendem Code heraus.

[int]$true
# >> 1
[int]$false
# >> 0

Daraus ist ersichtlich, dass im Array Index 0 der Falsch Wert stehen muss und im Index 1 der Wahr Wert. Das oben verwendete Beispiel sieht dann folgendermassen aus.

# Konstruktion
array(value_if_false,value_if_true)[condition]

# Beispiel
[int]$threshold = 79
Write-Host @('normal','critical')[$threshold -gt 80]
# >> normal

Wofür das Ganze sinnvoll eingesetzt werden kann ist der Kreativität überlassen. In einem noch folgenden Beitrag werde ich aufzeigen wofür ich es bereits verwendet habe.

Powershell Logfile Archiver

Leider gibt es heutzutage immer noch massenhaft Anwendungen, welche frisch fröhlich ihre Logfiles auf die Festplatte schreiben als gäbe es kein Morgen mehr. Dies hat zur Folge, dass der Platz auf der Festplatte immer weiter schrumpft und im ungünstigsten Fall, wenn es sich um die Systemplatte handelt, der ganze Server nicht mehr starten kann.

Um dem entgegen zu wirken habe ich ein kleines Script erstellt, welches alle Dateien welche älter als X Tage sind in komprimierten Archiven nach Monat ablegt. Die Monate wiederum in Ordner als Jahr abgelegt. Weiter löscht das Script alle Archiv Dateien, welcher älter als Y Tage sind. Somit haben wir die Logfiles welche jünger als X Tage sind im Klartext, alle die älter als X Tage sind komprimiert in Monats Archiven und alles was älter als Y Tage ist gar nicht mehr.

<#
.SYNOPSIS
  Moves Logfiles to archive packages for amount of time

.DESCRIPTION
  Moves logfiles older than specified days to monthly archives. Deletes archives older than archive retention days.

.PARAMETER Path
  Path to the folder where the logfiles are

.PARAMETER ArchivesRoot
  Path where the archives shall be stored. Defaults to $Path\Archives

.PARAMETER RetentionDays
  Number of days to skip archiving for. Defaults to 30

.PARAMETER RetentionArchiveDays
  Number of days which trigger an archive to be deleted if older. Defaults to 365

.INPUTS
  Path of the logfile folder

.OUTPUTS
  Reorganized Logfiles folder

.NOTES
  Author: Philipp R
  Version: 1.0
  Last Modify: 01.04.2020

.EXAMPLE
  .\LogArchiver.ps1 -Path C:\my\logs
#>
#Requires -Version 4

Param(
    [Parameter(Mandatory=$true, ValueFromPipeline=$true)][string]$Path,
    [string]$ArchivesRoot = "$Path\Archives",
    [int]$RetentionDays = 30,
    [int]$RetentionArchiveDays = 365
)

Begin{
    if(-not(Test-Path $Path)){
        Write-Host -ForegroundColor Red "Path not found, $Path"
        return
    }
}

Process{
    $CompressionCandidates = Get-ChildItem $Path -File | Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$RetentionDays)}
    try{
        $CompressionCandidates | 
        Group-Object -Property {$_.LastWriteTime.Year} | ForEach-Object {
            $Year = $_.Name
            $_.Group | Group-Object -Property {$_.LastWriteTime.Month} | ForEach-Object {
                $TargetArchive = "$ArchivesRoot\$Year\$($_.Name).zip"
                # Create the year folder
                $null = New-Item -ItemType Directory -Path (Split-Path $TargetArchive) -ErrorAction SilentlyContinue
                # if archive exists add it otherwise create new
                $_.Group | Compress-Archive -Update:(Test-Path $TargetArchive) -DestinationPath $TargetArchive
            }
        }
        # Only remove files if we successfully archived them
        $CompressionCandidates | Select-Object -ExpandProperty FullName |  Remove-Item  -Force
    }
    catch {
        Write-Host -ForegroundColor Red "Something strange happened"
        $_.Exception
    }

    # Remove old archives
    $DueDate = (Get-Date).AddDays(-$RetentionArchiveDays)
    Get-ChildItem $ArchivesRoot | Where-Object { [int]($_.Name) -lt [int]($DueDate.Year) } | Select-Object -ExpandProperty FullName | Remove-Item -Recurse -Force
    Get-ChildItem "$ArchivesRoot\$($DueDate.Year)" | Where-Object { [int]($_.Name.Split(".")[0]) -lt $DueDate.Month } | Select-Object -ExpandProperty FullName | Remove-Item -Recurse -Force
}

End{

}

Wifi Led Strip Selbstbau

Der Zeit schreitet voran und die Möglichkeiten vervielfältigen sich. So gerne ich Dinge von Grund auf selber entwickle um zu verstehen wie etwas funktioniert, erweitere ich mittlerweile auch gerne Bestehendes um mein Ziel zu erreichen.

So habe ich mein selbst entwickeltes Home Automation Programm (Python Web Applikation) durch das Community gestützte Projekt Homeassistant abgelöst. Die RGB Led Streifen (12V mit RF433 Empfänger) habe ich durch eine Kombination von ESP8266, WS2812 und ESPhome, die mit der FastLed Library ein super Bild erzeugen, abgelöst. Ein entsprechendes Gehäuse für den Controller habe ich mit Fusion 360 entworfen und mit meinem 3D Drucker gedruckt.

Material
Material

Zuerst habe ich 2 Dupont Stecker an das abgeschnittene USB Kabel gelötet, auf den RGB Controller aufgesteckt und das Kabel mit Sekundenkleber am Eingangsloch befestigt um eine Zugentlastung zu erhalten. Anschliessend habe ich den ESP8266 mit ESPHome geflashed (siehe Source YAML weiter unten) und eingesteckt. Deckel drauf, die Stützstruktur beim RGB Port entfernt und der Node ist bereit.

Dies ist die Yaml Datei für ESPHome. Wichtig: Die rgb_order für WS2812 ist Grün-Rot-Blau

esphome:
  name: ledstrip03
  platform: ESP8266
  board: esp01_1m

wifi:
  ssid: !secret WIFI_SSID
  password: !secret WIFI_PSK

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Ledstrip03"
    password: !secret PORTAL_PSK

captive_portal:

# Enable logging
logger:

# Enable Home Assistant API
api:
  password: !secret API_PASSWORD

ota:
  password: !secret API_PASSWORD

light:
  - platform: fastled_clockless
    chipset: WS2812
    pin: 2
    num_leds: 30
    rgb_order: GRB
    name: "Ledstrip03"
    effects:
      - random:
      - strobe:
      - flicker:
      - addressable_rainbow:
      - addressable_color_wipe:
      - addressable_scan:
      - addressable_twinkle:
      - addressable_random_twinkle:
      - addressable_fireworks:
      - addressable_flicker:

Anschliessend habe ich den neuen Node in Homeassistant via Integration eingebunden.

Provisorisch am Zielort montiert und das Ganze bei Tageslicht und Dämmerung getestet.

ADFS ID8025 Fehler

Die Konfiguration eines neuen ADFS Server ist mit dem nicht sehr aussagekräftigen Fehler ID8025 fehlgeschlagen. Die naheliegendste Ursache dafür scheint mir mit dem SSL Zertifikate zusammen zu hängen, da alle anderen Einstellungen Standart sind. Leider liefert mir weder eine Suche nach ID8025 noch das Handbuch von Microsoft einen Hinweis auf den Fehler. Also setzte ich beim Erstellen des Zertifikates an. Da es sich um meine HomeLab Installtion handelt, löse ich das Zertifikat von der Lets Encrypt CA mit dem Kommandozeilen Tool lego.

https://github.com/go-acme/lego

Mehr oder weniger zufällig fällt mir beim Anschauen der Hilfe für lego auf, dass beim erzeugen des privaten Schlüssel ein Algorithmus basierend auf elliptischen Kurven verwendet wird. Sobald ich das Zertifikat mit dem Argument -k rsa4096 erstelle lässt es sich ohne Probleme für ADFS verwenden.

Grim Fandango und ResidualVM

Neulich beim Keller aufräumen fiel mit eine alte Kindheitserrinerung in die Hände:
IMG_20160414_062654~2
Dieses Meisterwerk von LucasArts aus dem Jahre 1998 ist offiziell nur unter Windows 95 / 98 lauffähig, allerdings gibt es einen Open Source Interpreter, welcher es erlaubt Lua basierte LucasArts Adventures auf verschiedenen Plattformen zu spielen.
Benötigtes Material:

  • Original CD von Grim Fandango
  • ResidualVM
  • LabCopy

Bei der Version die ich habe sind die Spieldaten (.LAB Dateien) mit einem Kopierschutz versehen, der sie viel grösser aussehen lässt als sie sind. Hier kommt das Hilfstool LabCopy zum Einsatz, welches den Header der Dateien liest und nur die entsprechenden Blöcke kopiert.
Die Quelle ist hier zu finden: https://github.com/klusark/residual-tools/blob/master/tools/labcopy.cpp
Ich habe das Tool von der oben angegebenen Quelle kompiliert:

OSX: labcopy_osx Mach-O 64-bit executable x86_64
Windows x86: labcopy_win
Linux x86: labcopy_lin ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, not stripped

Also mit dem LabCopy bewaffnet kopieren wir alle .Lab Dateien von den beiden CDs in einen Ordner (vorhandene Dateien können überschrieben werden, die sind auf beiden CDs dieselben)
In ResidualVM fügen wir das Spiel mittels “Add Game” hinzu. Dabei kann es zu einer Warnung kommen mit der aktuellen Version von ResidualVM (0.2.1) welche sagt, dass die Game Dateien nicht korrekt sind. Diese Warnungen (ca. 5) können mit OK bestätigt werden, die MD5 Prüfsummen wurden in zukünftigen Releases angepasst, so dass sie stimmen.
Nun geniessen wir eine schöne Runde Grim Fandango

Ingame Szene “erste Begegnung mit Glottis”

Meine Home Automation

Als ich das erste Mal mit Arduino in Berührung kam wurde ich sofort wieder gepackt von der Faszination der Elektronik (als Kind hatte ich mich immer dafür interessiert, allerdings hat sich das mit der Zeit gelegt). Ich kaufte mir allerlei Sensoren und Elemente und fing an wild zu experimentieren. Bereits damals überlegte ich mir, wie ich diese Entwicklungen in den täglichen Haushalt einbauen könnte. Als mir dann eine Freundin ihre Funkgesteuerten Steckdosen zeigte war die Idee geboren. Ich wollte die Lichter über diese Steckdosen nicht mit einer Fernbedienung steuern, sondern mit etwas das mittlerweile fast jeder Mensch besitzt: Das Smartphone. Da ich noch nie eine App programmiert hatte und das ganze Projekt ständig weiter entwickelt werden soll entschied ich mich für ein responsive Webinterface, welches mit einem Arduino kommuniziert, der die Funk Befehle für die Steckdose sendet. Um möglichst Platform unabhängig zu sein verwendete ich Python, welches die Befehle via USB an den Arduino sendet.
Technologien
Frontend: HTML5, AngularJS und Bootstrap
Backend: Python mit CherryPy
Interface: Arduino mit 433Mhz RF Link Kit
Master_Steckplatine
Der erste Wurf funktionierte äusserst zuverlässig und ich entschied mich das Ganze auszubauen. Geräte nur ein- und auszuschalten reichte mir nicht mehr. Zu dieser Zeit hatte ich bereits eine RGB Licherkette im Einsatz um dekoratives Licht zu spenden. Diese wird allerdings mit Infrarot angesteuert, was den Nachteil hat, dass ich nicht von einem Raum aus Ketten in anderen Räumen steuern kann.
Die erste Version war ein Infrarot Emulator, der 433Mhz Codes empfängt und das entsprechende Signal auf den Infrarot Port der Licherkette Steuerung schickt.
<Bild IR EMULATOR>
Dadurch konnte ich die Schaltung der Lichterkette fast vollkommen intakt lassen.
Einige Zeit später fragte mich meine Freundin, ob ich nicht eine hellere Lichterkette in eine Vase die sie gebastelt hat einbauen kann. Die vorhandene war eine mit 2 AA Batterien betriebene 3V Warm White Kette. Kein Problem dachte ich, allerdings wieso muss die Vase nur eine Farbe anzeigen können? Als ich die Schaltung im Geiste bereits zusammen setzte überlegte ich mir, dass ich die gleiche Schaltung auch für die RGB Lichterkette bauen könnte, also einmal eine 5V Variante und eine 12V Variante. Wenig später wurde daraus nur eine Schaltung, bei der mit einem Jumper die Eingangsspannung wahlweise via Spannungsregler 7805 oder direkt auf die Steuerelektronik geführt wird. Dadurch hatte ich ein Modell, das sowohl für 5V wie auch für 12V LEDs ausgelegt geschaffen.
20150621_110020
Später habe ich dem Master einen 433 Mhz Empfänger hinzugefügt, um Daten von Sensoren aufzuzeichen.

Der Fade Mode

Attiny84 und 85 Programmer Shield

Da ich bei Projekten häufig den Attiny85 und Attiny84 verwende habe ich mir aus einem Arduino Prototype Shield einen Programmer gebastelt, der bei Bedarf einfach auf einen UNO gesteckt werden kann um so die Attiny’s zu programmieren.
Stückliste:

  • 1x Arduino Prototype Shield v.5
  • 28x Male Pin Header
  • 1x Sockel DIP 8 Pin
  • 1x Sockel DIP 14 Pin
  • 2x Kondensator 66nF
  • 1x Elko 220uF
  • 2x Drucktaster
  • 1x Led 5mm Grün
  • 1x Led 5mm Rot
  • 2x Widerstand 560 Ohm

Dies sind die Pin Belegungen der beiden Tiny’s:
attiny84_pins attiny85_pins
Das Schaltbild sieht folgendermassen aus:
Attiny Programmer Shield
Die Einzelteile:
DSC00295
Zusammengebaut: