Blog

Icinga2 check by ssh

Veröffentlicht am

Neben dem Agenten, der für die Ausführung der Checks beim Client verantwortlich ist, gibt es auch noch den Check über ssh(check_by_ssh). Im Gegensatz zum Agent muss hierbei nicht eine Icinga Instanz auf dem Remote Host laufen. Die Plugins, bzw. die Checks sollten trotz-alledem auf dem Client vorhanden sein. Die Ausführung läuft dabei über den Port 22(ssh) und nicht wie beim Agent auf Port 5665. Der ssh Port ist meistens schon geöffnet, daher muss auch keine weitere Portfreigabe in der Firewall eingerichtet werden. 

Damit die Plugins auf dem Client vorhanden sind, installieren wir diese. 

root@client:~ sudo apt install nagios-plugins

Icinga führt die checks über ssh aus und dies verlangt im Normalfall das Passwort vom Benutzer, mit dem wir uns anmelden wollen. Dies umgehen wir, indem wir ein privaten Schlüssel erzeugen und den öffentlichen Schlüssel mit dem Client teilen. Der User, mit dem die Icinga Instanz läuft, muss auch auf dem Remote Host angelegt werden. Ermitteln wir die laufenden Icinga Prozesse, sehen wir, dass unsere Instanz mit dem nagios Benutzer läuft.

pi@server:~ $ ps -aux | grep -i icinga
pi         725  0.0  0.0   7360   540 pts/0    S+   08:13   0:00 grep --color=auto -i icinga
nagios   26752  0.9  0.6  80788 24492 ?        Ssl  Aug01   7:31 /usr/lib/arm-linux-gnueabihf/icinga2/sbin/icinga2 --no-stack-rlimit daemon --close-stdio -e /var/log/icinga2/error.log
nagios   26790  0.1  0.1  57556  5796 ?        S    Aug01   1:32 /usr/lib/arm-linux-gnueabihf/icinga2/sbin/icinga2 --no-stack-rlimit daemon --close-stdio -e /var/log/icinga2/error.log
postgres 26820  0.3  0.6 206692 24996 ?        Ss   Aug01   3:01 postgres: 11/main: rtf icinga_ido ::1(40910) idle in transaction

Legen wir diesen Benutzer nun auf dem Client an.

root@client:~ useradd -m nagios
root@client:~ passwd nagios

Um weiter fortfahren zu können, müssen wir uns als nagios auf dem Server anmelden. Der Shell Zugriff ist aber meist für diesen Benutzer gesperrt, sodass wir eine Fehlermeldung bekommen sollten. Falls eine ähnliche Nachricht wie „This account is currently not available.“ kommt, müsst ihr den Benutzer noch für die Shell freischalten. Ersetzt dafür in der /etc/passwd „/usr/sbin/nologin“ mit „/bin/bash“.

root@server:~ grep nagios /etc/passwd
nagios:x:110:115::/var/lib/nagios:/usr/sbin/nologin

root@server:~ vi /etc/passwd

root@server:~ grep nagios /etc/passwd
nagios:x:110:115::/var/lib/nagios:/bin/bash

Nun sollte ein Login möglich sein.

root@server:~ sudo su - nagios

Falls der Login erfolgreich war, können wir das Schlüssel Paar anlegen.

nagios@server:~$ ssh-keygen -b 4096 -t rsa -C "nagios@$(hostname) user for check_by_ssh" -f $HOME/.ssh/id_rsa

Von dem erzeugten Schlüsselpaar müssen wir den öffentlichen Schlüssel zum Client übertragen. Dies könnt ihr manuell machen oder mit ssh-copy-id, den wir hier soweit benutzen. In diesem Zusammenhang werden wir einmal nach dem Passwort abgefragt, damit der Schlüssel übertragen werden kann.

nagios@server:~$ ssh-copy-id -i $HOME/.ssh/id_rsa nagios@client
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/var/lib/nagios/.ssh/id_rsa.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys


nagios@client's password: 

Number of key(s) added: 1

Now try logging into the machine, with:   "ssh 'nagios@client'"
and check to make sure that only the key(s) you wanted were added.

Da wir den „server“ nun als autorisierten Schlüssel beim Client eingetragen haben, sollte der Login ohne Passwort erfolgen.

nagios@server:~ ssh nagios@client

Sind wir erfolgreich angemeldet, können wir uns vom Client wieder abmelden. Entfernen wir nun wieder den Zugriff auf die Shell für den nagios Benutzer auf dem Server. Das Passwort sperren wir ebenfalls, dass es nicht mehr geändert werden kann.

root@server:~ vi /etc/passwd

root@server:~ grep nagios /etc/passwd
nagios:x:110:115::/var/lib/nagios:/usr/sbin/nologin

root@server:~ passwd -l nagios

Icinga2 Service + Host einrichten

Wir sind nun fast am Ende, allerdings fehlt noch der Service und die Änderungen am Host. Ein Service könnte unter in etwa so aussehen, das ist soweit euch überlassen:

apply Service "users" {
  import "generic-service"
  check_command = "by_ssh"

  vars.by_ssh_command = [ "/usr/lib/nagios/plugins/check_users" ]

  // Follows the same principle as with command arguments, e.g. for ordering
  vars.by_ssh_arguments = {
    "-w" = {
      value = "$users_wgreater$" // Can reference an existing custom variable defined on the host or service, evaluated at runtime
    }
    "-c" = {
      value = "$users_cgreater$"
    }
  }

  vars.users_wgreater = 3
  vars.users_cgreater = 5

  assign where host.vars.os == "Linux" && host.vars.agent_type == "ssh"
}

Für die Client-Host Datei legen wir noch die Variable vars.agent_type an und setzten den Wert „ssh“, damit dieser zum Service verknüpft wird.

object Host "client" {
        import "generic-host"
        vars.agent_type = "ssh"
        address = "192.168.188.37"
        check_command = "hostalive"
        vars["os"] = "Linux"
        version = 1596120607.191147
        zone = "raspberrypi"
}

Zu guter letzt legen wir noch einen Endpoint für den Client in der zones.conf an. Damit sollte der Icinga Master/Satellite seine Abfragen an den Endpoint schicken.

