Projekt: Asset Management Teil3 Metabase

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.

 

 

Projekt: Asset Management Teil2 Django

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:

 

 

 

 

 

 

 

 

 

 

Projekt: Asset Management

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

Raspberry Backup erstellen

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.

rsync over ssh

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.

 

 

 

SSH Verbindung mit privaten/öffentlichen Schlüssel

Üblicherweise kennt man einen SSH Login mit Passwort. Was ist aber, wenn es erforderlich ist, eine Verbindung ohne Password herzustellen? Hier kommt die Schlüssel-Authentifizierung ins Spiel. Bei dieser Art der Authentifizierung wird mit Schlüsseln gearbeitet. Genauer gesagt dem öffentlich- und dem privaten Schlüssel.  Wollen wir z.B., dass Server1 sich ohne Passwort Abfrage auf dem Server2 anmelden kann, so erstellen wir auf Server1 einen privaten, samt öffentlichen Schlüssel. Server2 müssen wir den öffentlichen Schlüssel mitteilen. Schauen wir uns das untere Bild mal genauer an. Server1 möchte eine SSH Verbindung nach Server2 aufbauen. Server2 schickt eine Random Antwort, die Server1 mit einer verschlüsselten Antwort wieder an Server2 zurücksendet. Wenn Server2 diese Nachricht mittels des öffentlichen Schlüssel entschlüsseln kann, ist der Client autorisiert. Bedenkt bitte, dass jeder, der im Besitz des öffentlichen Schlüssels ist, sich mit dem Server authentifizieren kann. Es sei denn der Schlüssel beinhaltet noch eine Passphrase. Sprich zum Entschlüsseln wird ein Art Passwort benötigt. Dies ist generell sinnvoll, da es die Sicherheit nochmals erhöht. Hindert aber den Ablauf eines automatischen Scripts, da es die Eingabe erfordert.

 

 

 

 

Schlüsselpaar erzeugen

Erzeugen wir nun das Schlüsselpaar auf unserem Server1. Server1 ist in diesem Fall ein Linux Server.

ssh-keygen -t rsa -b 4096

Mit diesem Befehl erzeuge ich einen Schlüssel vom Typ RSA und eine Bitlänge von 4086 bit. RSA ist das Verschlüsselungsverfahren. Je höher die Bitlänge umso sicherer die Verbindung, aber schlechter die Performance. 4096 Bit ist ein guter Mix aus Performance und Sicherheit.

Das erzeugte Schlüsselpaar existiert nun im versteckten ssh Ordner: ~/.ssh/

Den öffentlichen Schlüssel könnt ihr jetzt ganz einfach mit folgendem Befehl auf Server2 transferieren.

ssh-copy-id -i ~/.ssh/öffentlicherSchlüssel user@server2

Steht euch allerdings ssh-copy-id nicht zur Verfügung oder schlägt fehl, so könnt ihr den öffentlichen Schlüssel auch eigenständig kopieren und unter Server2 in ~/.ssh/authorized_keys abspeichern. Dort sind alle öffentlichen Schlüssel enthalten, von Clients die Zugriff mittels diesem Verfahren haben. Löscht ihr diesen Key, so hat auch der angegebene Client keinen Zugriff mehr.

 

icinga2 auf dem Raspberry Pi mit Postgresql

Icinga2 und Icingaweb2 installieren

Damit wir Icinga2 und Icingaweb2 installieren können, benötigen wir das entsprechende Repository. Sobald dies eingerichtet ist, installiert es mit dem apt Befehl.

cuurl https://packages.icinga.com/icinga.key | sudo apt-key add - echo "deb http://packages.icinga.com/raspbian icinga-stretch main" \ | sudo tee /etc/apt/sources.list.d/icinga.list
sudo apt update
sudo apt install icinga2 icingaweb2
http://localhost/icingaweb2

 

Nachdem wir auf die Seite gelangt sind, verlangt Icinga ein Token zur Authentifizierung von uns. Dem wollen wir nun nachgehen.

sudo icingacli setup config directory --group icingaweb2;
sudo icingacli setup token create;

 

Optional könnten wir noch überprüfen, ob die erforderlichen Gruppen für Icinga existieren. Es sollte ungefähr wie unten aussehen. Die icingaweb2 Gruppe muss existieren und die Gruppe www-data enthalten.

cat /etc/groups |grep -i incinga

icingaweb2:x:117:www-data

 