object Endpoint "client" {
host = "192.168.188.37"
}

object Zone "client" {
parent = NodeName
endpoints = [ "client" ]
}

Die Einrichtung ist nun komplett abgeschlossen. Falls alles funktioniert hat, sollte nun ein weiterer Service für den Client in icinga2web zu sehen sein.

icinga checkbyssh
Blog

Icinga Linux remote client einrichten

Veröffentlicht am

Damit Icinga auch optimal genutzt wird, sollten Clients installiert werden. Dabei gibt es mehrere Möglichkeiten diese zu überwachen. Die eine ist mit dem Icinga Agent, den ich hier beschreiben werde. Die andere über ssh, die ich in „Icinga2 check by ssh“ beschrieben habe. Icinga benötigt dafür ein Master + Satellite/Agent Aufbau. 

Rollenmodell
  • Ein master hat kein weiteres Elternknoten
    • Icinga Web 2 ist normalerweise auf dem master installiert
    • Ein master fügt die Informationen vom Kindknoten in das Backend oder generiert Benachrichtigungen
  • Ein Satellite hat ein Eltern- und ein Kindknoten
    • Der Satellite kann die Konfiguration für Hosts/services vom Elternknoten erhalten
    • Ein Satellite kann eigene checks ausführen, aber diese auch weiterleiten
    • Ein Satellite kann auch dann weiterlaufen, wenn der master nicht verfügbar ist
  • Ein agent hat immer nur Elternknoten
    • Ein agent führt entweder seine eigenen checks aus oder erhält diese vom Elternknoten

Da ich mein Icinga nur für den privaten Gebrauch verwende, macht für mich ein Cluster keinen Sinn. Auch ein Satellite ist nicht in Verwendung, sodass der Master die Checks direkt zum Agent schickt. Für den Client installieren wir das Icinga2 Packet. In der Regel kommen damit auch die Plugins/Checks mit.

root@client:~$ sudo apt install icinga2

Client Node Wizard

Nun können wir das Setup für den Agent auf dem Client ausführen. Das Setup führt euch durch jeden einzelnen Punkt und fragt euch einige Sachen ab. Meinen Durchlauf habe ich euch hier niedergeschrieben. „raspberrypi“ ist dabei mein Master.

root@client:~# icinga2 node wizard
Welcome to the Icinga 2 Setup Wizard!

We will guide you through all required configuration details.

Please specify if this is a satellite/client setup ('n' installs a master setup) [Y/n]: Y

Starting the Client/Satellite setup routine...

Please specify the common name (CN) [clientname]:

Please specify the parent endpoint(s) (master or satellite) where this node should connect to:
Master/Satellite Common Name (CN from your master/satellite node): raspberrypi

Do you want to establish a connection to the parent node from this node? [Y/n]: Y
Please specify the master/satellite connection information:
Master/Satellite endpoint host (IP address or FQDN): 192.168.188.67
Master/Satellite endpoint port [5665]:

Add more master/satellite endpoints? [y/N]: N
Parent certificate information:

 Subject:     CN = raspberrypi
 Issuer:      CN = Icinga CA
 Valid From:  Apr 26 19:50:56 2020 GMT
 Valid Until: Apr 23 19:50:56 2035 GMT
 Fingerprint: XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX

Is this information correct? [y/N]: y

Please specify the request ticket generated on your Icinga 2 master (optional).
 (Hint: # icinga2 pki ticket --cn 'clientname'):

No ticket was specified. Please approve the certificate signing request manually
on the master (see 'icinga2 ca list' and 'icinga2 ca sign --help' for details).
Please specify the API bind host/port (optional):
Bind Host []:
Bind Port []:

Accept config from parent node? [y/N]: y
Accept commands from parent node? [y/N]: y

Reconfiguring Icinga...
Disabling feature notification. Make sure to restart Icinga 2 for these changes to take effect.
Enabling feature api. Make sure to restart Icinga 2 for these changes to take effect.

Local zone name [clientname]:
Parent zone name [master]: raspberrypi

Default global zones: global-templates director-global
Do you want to specify additional global zones? [y/N]: N

Do you want to disable the inclusion of the conf.d directory [Y/n]: n

Done.

Now restart your Icinga 2 daemon to finish the installation!
root@retropie:~# systemctl restart icinga2

Den Neustart bitte nicht vergessen.

Das Setup wizard stellt sicher, dass folgende Aufgaben gemacht sind:

  • Das api Feature aktivieren
  • Eine Zertifikat-Signierunganfrage(CSR) für den eigenen Knoten erzeugen
  • Erlauben das Zertifikat vom Elternknoten zu verifizieren
  • Das signierte Agent/Satellite Zertifikat + ca.crt in /var/lib/icinga2/certs ablegen
  • zones.conf mit der neuen Zonen Hierarchie updaten
  • Update von /etc/icinga2/features-enabled/api.conf (accept_config, accept_commands) und constants.conf
  • Update von /etc/icinga2/icinga2.conf und include_recursive „conf.d“ auskommentieren

 Mit dem Wizard haben wir eine Signierungsanfrage an den Master geschickt. Diese gilt es nun zu akzeptieren. Suchen wir erst die Anfrage raus und kopieren uns den Fingerprint. Diesen benötigen wir für die Signierung.

root@master:~# icinga2 ca list

Fingerprint                                                      | Timestamp                | Signed | Subject
-----------------------------------------------------------------|--------------------------|--------|--------
<fingerprint> | Jul 30 17:23:36 2020 GMT |       | CN = clientname

Nehmen wir nun den Fingerprint und benutzen ihn im folgenden Snippet

root@master:~# icinga2 ca sign <fingerprint>
information/cli: Signed certificate for 'CN = clientname'.

Endpoint einrichten

Unserem Master müssen wir nun noch den neuen Endpoint bekannt machen und einer Zone zuordnen. Das machen wir in der /etc/icinga2/zones.conf. Nehmt dort folgende Änderungen vor.

#NodeName ist in constants.conf definiert
#NodeName ist bei mir 'raspberrypi'


object Endpoint NodeName {
  host = NodeName
}

object Zone ZoneName {
  endpoints = [ NodeName ]
}

# Endpoint einrichten
object Endpoint "clientname" {
  host = "clientip"
}


object Zone "clientname" {
  parent = NodeName
  endpoints = [ "clientname" ]
}

Dem Endpoint müsst ihr nur noch Services zuordnen und diese werden nach einem Neustart der Instanz vom Master beim Endpoint/Client abgefragt. Ebenfalls habe ich die Variable vars.client_endpoint in den Host eingebracht.

object Host "clientname" {
        import "generic-host"

        address = "clientIP"
        check_command = "hostalive"
        vars["os"] = "Linux"
        vars.remote_client = name
        vars.client_endpoint = name
        version = 1596036000.782959
}

Damit der Service hinzugezogen wird, ist das „assign where“ anzupassen. Mit „host.vars_client_endpoint“ wird überprüft, ob dieser im Host gesetzt ist. Falls dies der Fall ist, wird der Service dem Host angeknüpft.

apply Service "users" {
  import "generic-service"
  check_command = "users"
  command_endpoint = host.vars.client_endpoint
  assign where host.name == NodeName || host.vars.os == "Linux" && host.vars.client_endpoint
}
Allgemein

Meteor Neowise

Veröffentlicht am

Gestern habe ich mehr oder weniger ein Schnappschuss vom Meteor Neowise gemacht. Dieser ist nur alle paar tausend Jahre zu sehen, daher wollte ich die Chance war nehmen und auch mich in die Liste derer eintragen, die von dem Meteor ein Foto gemacht haben. Mit der App Star Walk 2 konnte ich Neowise virtuell direkt aufspüren und wusste wo er ungefähr sein müsste. Auch wenn ich aus dem Büro fotografiert hatte, so kann ich mit dem Foto sehr gut leben :). Eigentlich wollte ich um 23:30 schon aufgeben, da ich den Meteor nicht am Himmel sah, konnte dann aber doch einen hauch dünnen Schweif am Himmel sehen. Ich hielt die Linse also drauf und tatsächlich, mit einer längeren Verschlusszeit ließ sich der Meteor ganz gut erkennen. Also Kamera wieder aufs Stativ. Am Fenster musste ich das Stativ provisorisch befestigen, um eine ruhige Position zu erhalten. Ich bin froh, dass diese Konstellation gehalten hat 🙂

Auf der Canon war mein Sigma 24mm Weitwinkelobjektiv. Auf Blende 1.4 hatte ich für knappe 30 Sekunden belichtet und dieses Bild ist dabei rum gekommen. 

Ich bin sehr froh, dass ich das Bild geschossen habe. Es ist sehr interessant, Neowise auf ein eigenes Bild festgehalten zu haben. Bis zum Ende des Monats soll man ihn noch sehen können, ehe er im August in den Tiefen der Galaxie verschwindet. Wer weiß vielleicht wage ich noch ein weiteres Foto an einem besseren Spot. 

Blog

Windows 10 Wlan Passwort auslesen

Veröffentlicht am

Habt ihr euch mal gefragt, wie ihr von einem zuvor verbundenen WLAN, das Passwort auslesen könnt, weil ihr es z.B. vergessen habt? Dafür gibt es 2 Wege. Natürlich muss eurem Gerät das Netzwerk vorher bekannt gewesen sein. Die erste führt über die Systemsteuerung. Lasst euch den Status eures drahtlosen Adapters anzeigen und geht dort auf Drahtloseigenschaften.

Es öffnet sich ein weiteres Fenster mit den Eigenschaften des drahtlosen Netzwerkes. Dort müsst ihr nur noch auf den Tab Sicherheit klicken und von dem Sicherheitsschlüssel die Zeichen anzeigen lassen.

Ein zweiter Weg geht über die Kommandozeile. Öffnet die CMD als Administrator und gebt dort folgenden Befehl ein.

netsh wlan show profiles <WLAN-NAME> key=clear

Eure Ausgabe könnte dann wie folgt aussehen, wobei unter dem Schlüsselinhalt das verwendete Wlan Passwort sein sollte.

Blog

Gesichtserkennung Raspberry GPIO Schaltung

Veröffentlicht am

Mich haben zuletzt vermehrt Anfragen erreicht, die GPIOs mit der Gesichtserkennung zu kombinieren. Zum Beispiel als Schaltung für die Garage oder das eine LED für eine bestimmte Zeit aktiviert ist. Zudem wäre eine Benachrichtung aufs Smartphone interessant. Diesen Anfragen möchte ich mit diesem Beitrag gerecht werden. Die Anforderung an mich selber waren, dass für jedes Gesicht, welches die Software im Frame erkennt, das Script eine Nachricht an das Smartphone schicken soll. Für jedes bekannte Gesicht soll zusätzlich die LED für 5 Sekunden laufen. Für die Smartphone Benachrichtigung habe ich Pushbullet verwendet. Pushbullet stellt eine App bereit und es stehen mehrere Python Module zur Verfügung. Ich habe die eigene von Pushbullet verwendet.

sudo pip3 install pushbullet.py

Für die Registrierung benötigt man einen Google oder Facebook Account.

Auf der Seite könnt ihr den API Key unter Access Tokens einsehen. Diesen Schlüssel übertragt ihr bei jeder Push Nachricht mit. Ich empfehle euch, die End-zu-End Verschlüsselung zu aktivieren. Ihr müsst ein einmaliges Passwort eintragen. Jedes Gerät das ihr bei Pushbullet registriert habt, benötigt diesen Schlüssel, um die Nachrichten zu entschlüsseln.

 

Für die Schaltung habe ich ein Steckbrett mit LED und Widerstand benutzt. Auf dem Pi ist der GPIO Pin 17 angeschlossen. Es ist euch selber überlassen, welchen Pin ihr nehmt, ihr müsst diesen nur vernünftig schalten können.

Damit hätten wir alles Organisatorische geregelt. Kommen wir nun also zum Code, der an die anderen Gesichtserkennungs-Beiträgen angelehnt ist. Ich habe hier zusätzlich Threads mit eingebaut, damit die Abarbeitung der Notifications oder die Schaltung der LED neben dem Main-Thread laufen können. Die Threads rufen jeweils Funktionen auf, daher besteht für die Notification, als auch für die LED Schaltung eine eigenständige Funktion, denen ich Argumente übergebe. Damit ein Thread nicht doppelt aufgerufen werden kann, habe ich mit if tgpio not in threading.enumerate(): eine Verriegelung eingebaut. threading.enumerate() enthält jede zurzeit aktive Thread-ID.