Können wir, dank der erfolgreichen Authentifizierung, weiter auf der Seite gehen, so gelangen wir zur Auswahl der Module. Wählt euch eure entsprechenden Module aus.

Auf der nächsten Seite überprüft Icinga die System-Abhängigkeiten.

Bevor wir weitergehen, sollten wir diese Fehler lösen. Als erstes richten wir die korrekte Zeitzone ein. Mit einem Editor wie vi könnt ihr die date.timezone entkommentieren und eure entsprechende Zeitzone setzen. Bei mir ist es Europe/Berlin.

sudo vi /etc/php/7.3/apache2/php.ini


cat /etc/php/7.3/apache2/php.ini | grep date.timezone
; http://php.net/date.timezone
date.timezone = Europe/Berlin

Gefolgt von der Installation der fehlenden PHP Module.

sudo apt-get install php-pgsql 
sudo systemctl restart apache2

Es sollten nun alle Fehler und Warnungen beseitigt sein.

Auf der nächsten Seite wird nun die Authentifizierungsmethode abgefragt. Ich habe mich für die Datenbank entschieden.

Die Einstellungen für die Datenbank müssen wir nun setzen.  Icinga richtet die Datenbank und den User selber ein. Ihr müsst also nur einen User angeben, der die Rechte hat Datenbanken und User anzulegen.

Danach kommt eine Abfragen bzgl. des Authentifizierungs-Backend, die ihr weiterklicken könnt.

 

Im darauffolgenden Schritt richten wir den ersten Benutzer für die Icinga Weboberfläche ein. Die Eingabe ist dabei euch überlassen.

Die Konfiguration vom Logging kann soweit, wie von Icinga vorgegeben, verwendet werden.

Die Einrichtung von Icingaweb2 wäre nun abgeschlossen und es folgt eine Übersicht der getätigten Konfigurationen.

 

Icinga2-IDO

Kommen wir nun zur Einrichtung von Icinga2-IDO.

Vorweg möchte ich sagen, dass wir für die IDO eine Datenbank einrichten müssen. Wie ihr das macht habe ich unter “Postgres Datenbank installieren und einrichten” beschrieben. Da die IDO Einrichtung nichts weiteres verlangt, mache ich hier mit der Visualisierung weiter.

 

Sobald die Einrichtung abgeschlossen ist, könnt ihr die Verbindung validieren.

“Es gibt zurzeit keine Icinga-Instanz die in die IDO schreibt. Stelle sicher, dass eine Icinga-Instanz konfiguriert ist und in die IDO schreiben kann” – Falls ihr diese Meldung bekommt, dann solltet ihr unbedingt /etc/icinga2/features-enabled/ido-pgsql.conf überprüfen. Dort sollten die Einstellungen deckungsgleich mit denen sein, die ihr vorher in der Web-Konfiguration festgelegt habt.

 

/**
 * The db_ido_pgsql library implements IDO functionality
 * for PostgreSQL.
 */

library "db_ido_pgsql"

object IdoPgsqlConnection "ido-pgsql" {
  user = "user",
  password = "password",
  host = "localhost",
  database = "icinga_ido"
}

 

 

Postgres Datenbank installieren und einrichten

Zuallererst müssen natürlich die erforderlichen Pakete installiert werden.

sudo apt install postgresql 
sudo apt install icinga2-ido-pgsql

Für Icinga IDO müssen wir diesmal eine eigene Datenbank erzeugen. Dafür melden wir uns mit dem postgres User in Linux an

sudo -u postgres -i

Erstellen wir nun den User und die Datenbank

postgres@raspberrypi:~$ createuser --interactive -P
Geben Sie den Namen der neuen Rolle ein: user
Geben Sie das Passwort der neuen Rolle ein:
Geben Sie es noch einmal ein:
Soll die neue Rolle ein Superuser sein? (j/n) n
Soll die neue Rolle Datenbanken erzeugen dürfen? (j/n) n
Soll die neue Rolle weitere neue Rollen erzeugen dürfen? (j/n) n
postgres@raspberrypi:~$ createdb -O user icinga_ido
psql -d icinga_ido -f /usr/share/icinga2-ido-pgsql/schema/pgsql.sql

Noch ein letzter Kontrollblick, ob alles korrekt erstellt worden ist. Dafür verwenden wir den psql Befehl und im weiteren \du für die Liste der Rollen(User) bzw. \l für die Liste der Datenbanken.

psql
postgres=# \du
                                      Liste der Rollen
 Rollenname |                            Attribute                            | Mitglied von