Funktion NotifyPushbullet:

Ein Foto wird zwar außerhalb der Funktion gemacht, aber immer dann, wenn das Script ein Gesicht im Frame entdeckt hat. Das Foto ist lokal gespeichert, bevor die Funktion NotifyPushbullet es verschickt. Ich lege diese Funktion danach noch 30 Sekunden „schlafen“, um eine weitere unnötige Notifications zu vermeiden.

Funktion FaceGPIO:

Die LED wird geschaltet, wenn ein bekanntes Gesicht im Frame ist. Sobald eines erkannt wurde, leuchtet die LED für 12 Sekunden, ehe sie sich selber wieder ausschaltet. Falls es bei den GPIOs zu Fehlern kommt, z.B. eine Doppelbelegung des Pins, löst eine Exception aus und gibt den Fehler im Terminal zurück.

Weiter unten findet ihr den Code. Ich habe einige Einträge kommentiert, die helfen sollen den Code einfacher zu verstehen.

# This program will print out the names of anyone it recognizes to the console.

# To run this, you need a Raspberry Pi 2 (or greater) with face_recognition and
# the picamera[array] module installed.
# You can follow this installation instructions to get your RPi set up:
# https://gist.github.com/ageitgey/1ac8dbe8572f3f533df6269dab35df65

try:
    import face_recognition
    import picamera
    import numpy as np
    import os
    import datetime
    from pushbullet import Pushbullet
    import gpiozero
    from time import sleep
    import threading

except:
    print("failed to load module")
    exit()


# Initialize some variables
tpush = None
tgpio = None
face_locations = []
face_encodings = []
image = []
known_face_encoding = []
known_faces = []

# Set some variables
pb = Pushbullet('apikey')
framerate = 15

def NotifyPushbullet(pb,file):
    #push notification
    push = pb.push_note("Face detected","Someone was detected")
    # push image
    with open(file, "rb") as pic:
    file_data = pb.upload_file(pic, "picture.jpg")
    push = pb.push_file(**file_data)
    # set NotifyPushbullet to sleep
    print("Push Notification sent")
    sleep(30)


# Function for GPIO
def FaceGPIO(timetosleep):
    try:
        #initialize LED
        led = gpiozero.LED(17)
        # switch LED on
        led.on()
        #set to sleep
        sleep(timetosleep)
        # switch LED off
        led.off()
    # if any error occurs call exception
    except gpiozero.GPIOZeroError as err:
        print("Error occured: {0}".format(err))

# Get a reference to the Raspberry Pi camera.
# If this fails, make sure you have a camera connected to the RPi and that you
# enabled your camera in raspi-config and rebooted first.
try:
    camera = picamera.PiCamera()
    camera.resolution = (320, 240)
    camera.framerate = framerate
    output = np.empty((240, 320, 3), dtype=np.uint8)
except:
    print("something went wrong while initialize camera")
    exit()

# Load pictures and learn how to recognize it.
print("Loading known face image(s)")

#is loading all images in faces/ and create a stamp
try:
    for faces,i in zip(os.listdir("faces"),range(len(os.listdir("faces")))):
        known_faces.append("faces/"+faces)
        image.append(face_recognition.load_image_file("faces/" + faces))
        known_face_encoding.append(face_recognition.face_encodings(image[i])[0])
except:
    print("could not find known pictures")
    exit()


while True:
    print("Capturing image.")
    # Grab a single frame of video from the RPi camera as a numpy array
    camera.capture(output, format="rgb")

    # Find all the faces and face encodings in the current frame of video
    face_locations = face_recognition.face_locations(output)
    print("Found {} faces in image.".format(len(face_locations)))
    face_encodings = face_recognition.face_encodings(output, face_locations)

    #print all active threads
    print(threading.enumerate())

    # Loop over each face found in the frame to see if it's someone we know.
    for face_encoding in face_encodings:

        # make a picture
        camera.capture('/home/pi/image.jpg', use_video_port=True)
        # create a thread to push Notifications
        # check if such a thread already exists
        if tpush not in threading.enumerate():
            #define thread
            tpush = threading.Thread(target=NotifyPushbullet,args=(pb,'/home/pi/image.jpg',))
            #start thread
            tpush.start()
        else:
            # if a thread was started already and still is running
            print("Thread tpush already started: {0}".format(str(tpush)))

        for face, i in zip(os.listdir("faces"), range(len(known_faces))):

            if face_recognition.compare_faces([known_face_encoding[i]], face_encoding,0.6)[0]:
                #print found face all upper case
                print("<found: ".upper() + known_faces[i].upper() + ">")

                #create a thread to switch the LED on
                # check if such a thread already exists
                if tgpio not in threading.enumerate():
                    #define thread; call function FaceGPIO with argument
                    tgpio = threading.Thread(target=FaceGPIO,args=(5,))
                    #start thread
                    tgpio.start()
                else:
                    # if a thread was started already and still is running
                    print("Thread tgpio already started: {0}".format(str(tgpio)))

Blog

Projekt: Asset Management Teil3 Metabase

Veröffentlicht am

Kommen wir nun zum dritten Teil dieses Projektes. In den vorherigen Teilen habe ich über das Projekt selber gesprochen, die Datenbankstruktur und generelle Scripte erläutert bzw. erwähnt. Im zweiten Teil kam Django dazu. In diesem Teil werde ich das Metabase Frontend etwas genauer zeigen. Eins vorweg, ich werde hier nicht alle Funktionen von Metabase beschreiben, sondern nur diejenigen, die ich zurzeit verwende oder eventuell später verwenden möchte. Metabase selber betreibe ich zurzeit mit 2 Dashboards. Eins für die Dividenden und eins für meine gehaltenen Aktien und meine Einkünfte. Sind wir auf der Startseite, so kann eins von beiden ausgewählt werden.

 