------------+-----------------------------------------------------------------+--------------
 postgres   | Superuser, Rolle erzeugen, DB erzeugen, Replikation, Bypass RLS | {}
 user       | Passwort gültig bis infinity                                    | {}

postgres=# \l
                                  Liste der Datenbanken
    Name    | Eigentümer | Kodierung | Sortierfolge | Zeichentyp  |  Zugriffsprivilegien
------------+------------+-----------+--------------+-------------+-----------------------
 icinga_ido | user       | UTF8      | de_DE.UTF-8  | de_DE.UTF-8 | =Tc/user             +
            |            |           |              |             | user=CTc/user
 postgres   | postgres   | UTF8      | de_DE.UTF-8  | de_DE.UTF-8 |
 template0  | postgres   | UTF8      | de_DE.UTF-8  | de_DE.UTF-8 | =c/postgres          +
            |            |           |              |             | postgres=CTc/postgres
 template1  | postgres   | UTF8      | de_DE.UTF-8  | de_DE.UTF-8 | =c/postgres          +
            |            |           |              |             | postgres=CTc/postgres
(4 Zeilen)

 

 

Icinga2 API einrichten

Da ich die Befehle mittels API an die Monitoring-Instanz entsenden möchte, muss ich die Icinga2 API installieren.

root@raspberrypi:/home/pi# icinga2 api setup
information/cli: Generating new CA.
information/base: Writing private key to '/var/lib/icinga2/ca//ca.key'.
information/base: Writing X509 certificate to '/var/lib/icinga2/ca//ca.crt'.
information/cli: Generating new CSR in '/var/lib/icinga2/certs//raspberrypi.csr'.
information/base: Writing private key to '/var/lib/icinga2/certs//raspberrypi.key'.
information/base: Writing certificate signing request to '/var/lib/icinga2/certs//raspberrypi.csr'.
information/cli: Signing CSR with CA and writing certificate to '/var/lib/icinga2/certs//raspberrypi.crt'.
information/pki: Writing certificate to file '/var/lib/icinga2/certs//raspberrypi.crt'.
information/cli: Copying CA certificate to '/var/lib/icinga2/certs//ca.crt'.
information/cli: Adding new ApiUser 'root' in '/etc/icinga2/conf.d/api-users.conf'.
information/cli: Enabling the 'api' feature.
Enabling feature api. Make sure to restart Icinga 2 for these changes to take effect.
information/cli: Updating 'NodeName' constant in '/etc/icinga2/constants.conf'.
information/cli: Created backup file '/etc/icinga2/constants.conf.orig'.
information/cli: Updating 'ZoneName' constant in '/etc/icinga2/constants.conf'.
information/cli: Backup file '/etc/icinga2/constants.conf.orig' already exists. Skipping backup

 

Richten wir nun einen neuen API-User ein und fahren mit dem Websetup fort:

vi /etc/icinga2/conf.d/api-users.conf

object ApiUser "user" {
  password = "password"
  // client_cn = ""

  permissions = [ "*" ]
}

Diesen gerade erstellten Login tragen wir in das Websetup ein.

Falls alles soweit geklappt hat, können wir mittels curl die API testen. Icinga sollte mit einem kleinem Response antworten.

curl -k -s -u icinga:password 'https://localhost:5665/v1' 
<html><head><title>Icinga 2</title></head><h1>Hello from Icinga 2 (Version: r2.10.5-1)!</h1><p>You are authenticated as <b>user</b>. Your user has the following permissions:</p> <ul><li>*</li></ul><p>More information about API requests is available in the <a href="https://docs.icinga.com/icinga2/latest" target="_blank">documentation</a>.</p></html>

 

Das Setup wäre nun abgeschlossen. Icinga ist lauffähig, hat bislang aber nur den PI als überwachten Host. Wie die Objektpflege über die API passiert, erkläre ich in einem neuen Beitrag, damit dieser nicht zu lange wird :).

 

Gutes gelingen!

check_mk Update durchführen

Das Update von check_mk verhält sich ein wenig anders als man es gewohnt ist. Es muss zuerst dass jeweilige Paket runtergeladen und installiert werden. Achtet dabei auf die Version, die ihr haben wollt und die Edition, die ihr benötigt. Nehmt aus der unteren Tabelle das Editionskürzel. CRE ist dabei die kostenlose Variante. Auf der Seite werden die unterstützten Distributionen aufgelistet. Ladet eure richtige Version herunter.

https://checkmk.de/download_version.php?&version=1.6.0&edition=cee