In meinen Dividenden Dashboard lasse ich mir durch native Fragen, die Dividende pro Jahr, pro Firma und pro Firma im Jahr anzeigen. Native Fragen sind SQL Abfragen, die ich soweit selber geschrieben habe. Solch Fragen können aber auch mithilfe der Metabase Tools gänzlich ohne SQL erzeugt werden.

Hier ist z.B. die SQL Abfrage für die Dividende pro Firma im Jahr:

select min(COMPANY.NAME), sum(DIVIDENT.PROFIT),year(DIVIDENT.RECEIVED_AT)  as YEAR, min(Currency.CURRENCY) from STOCK_DIVIDENT DIVIDENT
inner join STOCK_COMPANY COMPANY on DIVIDENT.COMPANY = COMPANY.ID
inner join Currency on DIVIDENT.CURRENCY = Currency.ID Group by DIVIDENT.COMPANY, YEAR(DIVIDENT.RECEIVED_AT);

 

Unter meinem Stock Dashboard gibt es etwas mehr zu sehen. Dort liste ich alle mein kompletten Aktien auf. Käufe und Verkäufe und den Gesamtwert, den ich mit Aktien bisher realisiert habe. Auch hier sind alle Anzeigen per SQL von mir selbst geschrieben. Hier als Beispiel die Query von dem Torten Diagramm:

select sum(T.QUANTITY) as QUANTITY,C.NAME from STOCK_AVAILABLE_STOCK T INNER JOIN STOCK_COMPANY C on T.COMPANY = C.ID group by T.COMPANY

 

Zudem können sogenannte Pulse erstellt werden. Mit einem Pulse könn ihr Daten per Mail oder Slack nach einem bestimmten Zeitplan versendeen. Bisher habe ich diese Funktion noch nicht verwendet, aber ich gehe davon aus, dass ich die Benachrichtigung ab einem bestimmten Zeitpunkt einrichten werde.

 

Falls ihr euch noch die vorherigen Teile vom Projekt anschauen wollt, so findet ihr den ersten Teil hier und den zweiten Teil hier.

 

Fazit

Das war es von vorerst von diesem Projekt. Ich habe viel wissenswertes mitgenommen und konnte mich etwas in Django einarbeiten. Für mich war und ist es ein interessantes Projekt und ich entwickel es definitiv noch weiter. Für mich bringt es mittlerweile schon einen erheblichen Mehrwert. Durch das Admin Interface ist die Pflege der Daten einfach und komfortable und durch die Skalierbarkeit von Metabase kann ich mir mehrere Dashboard erzeugen, die mir jeweils andere Einsichten geben.

 

 

Blog

Projekt: Asset Management Teil2 Django

Veröffentlicht am

Ein kurzer Rückblick zu Teil 1:

Das Projekt „Asset Management“ habe ich ins Leben gerufen, damit ich meine Investments zentral gespeichert habe. Dafür verwende ich Metabase. Die Datenbank Struktur ist im vorherigen Bericht zu sehen. Investiert habe ich in mehrere Arten, von P2P bis Aktien. Dabei haben alle ihre eigene Plattform, wo ich die Informationen bekomme. Diese Informationen liegen allerdings nicht zentral an einem Ort. Dies wollte ich mit diesem Projekt ändern.

Ich habe das Django Projekt „controlpanel“ genannt und 2 Apps in dem Projekt erstellt. Diese haben den Namen „Metabase“ und „Weather. Das integrierte Admin Interface von Django leistet mir hier große Dienste. Ich benutze es um händische Einträge in die Datenbank zu machen, aber auch um bereits vorhandene Datensätze abzuändern. Durch die Passwort Abfrage sind diese auch vor Unbefugten geschützt.

Das Controlpanel besteht aus 3 Segmenten, was soweit im Bild zu ersehen ist. Das Erste ist für die Weather Daten(Schnittstelle openweather). Dort kann ich, falls gewünscht, die Daten ändern. Ebenfalls gibt es hier den Link zu meinem Dashboard, wo ich verschiedene Auswertungen über die Wetterdaten fahren kann.

Das zweite Segment gehört zu Metabase und beinhaltet die Eingabe der Daten vom Windpark z.B. der eingespeiste Energieertrag, Windmühlen etc. Natürlich werden hier auch die Daten von meinem Aktienhandel eingetragen. Diese Daten dienen Metabase zur Darstellung. Auch in diesem Segment existiert, wie im Weather Segment, eine Verlinkung zum Metabase Frontend.

Das dritte Segment beinhaltet die Backups, die jeden Abend erstellt werden. Das Script schreibt dafür eine Log-Datei. Die Funktion „readbackups(file)“ in der function.py weiter unten liest diese Informationen aus und übermittelt diese dem Template zur Ausgabe.

 

Da ich bisher nur insgesamt 2 Apps habe, beschränken sich die URLs ebenfalls nur auf diese zwei.

urls.py:

from django.contrib import admin
from django.urls import include, path
from metabase import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('',views.index, name='index'),
    path('metabase/', views.metabase, name='metabase'),
    path('weather/', views.weather, name='weather'),
]

 

functions.py:

import os
import random
import re

def readbackups(file):
  backup_dict = {'backup': {}}
  try:
    fileline = open(file,"r")
    for count,line in enumerate(fileline):
      #print(line)
      backup_dict['backup'][count] = line.split("/")
      if count == 0:
        #add a name to the total backuo
        backup_dict['backup'][0].append("Total")
        # cut the last two character(". ")
        backup_dict['backup'][count][0] =  backup_dict['backup'][count][0][:-2]
      # cut the last character(.)
      backup_dict['backup'][count][0] =  backup_dict['backup'][count][0][:-1]	

  except os.error as e:
        print(e)
        return {'error': e}
  
  return backup_dict


  

 

Am wichtigsten sind hier allerdings die Models. Mit den Models kann Django die Datenbankstruktur abbilden. Sie sind also sehr hilfreich für das Admin Interface. Für jede App gibt es eine Models.py im jeweiligen Ordner. Da diese Datei ziemlich lang ist, stelle ich nur einen Teil online.

from django.db import models
from django.core.validators import MaxValueValidator, MinValueValidator
import datetime


class AuxProjects(models.Model):
    id = models.IntegerField(db_column='ID', primary_key=True)  # Field name made lowercase.
    datum = models.DateField(db_column='DATUM')  # Field name made lowercase.
    zins = models.DecimalField(db_column='ZINS', max_digits=4, decimal_places=2)  # Field name made lowercase.
    laufzeit = models.DecimalField(db_column='LAUFZEIT', max_digits=4, decimal_places=2)  # Field name made lowercase.
    anlage = models.DecimalField(db_column='ANLAGE', max_digits=4, decimal_places=2)  # Field name made lowercase.
    status = models.ForeignKey('AuxStatus', models.DO_NOTHING, db_column='STATUS')  # Field name made lowercase.
    score = models.ForeignKey('AuxScore', models.DO_NOTHING, db_column='SCORE')  # Field name made lowercase.
    auszahlung = models.DateField(db_column='AUSZAHLUNG')  # Field name made lowercase.
    bezahlt = models.DecimalField(db_column='BEZAHLT', max_digits=4, decimal_places=2)  # Field name made lowercase.
    currency = models.ForeignKey('CURRENCY', models.DO_NOTHING, db_column='CURRENCY')  # Field name made lowercase.
    uploaded_at = models.DateTimeField(db_column='UPLOADED_AT')  # Field name made lowercase.

    class Meta:
        managed = False
        db_table = 'AUX_PROJECTS'

    def __str__(self):
        return 'AUX: {}'.format(self.id)


class AuxScore(models.Model):
    id = models.AutoField(db_column='ID', primary_key=True)  # Field name made lowercase.
    name = models.CharField(db_column='NAME', max_length=50)  # Field name made lowercase.

    class Meta:
        managed = False
        db_table = 'AUX_SCORE'

    def __str__(self):
        return 'Score: {}'.format(self.name)
[...]

Die Models tragen wir ebenfalls in der admin.py ein:

# Erste Variante
# Register your models here.
myModels = [AuxProjects, AuxScore,AuxStatus,AuxTotalpaidin,StockBrokerage,StockCompany,StockTradingPlace,Currency,StockCountry,StockTradingFee, Unit,WindLocation,WindManufacturer,WindParc]
admin.site.register(myModels)

# zweite Variante
class StockTradingAdmin(admin.ModelAdmin):
    list_display = ('company','orderno','transaction_time')
    ordering = ('company__name','orderno','transaction_time')
    search_fields = ('company__name',)

admin.site.register(StockTrading, StockTradingAdmin)

Dabei gibt es 2 Varianten. Die erste ist die einfachste und Django übernimmt die Sortierung und die Beschriftung. Dazu tragt den Models Namen in der MyModels Liste ein. Falls ihr z.B. die angezeigten Informationen, die Sortierung und die Suche anpassen wollt, legt eine eigene Klasse für das Model an.

Anhand der Models wird das Admin Interface aufgebaut. Zurzeit sind dieses bei mir so aus:

 

Dadurch, das die Datentypen wie int, float, varchar etc. bei den Models mit angegeben werden, wird die Eingabe vereinfacht. Django sucht dabei schon die richtigen Eingabefelder aus. Hier ganz gut zu sehen bei dem Hinzufügen eines neuen Trades:

 

 

 

 

 

 

 

 

 

 

Blog

Projekt: Asset Management

Veröffentlicht am

Ich habe mich vor längerem dazu entschlossen, ein Teil meines Geldes zu investieren, um daraus später Profite zu erzielen. Dabei habe ich in die unterschiedlichsten Formen wie erneuerbare Energien, Aktien und P2P Netzwerke (z.B. Auxmoney) investiert. Jeder dieser Formen hat zumeist sein eigenes, ich nenne es mal Dashboard, wo der Verlauf begutachtet und der Profit aufgelistet wird. Daraus resultierend habe ich eine Menge Apps/Internetseiten, wo die jeweiligen Informationen liegen. Einige boten mir schon die, von mir verlangten Informationen, einige nicht. Alles in allem wollte ich eine zentrale Stelle. Ich entschloss mich dafür ein Datawarehouse aufzusetzen und bin mit dem OpenSource DWH Metabase fündig geworden. Metabase kann ich auf meinem vServer betreiben, was für mich Voraussetzung war. Die Installation bzw. das Starten des DWH’s ist relativ einfach – die geladene metabase.jar ausführen. Für diesen Prozess habe ich mir ein Script geschrieben, welches ihr unter dem Beitrag Metabase Startup Script finden könnt.

Backend

Das Backend besteht aus 2 MySql Datenbanken mit mehreren Tabellen. Um nicht alle Daten selbst in die Tabellen eintragen zu müssen, gibt es einige Scripte, die diese Arbeit für mich abnehmen. Unter anderem das Script aus dem Beitrag openweather. Auch benutze ich das Django Admin Interface, dazu aber mehr im zweiten Teil dieses Projektes.

 

 

Damit die Struktur einfacher zu verstehen ist, habe ich den Hintergrund jeder Datenbank Tabelle niedergeschrieben.

AUX_PROJECTS finanzierte Auxmoney Projekte
AUX_SCORE AuxmoneyScore zur Bewertung der Liquidität
AUX_TOTALPAIDIN Einzahlungen auf das Auxmoneykonto
AUX_STATUS Projektsstatus(verkauft,aktiv,etc.)
STOCK_TRADING_PLACE Handelsplatz(Tetra,direkt,etc.)
STOCK_TRADING_FEE Handelsplatzgebühren
STOCK_COMPANY Firmeninformationen
STOCK_COUNTRY Land der Firma
STOCK_BROKERAGE der Broker/Bank
STOCK_DIVIDENT Ausgezahlte Dividende
STOCK_AVAILABLE_STOCK zurzeit gehaltene Aktien
STOCK_TRADING gekaufte Aktien
STOCK_SOLD_STOCK verkaufte Aktien
WIND_DEPOSIT getätigte Einzahlung
WIND_INCOME erzieltes Einkommen
WIND_PARC Windpark Informationen
WIND_LOCATION Windpark Standorte
WIND_MANUFACTURER Windmühlenhersteller
WIND_TYPE Windmühlen-Typ
WIND_MILL Windmühlen Informationen
WIND_ENERGY_PERFORMANCE Windmühlen Performance
WEA_OPENWEATHER URL + apikey
WEA_CITY Standortcode von Openweather
CUR_WEATHER Wetterdaten von Openweather
Currency Währungen
Unit Einheiten