Die Dateiendung .cee steht für Checkmk Enterprise Edition. Neben dieser gibt es noch

.cre Checkmk Raw Edition
.demo Demo Version der Checkmk Enterprise Edition
.cme Checkmk Managed Services Edition

 

wgett https://checkmk.de/support/1.6.0/check-mk-enterprise-1.6.0_0.xenial_amd64.deb

Überprüfen wir die aktuelle check_mk Version auf dem Server, sehen wir noch die anderen Versionen die zur Verfügung stehen. Die Besonderheit an check_mk ist, dass wir alle Instanzen(Sites) mit einer unterschiedlichen Version laufen lassen könnten.

omd versions

Ausgabe:
1.2.8p18.cee
1.4.0b4.cee
1.4.0p5.cee (default)

Listen wir die Sites auf, so sehen wir, dass wir 2 zur Verfügung haben.

omd sites

Ausgabe:
SITE            VERSION          COMMENTS
Testsite2       1.4.0p5.cee      default version
checkmk         1.4.0p5.cee      default version

Da jede Instanz einen gleichnamigen User in Linux erzeugt, habe ich mir die passwd ebenfalls angeschaut, ob der relevante User immer noch vorhanden ist. Dieser Schritt ist allerdings optional und wird nicht benötigt.

grep omd /etc/passwd

checkmk:x:999:1001:OMD site checkmk:/omd/sites/checkmk:/bin/bash
Testsite2:x:997:1006:OMD site Testsite2:/omd/sites/Testsite2:/bin/bash

Zur Installation benutzen wir dpkg mit dem Parameter -i.

sudo dpkg -i check-mk-raw-1.6.0_0.bionic_amd64.deb

Überprüfen wir nun ein weiteres mal die check_mk Versionen, sehen wir die gerade installierte.

sudo omd versions
1.2.8p18.cee
1.4.0b4.cee
1.4.0p5.cee
1.6.0.cee (default)

Die Instanzen werden nicht automatisch auf die neuste Version gebracht. Damit dies geschieht, müssen wir uns vorerst als den Site User anmelden.

sudo su <Instanzuser>

Geben wir

omd version

ein, so sehen wir, dass sich für diese Instanz die Version nicht geändert hat, was wir nun nachholen.

sudo su checkmk
OMD[checkmk]:~$ omd version
OMD - Open Monitoring Distribution Version 1.4.0p5.cee

Achtet bitte darauf, dass ihr immer noch als der Instanz User angemeldet seid!

Zuerst müssen wir die Instanz stoppen.

omd stop

Removing Crontab...OK
Stopping apache...killing 1393....OK
Stopping nagios....OK
Stopping npcd...OK
Stopping rrdcached...waiting for termination...OK
Stopping mkeventd...killing 1286...OK
Stopping 1 remaining site processes...OK

 

Das Update

Nach dem die Instanz gestoppt ist, können wir mit dem Update beginnen.

omd update

Es sollte ein Abfragefenster mit einer Warnung auftauchen, in der ihr explizit das Update genehmigen müsst.

 

Nachdem bestätigt wurde, wird das Update für die Instanz installiert.

So sah bei mir die Ausgabe aus:

* Updated        .profile
 * Installed link var/dokuwiki/lib/plugins/cli.php
 * Installed dir  local/share/check_mk/web/htdocs/themes
 * Installed dir  etc/stunnel
 * Merged         etc/mk-livestatus/xinetd.conf
 * Updated        etc/nagvis/nagvis.ini.php
 * Updated        etc/dokuwiki/dokuwiki.php
 * Updated        etc/dokuwiki/mime.conf
 * Updated        etc/dokuwiki/local.php
 * Installed link etc/rc.d/85-stunnel
 * Installed file etc/logrotate.d/stunnel
 * Updated        etc/check_mk/apache.conf
 * Updated        etc/init-hooks.d/README
 * Updated        etc/apache/apache.conf
 * Installed file etc/apache/conf.d/security.conf
 * Updated        etc/apache/conf.d/omd.conf
 * Installed file etc/apache/conf.d/01_wsgi.conf
 * Installed file etc/init.d/stunnel
 * Installed file etc/stunnel/server.conf
 * Vanished       etc/icinga/ssi/extinfo-header.ssi
 * Vanished       etc/icinga/ssi/status-header.ssi
 * Vanished       etc/icinga/ssi/README
 * Vanished       etc/icinga/icinga.d/omd.cfg
 * Vanished       etc/icinga/icinga.d/timing.cfg
 * Vanished       etc/icinga/icinga.d/mk-livestatus.cfg
 * Vanished       etc/icinga/icinga.d/flapping.cfg
 * Vanished       etc/icinga/icinga.d/obsess.cfg
 * Vanished       etc/icinga/icinga.d/misc.cfg
 * Vanished       etc/icinga/icinga.d/retention.cfg
 * Vanished       etc/icinga/icinga.d/logging.cfg
 * Vanished       etc/icinga/icinga.d/freshness.cfg
 * Vanished       etc/icinga/icinga.d/dependency.cfg
 * Vanished       etc/icinga/icinga.d/eventhandler.cfg
 * Vanished       etc/icinga/icinga.d/tuning.cfg
 * Vanished       etc/icinga/idomod.cfg-sample
 * Vanished       etc/icinga/apache.conf
 * Vanished       etc/icinga/cgiauth.cfg
 * Vanished       etc/icinga/resource.cfg
 * Vanished       etc/icinga/icinga.cfg
 * Vanished       etc/icinga/config.inc.php
 * Vanished       etc/icinga/cgi.cfg
 * Vanished       etc/icinga/icinga.d
 * Vanished       etc/icinga/conf.d
 * Vanished       etc/icinga/ssi
 * Vanished       etc/init.d/icinga
 * Vanished       etc/apache/conf.d/01_python.conf
 * Vanished       etc/rc.d/80-icinga
 * Vanished       etc/icinga
 * Vanished       local/share/icinga/htdocs
 * Vanished       local/share/icinga
 * Vanished       local/lib/icinga
 * Vanished       var/icinga
Executing update-pre-hooks script "cmk.update-pre-hooks"...OK
Output: Initializing application...
Loading GUI plugins...
Updating Checkmk configuration...
 + Rewriting WATO tags...
 + Rewriting WATO hosts and folders...
 + Rewriting WATO rulesets...
 + Rewriting autochecks...
Done

Finished update.

Das Update wäre nun vollzogen und wir können die Instanz wieder starten:

omd start

Creating temporary filesystem /omd/sites/test/tmp...OK
Starting mkeventd...OK
Starting rrdcached...OK
Starting npcd...OK
Starting nagios...OK
Starting apache...OK
Initializing Crontab...OK

 

Nach Abschluss sollte der Status und die Version ein weiteres Mal überprüft werden.

OMD[test]:~$ omd status
mkeventd:       running
rrdcached:      running
npcd:           running
nagios:         running
apache:         running
crontab:        running
-----------------------
Overall state:  running


OMD[test]:~$ omd version
OMD - Open Monitoring Distribution Version 1.6.0.cre

 

Ihr solltet unbedingt die Release Notes auf etwaige Inkompatibilitäten überprüfen, sowie Änderungen, die anfallen.

multiple Datenbanken mit Django

Django hat durch sein MVC(Model-View-Controller) von Beginn an eine Datenbank mit am Board. Mein Projekt basiert auf mysql, jedoch hatte ich weitere Datenbanken, die ich über Django managen wollte, die allerdings nicht in der Default DB von Django sind. Daher müssen diese Django bekannt gemacht und Models für diese Datenbanken erstellt werden.

Die Bekanntmachung findet in der settings.py statt. Hier trägt man die weiteren DB Verbindungen ein. Dabei könnte es wie folgt aussehen. In dem DATABASE Dictionary können neben dem Default weitere Werte eingetragen werden. Die ENGINE bestimmt dabei, ob mysql, sqlite oder die anderen unterstützen DBs eingetragen werden. Im NAME steht der Name der Datenbank. User und Password sind selbsterklärend, genauso wie der HOST und der PORT. Ich habe insgesamt 2 weitere Datenbanken hinzugefügt.

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'OPTIONS': {
                'read_default_file': '~/Projekte/controlpanel/controlpanel_root/controlpanel/my.conf',
        },
    },
    'Weather': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'Weather',
        'USER': 'user',
        'PASSWORD': 'password',
        'HOST': '127.0.0.1',
        'PORT': '3306'
    },
     'Metabase': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'Metabase',
        'USER': 'user',
        'PASSWORD': 'password',
        'HOST': '127.0.0.1',
        'PORT': '3306'

    }
}

Damit mit den neuen Datenbanken gearbeitet werden kann, müssen Models erzeugt werden. Django bietet dazu ein eigenständiges Werkzeug an, welches über die Tabellen geht und die Atribute niederschreibt. Führt ihr folgendes Befehl aus,