Tabellen mit dem Präfix AUX sind für meine Auxmoney Aktivitäten. WIND ist für die Windpark-Daten und WEA für die von openweather gezogenen Winddaten. Stock hat hier den größten Anteil und ist für meine Tätigkeiten an der Börse.

STOCK_SOLD_STOCK hat zusätzlich noch 2 Trigger. Einen vor und einen nach dem Insert. Vor jedem Insert in STOCK_SOLD_STOCK wird das Profit berechnet, welches ich mit dem Verkauf erwirtschaftet habe.

CREATE DEFINER=`user`@`%` TRIGGER `STOCK_SOLD_STOCK_before_insert` BEFORE INSERT ON `STOCK_SOLD_STOCK` FOR EACH ROW BEGIN

SET NEW.PROFIT = NEW.FINAL_PAYOUT - (SELECT NEW.QUANTITY * PRICE_AVG FROM STOCK_AVAILABLE_STOCK WHERE COMPANY = NEW.COMPANY);

END

Nach dem Insert wird die STOCK_AVAILABLE_STOCK mit der aktuellen Stückzahl geupdated.

CREATE DEFINER=`user`@`%` TRIGGER `STOCK_SOLD_STOCK_after_insert` AFTER INSERT ON `STOCK_SOLD_STOCK` FOR EACH ROW BEGIN

UPDATE STOCK_AVAILABLE_STOCK SET QUANTITY = QUANTITY - NEW.QUANTITY WHERE COMPANY = NEW.COMPANY;

END

Ähnliches habe ich auch für STOCK_TRADING. Hier wird die Stückzahl addiert und nicht subtrahiert. Falls es eine Firma ist, mit der ich noch nicht gehandelt habe, wird ein neuer Eintrag erstellt.

CREATE DEFINER=`user`@`%` TRIGGER `STOCK_TRADING_after_insert` AFTER INSERT ON `STOCK_TRADING` FOR EACH ROW BEGIN

INSERT INTO STOCK_AVAILABLE_STOCK(COMPANY,QUANTITY,PRICE_AVG,UPDATED_AT) 
  select * from (select NEW.COMPANY,0 as QUANTITY,0 as PRICE_AVG,now()) AS tmp where NOT EXISTS ( 
    select COMPANY from STOCK_AVAILABLE_STOCK where COMPANY = NEW.COMPANY
  ) LIMIT 1;

UPDATE STOCK_AVAILABLE_STOCK SET QUANTITY = QUANTITY + NEW.QUANTITY, PRICE_AVG = ((PRICE_AVG * (QUANTITY - NEW.QUANTITY)) + NEW.FINAL_PAYMENT) / (QUANTITY) WHERE COMPANY = NEW.COMPANY;

END

Durch diese Trigger habe ich immer einen Überblick über meine gehaltenen Aktien und deren Mittelwert, ohne dass ich diese immer wieder neu ermitteln muss. Erspart mir also eine Menge Arbeit.

Controlpanel

Damit ich nicht alle Informationen mit SQL Befehlen in die Datenbank schreiben muss, habe ich Django als Zwischenschicht eingebracht. Django stellt mir mit seinem Admin Interface große Dienste zur Verfügung. Den Django Beitrag zu diesem Projekt findet ihr hier: Projekt: Asset Management Teil2 Django

Linux

Raspberry Backup erstellen

Veröffentlicht am

Auch wenn der Raspberry Pi schnell aufgesetzt ist, so ist es immer ärgerlich wenn Daten verloren gehen. Hier besonders die Konfigurationen, die bei einem Ausfall der SD Karte verloren gehen könnten. Ein Backup schützt vor totalen Daten- und Konfigurationsverlust. Nun gibt es mehrere Lösungen ein Backup zu erstellen, ich benutze hier aber die Linux internen, die per default schon bei Raspbian dabei sind. in diesen Zusammenhang spreche ich von dem Linux-Tool dd(disk dump). dd kopiert Festplatten, Dateien und Partitionen „bit-genau“, heißt nichts anderes, als das der Datenträger Bit-für-Bit ausgelesen wird unabhängig von dessen Inhalt. Ebenfalls ignoriert es das Dateisystem auf dem Datenträger.

Schauen wir uns nun die Festplatte bzw. die Partitionen an, die wir sichern können.

sudo fdisk -l

Disk /dev/mmcblk0: 29,7 GiB, 31914983424 bytes, 62333952 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xa92095ed

Device         Boot   Start      End  Sectors  Size Id Type
/dev/mmcblk0p1         2048  5033203  5031156  2,4G  e W95 FAT16 (LBA)
/dev/mmcblk0p2      5033204 62333951 57300748 27,3G  5 Extended
/dev/mmcblk0p5      5038080  5103613    65534   32M 83 Linux
/dev/mmcblk0p6      5103616  5627901   524286  256M  c W95 FAT32 (LBA)
/dev/mmcblk0p7      5627904 62333951 56706048   27G 83 Linux

Um die gesamte Festplatte samt Bootloader und Partitionen zu sichern, nehme ich das /dev/mmcblk0 Label für den „if“ Parameter. Dieser benötigt die Source, die gesichert werden soll. In „of“ wird der Backup-Destination angegeben. Bei mir ist das mein Nas, welches ich vorher gemountet habe. Ab Version 8.24 kann eine Statusanzeige aktiviert werden.

sudo mount -t cifs -o user=user,password=passwort,rw,file_mode=0777,dir_mode=0777 //nas/<pfad> /mnt/nas

BACKUP_PFAD= "/mnt/nas"
BACKUP_NAME= "raspbackup"
sudo dd if=/dev/mmcblk0 of=${BACKUP_PFAD}/${BACKUP_NAME}-$(date +%Y%m%d).img bs=1MB status=progress

Eine weitere Möglichkeit den Progress zu sehen, ist die Verwendung des USR1 Signals in einem zusätzlichen Terminal.

Dazu sucht ihr im neuen Terminal mit

ps -a

den dd Prozess heraus und kopiert die PID.

kill -USR1 <PID>

Nachdem ihr diesen Befehl abgesetzt habt, erscheint eine Fortschrittsanzeige in dem 1. Terminal, wo der dd-Prozess läuft. Mit watch könnt ihr den Befehl automatisch in Intervallen triggern lassen.

watch -n300 "sudo kill -USR1 <PID>"

Je nach Größe der SD-Karte, kann das Backup natürlich länger dauern.

Damit das Backup im Intervall erstellt wird, kann ein Cronjob erzeugt werden, der diese Tätigkeiten eigenständig ausführt.

Dafür erzeugen wir eine .sh, die die erforderlichen Befehle ausführt.

vi backup.sh

#!/bin/bash
BACKUP_PFAD= "/mnt/nas" 
BACKUP_NAME= "raspbackup" 
sudo mount -t cifs -o user=user,password=passwort,rw,file_mode=0777,dir_mode=0777 //nas/<pfad> /mnt/nas 
sudo dd if=/dev/mmcblk0 of=${BACKUP_PFAD}/${BACKUP_NAME}-$(date +%Y%m%d).img bs=1MB status=progress

exit 0

Gefolgt von dem Cronjob.

sudo crontab -e

* 2 * * * /etc/backup.sh

 

Der Cronjob wird nun jeden Tag um 2:00h aufgerufen und erstellt uns ein Backup.

Blog

rsync over ssh

Veröffentlicht am

Ich bin gerade dabei mich intensiv mit Deep Learning zu beschäftigen. Als Werkzeug dient mir dazu Anaconda und Jupyter Notebook. Dabei hatte ich die Idee, Jupyter Notebook auch dann noch zu benutzen, wenn ich gerade nicht zuhause bin. Wollte aber trotz alledem überall mit den gleichen Daten arbeiten. Von Zuhause würde ich auf das NAS zugreifen, welches nicht von Außen erreichbar ist und auch nicht soll. Wenn ich unterwegs bin, muss ich also zwangsweise einen anderen Weg einschlagen.  Hier kommt mein vServer zum Einsatz, den ich als Cloud hinter einem VPN benutze. Dort läuft ebenfalls Anaconda mit Jupyter Notebook(Ich könnte zwar dauernd einen VPN Tunnel aufbauen, auch wenn ich Zuhause bin, aber das möchte ich nicht). Die technische Infrastruktur wäre also vorhanden, aber wie schaffe ich es, dass die beiden Sourcen in-sync bleiben und dass ohne mein Beisein oder das ich etwas beisteuere? Durch die offenen Ports auf dem vServer wird es noch erschwert, denn nur der VPN Port und der SSH Port sind nach Außen hin offen. Also kann ich ebenfalls nur einen Weg ansteuern der per SSH möglich ist. Getriggert wird das alles von meinem NAS, da der vServer keine Verbindung zu meinem NAS aufbauen kann. Die Gründe hatte ich weiter oben schon genannt. Das alles in ein Shell Script verpackt und auf dem NAS mittels cronjob periodisch ausführen lassen. SSH fragt allerdings per Default das Password ab, das müsste dann mit einem privaten, öffentlichen Schlüssel Verbund umgangen werden.

Zusammengefasst:

  • NAS triggert die Synchronisation
  • Synchronisation über SSH
  • SSH ohne Password Abfrage
  • Shell Script mittels Cronjob periodisch ausführen

Fangen wir also an. Zuerst erstellen wir den privaten/öffentlichen Schlüssel. Wie das geht, habe ich in dem Artikel „SSH Verbindung mit privaten/öffentlichen Schlüssel“ beschrieben. Ich gehe jetzt also davon aus, dass die SSH Verbindung ohne Passwort soweit funktioniert.
Es fehlt also nur noch das Script und der Cronjob. SCP wäre die einfachste Lösung. Jedoch wäre der Overload zu hoch und ich müsste eigene Funktionen einbauen, die zwischen Dateien und Ordner unterscheiden, sowie auf welcher Seite gerade die neuste Datei liegt und ob neue dazugekommen sind. Das wäre ziemlich mühselig. Daher habe ich was anderes gesucht und bin mit rsync fündig geworden. Rsync bietet solche Funktionen und lässt sich über SSH tunneln. Das Script besteht nun aus einfachen 5 Zeilen.

 

echo "`date +%d.%m.%Y\ %T` `basename $0` (pid: $$): NAS->CLOUD" >> /share/homes/user/Programing/anaconda/rsync.log
rsync -ruzv --size-only -e "ssh -p <sshport>" /share/homes/user/Programing/anaconda/notebooks/* user@vserver:~/Projekte/JNotebook/. >> /share/homes/user/Programing/anaconda/rsync.log
echo "---------------------------------------------" >> /share/homes/user/Programing/anaconda/rsync.log
echo "`date +%d.%m.%Y\ %T` `basename $0` (pid: $$): CLOUD->NAS" >> /share/homes/user/Programing/anaconda/rsync.log
rsync -ruzv --size-only -e "ssh -p <sshport>" root@vserver:~/Projekte/JNotebook/* /share/homes/user/Programing/anaconda/notebooks/. >> /share/homes/user/Programing/anaconda/rsync.log

 

Am wichtigsten sind hier natürlich die Schalter: -ruzv, –size-only und -e.

r = recurse into directories
u = skip files that are newer on the receiver
z = compress file data during the transfer
v = increase verbosity
e = specify the remote shell to use
size-only = skip files that match in size

Mit dem Schalter -e „ssh -p <sshport>“ wird der SSH Tunnel zum vServer aufgebaut. Als sshport tragt ihr den Port ein, auf dem euer Server auf SSH lauscht. Üblich auf dem Port 22.
Rsync führe ich in beiden Richtungen aus, also vom NAS -> CLOUD und CLOUD -> NAS, damit sich die Datenstände abgleichen.

den cronjob richten wir mit

crontab -e

ein und tragen die nachfolgende Zeile ein.

# m h dom m dow cmd
*/10 * * * * /etc/rsync-anaconda.sh

Diese Zeile bedeutet, dass das Script jede 10 Minuten aufgerufen wird. Die Ergebnisse der Ausführung wird soweit in eine Log Datei auf meinem NAS niedergeschrieben. Somit habe ich auch alles im Blick, falls etwas schief läuft.