python3 manage.py inspectdb --database Weather

sollte im Terminal der Aufbau vom Model stehen, wie Django es aufbauen würde. Bei mir sieht es so aus:

 

# This is an auto-generated Django model module.
# You'll have to do the following manually to clean this up:
#   * Rearrange models' order
#   * Make sure each model has one field with primary_key=True
#   * Make sure each ForeignKey has `on_delete` set to the desired behavior.
#   * Remove `managed = False` lines if you wish to allow Django to create, modify, and delete the table
# Feel free to rename the models, but don't rename db_table values or field names.
from django.db import models


class CurWeather(models.Model):
    id = models.AutoField(db_column='ID', primary_key=True)  # Field name made lowercase.
    c_lon = models.DecimalField(db_column='C_LON', max_digits=5, decimal_places=2)  # Field name made lowercase.
    c_lat = models.DecimalField(db_column='C_LAT', max_digits=5, decimal_places=2)  # Field name made lowercase.
    w_main = models.CharField(db_column='W_MAIN', max_length=50)  # Field name made lowercase.
    w_description = models.CharField(db_column='W_DESCRIPTION', max_length=100)  # Field name made lowercase.
    w_icon = models.CharField(db_column='W_ICON', max_length=50)  # Field name made lowercase.
    m_temp = models.DecimalField(db_column='M_TEMP', max_digits=4, decimal_places=2)  # Field name made lowercase.
    m_pressure = models.DecimalField(db_column='M_PRESSURE', max_digits=7, decimal_places=2)  # Field name made lowercase.
    m_humidity = models.DecimalField(db_column='M_HUMIDITY', max_digits=7, decimal_places=2)  # Field name made lowercase.
    m_temp_min = models.DecimalField(db_column='M_TEMP_MIN', max_digits=7, decimal_places=2)  # Field name made lowercase.
    w_speed = models.DecimalField(db_column='W_SPEED', max_digits=4, decimal_places=2)  # Field name made lowercase.
    w_deg = models.DecimalField(db_column='W_DEG', max_digits=5, decimal_places=2)  # Field name made lowercase.
    rain_3h = models.DecimalField(db_column='RAIN_3H', max_digits=5, decimal_places=2, blank=True, null=True)  # Field name made lowercase.
    cloudiness = models.DecimalField(db_column='CLOUDINESS', max_digits=5, decimal_places=2)  # Field name made lowercase.
    dt_timestamp = models.DateTimeField(db_column='DT_TIMESTAMP')  # Field name made lowercase.
    sys_country = models.CharField(db_column='SYS_COUNTRY', max_length=4)  # Field name made lowercase.
    sys_sunrise = models.DateTimeField(db_column='SYS_SUNRISE')  # Field name made lowercase.
    sys_sunset = models.DateTimeField(db_column='SYS_SUNSET')  # Field name made lowercase.
    timezone = models.IntegerField(db_column='TIMEZONE')  # Field name made lowercase.
    city = models.ForeignKey('WeaCity', models.DO_NOTHING, db_column='CITY_ID')  # Field name made lowercase.
    created_at = models.DateTimeField(db_column='CREATED_AT')  # Field name made lowercase.

    class Meta:
        managed = False
        db_table = 'CUR_WEATHER'


class WeaCity(models.Model):
    id = models.IntegerField(db_column='ID', primary_key=True)  # Field name made lowercase.
    city_name = models.CharField(db_column='CITY_NAME', max_length=100)  # Field name made lowercase.

    class Meta:
        managed = False
        db_table = 'WEA_CITY'


class WeaOpenweather(models.Model):
    id = models.AutoField(db_column='ID', primary_key=True)  # Field name made lowercase.
    query = models.CharField(db_column='QUERY', max_length=500)  # Field name made lowercase.
    city_id = models.IntegerField(db_column='CITY_ID')  # Field name made lowercase.

    class Meta:
        managed = False
        db_table = 'WEA_OPENWEATHER'

 

Diese Ausgabe sollte nun in eine Datei geleitet werden und schon existiert für die DB das Model.

python3 manage.py inspectdb --database Weather > pages/Weather.py

Allerdings nimmt Django nicht die komplette Arbeit ab. Wir müssen noch überprüfen, dass jedes Model mindestens ein Feld hat, wo das PrimaryKey Attribute True ist. Wenn Django Zeilen hinzufügen, entfernen und ändern darf, muss das managed Attribute noch auf True umgestellt werden.