openweather

Ich war auf der Suche nach einen kostenlosen Onlineservice um Wetterdaten via API abzugreifen. Ziel ist es, ein lokales Chart über das Wetter zu bekommen, welches über Jahre auswertbar ist. Mit den erworbenen Wetterdaten möchte ich vor allem die Windgeschwindigkeiten Vorort im Auge behalten. Fündig geworden bin ich bei Openweather. Um den Service und somit die API abzufragen, benötigt ihr ein Key. Dieser muss bei jeder Abfrage angegeben werden. Den Key erhält man bei der Registrierung im Portal. Ihr könnt dort ebenfalls zur kostenpflichtigen Version wechseln. Diese bietet gegenüber der Kostenfreien mehr Funktionen an. Für mich reicht die kostenlose Variante. Die Wetterdaten werden jede Stunde über die API abgefragt und bei mir lokal in eine Datenbank gespeichert.

Damit der http Request korrekt verläuft, muss unter anderem der vorher erlangte Key in jeder Abfrage verwendet werden.

http://api.openweathermap.org/data/2.5/weather?q=berlin,de&APPID=<key>

Dieser Request gibt ein JSON Packet als Response zurück, der wie folgt aussieht:

{"coord":{"lon":13.39,"lat":52.52},"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04n"}],"base":"stations","main":{"temp":279,"feels_like":274.77,"temp_min":277.59,"temp_max":279.82,"pressure":1032,"humidity":75},"visibility":10000,"wind":{"speed":3.6,"deg":300},"clouds":{"all":75},"dt":1577818777,"sys":{"type":1,"id":1262,"country":"DE","sunrise":1577776641,"sunset":1577804447},"timezone":3600,"id":2950159,"name":"Berlin","cod":200}

Die Parameter sind sehr gut auf der Openweather Seite beschrieben. Um mehr darüber zu erfahren, folgt diesen Link:
https://openweathermap.org/weather-data

Nur das JSON Packet alleine reicht natürlich nicht. Es muss ein komplettes Grundgerüst, bestehend aus Datenbank und Code der die Abfrage triggert, aufgebaut werden.

Datenbank Schema

Das Datenbank Schema, welches zurzeit existiert, ist relativ simple aufgebaut. Es besteht aus 3 Tabellen, eine für die Wetterdaten und die anderen beiden dienen zum Erstellen des Requests.

use Weather;

SET FOREIGN_KEY_CHECKS = 0;

DROP TABLE IF EXISTS CUR_WEATHER;
DROP TABLE IF EXISTS WEA_OPENWEATHER;
DROP TABLE IF EXISTS WEA_CITY;

SET FOREIGN_KEY_CHECKS = 1;

CREATE TABLE IF NOT EXISTS WEA_CITY(
  ID int(10) NOT NULL,
  CITY_NAME varchar(100) NOT NULL,
  PRIMARY KEY(ID)
);

CREATE TABLE IF NOT EXISTS CUR_WEATHER(
  ID int(14) NOT NULL auto_increment,
  C_LON decimal(5,2) NOT NULL,
  C_LAT decimal(5,2) NOT NULL,
  W_MAIN varchar(50) NOT NULL,
  W_DESCRIPTION varchar(100) NOT NULL,
  W_ICON varchar(50) NOT NULL,
  M_TEMP decimal(4,2) NOT NULL,
  M_PRESSURE decimal(7,2) NOT NULL,
  M_HUMIDITY decimal(7,2) NOT NULL,
  M_TEMP_MIN decimal(7,2) NOT NULL,
  W_SPEED decimal(4,2) NOT NULL,
  W_DEG decimal(5,2) NOT NULL,
  RAIN_3H decimal(5,2),
  CLOUDINESS decimal(5,2) NOT NULL,
  DT_TIMESTAMP datetime NOT NULL,
  SYS_COUNTRY varchar(4) NOT NULL,
  SYS_SUNRISE datetime NOT NULL,
  SYS_SUNSET datetime NOT NULL,
  TIMEZONE int(15) NOT NULL,
  CITY_ID int(10) NOT NULL,
  CREATED_AT datetime NOT NULL,
  PRIMARY KEY(ID),
  FOREIGN KEY(CITY_ID) REFERENCES WEA_CITY(ID)
);

CREATE TABLE IF NOT EXISTS WEA_OPENWEATHER(
  ID int(5) NOT NULL auto_increment,
  QUERY varchar(500) NOT NULL,
  CITY_ID int(10) NOT NULL,
  PRIMARY KEY(ID)
);

Shell Code

Der Shell Code ist dafür verantwortlich, dass das Python Script angestoßen wird und somit die Wetterdaten in die Datenbank landen.
Dabei wird der Code als Cronjob ausgeführt und jede Stunde getriggert. Die Ausgabe wird dabei in die Weather.log gepiped.

#!/bin/bash

echo "`date +%d.%m.%y\ %T` ===weather cronjob execution==== " >> /var/log/Weather.log
python3 ~/Projekte/Weather/WEATHER.py >> /var/log/Weather.log

exit

Python Code

Die benötigten Pakete für Python habe ich über pip3 installiert:

apt install python3-pip
pip3 install requests
pip3 install mysql-connector-python
pip3 install termcolor
pip3 install python-dateutil

Der Python Code versucht zuerst die benötigten Module zu laden und springt in eine Exception, sobald eins davon nicht vorhanden ist. Verwendet wird hier ebenfalls die von mir erstellte Datenbank Klasse PDB, welche ich in diesem Artikel beschrieben habe. Nach dem die Module geladen sind, folgt die Abfrage gegen die Datenbank, um die erforderliche Query für die Openweather API zu erhalten.  In die Variable response wird das JSON Objekt geschrieben, welches später mittels json.loads in WeatherVar geschrieben wird. Danach wird mi DB.QueryInsert ein neuer Eintrag in der Datenbank erzeugt und die Verbindung wieder geschlossen.

try:
        import json
        import requests
        import PDB
        from termcolor import colored
        from datetime import datetime
except:
        print("Python could not load all required modules")
        sys.exit(1)


DB=OpenweatherQuery=response=WeatherVar = None

if DB is None:
        DB = PDB.databaseconnection('localhost','user','password','Weather')

DB.Connect()

if OpenweatherQuery is None:
        OpenweatherQuery = DB.QuerySelect("select QUERY from WEA_OPENWEATHER where CITY_ID = 6552907", None,False)

try:
        if response is None:
                response = requests.get(OpenweatherQuery[0][0])

except requests.exceptions.RequestException as e:
        print(e)
        sys.exit(1)

print(colored("[I] + " + OpenweatherQuery[0][0],"green"))
print(colored("[I] + " + response.text,"green"))

if WeatherVar is None:
        WeatherVar = json.loads(response.text)

if not DB.QueryInsert("insert CUR_WEATHER(%s) VALUES(%s)",{"C_LON":WeatherVar["coord"]["lon"],"C_LAT":WeatherVar["coord"]["lat"],"W_MAIN":WeatherVar["weather"][0]["main"],"W_DESCRIPTION":WeatherVar["weather"][0]["description"],"W_ICON":WeatherVar["weather"][0]["icon"],"M_TEMP":float(WeatherVar["main"]["temp"]) - 273.15,"M_PRESSURE":WeatherVar["main"]["pressure"],"M_HUMIDITY":WeatherVar["main"]["humidity"],"M_TEMP_MIN":float(WeatherVar["main"]["temp_min"]) -273.15,"W_SPEED":float(WeatherVar["wind"]["speed"]) * 3.6,"W_DEG":WeatherVar["wind"]["deg"],"RAIN_3H":"0","CLOUDINESS":WeatherVar["clouds"]["all"],"DT_TIMESTAMP":datetime.utcfromtimestamp(WeatherVar["dt"]).strftime('%Y-%m-%d %H:%M:%S'),"SYS_COUNTRY":WeatherVar["sys"]["country"],"SYS_SUNRISE":datetime.utcfromtimestamp(WeatherVar["sys"]["sunrise"]).strftime('%Y-%m-%d %H:%M:%S'),"SYS_SUNSET":datetime.utcfromtimestamp(WeatherVar["sys"]["sunset"]).strftime('%Y-%m-%d %H:%M:%S'),"TIMEZONE":WeatherVar["timezone"],"CITY_ID":WeatherVar["id"],"CREATED_AT":str(datetime.now())}, True):
        print("Insert query went wrong for some reason, See Error")

DB.Disconnect()

 

Django mit Mysql Datenbank

Django

Django benötigt für das Model Prinzip eine eigene Datenbank. Standardmäßig hat es die SQLite Datenbank mit am Board , jedoch kann die Datenbank auch auf eine besser skalierbare Datenbank wie PostgreSQL, MySQL oder Oracle laufen.
Ich habe mich gegen die SQLite Version und für die MySQL Datenbank entschieden, daher gehe ich in diesem Beitrag auf die MySQL Integrierung ein. SQLite sollte unter keinen Umständen in einer produktiv Umgebung genutzt werden!

Um Mysql verwenden zu können, benötigen wir ein DB API Treiber wie mysqlclient. Mysqlclient ist ebenfalls die bevorzugte Wahl von Django. Die dazugehörige Dokumentation von Django kann unter dem Tutorial Part2 gefunden werden. Hier wird explizit auf die Integration der Datenbank eingegangen, wie eine anderen Datenbank Version verwendet werden kann und wie Models erzeugt und behandelt werden.

Den Link zum Tutorial findet ihr hier: https://docs.djangoproject.com/en/2.2/intro/tutorial02/

Um die DB API einrichten zu können, müssen einige Abhängigkeiten installiert werden:

sudo apt-get install python3-dev default-libmysqlclient-dev

gefolgt von dem eigentlichen mysqlclient Paket, welches mit pip installiert wird

pip3 install mysqlclient

Nach der Installation kann die Datenbank, mit den MySQL Utilities, eingerichtet werden.

CREATE DATABASE <dbname> CHARACTER SET utf8;

Auch der Datenbank User sollte nur Berechtigungen auf die für Django bereitgestellte Datenbank haben. Dafür bedienen wir uns bei dem grant Befehl von mysql.

GRANT ALL PRIVILEGES ON <database_name>.* TO '<username>'@'localhost';

Die Django Settings müssen für die MySQL Benutzung angepasst und bearbeitet werden. Dafür müssen wir die Settings.py vom Django-Projekt öffnen und wie unten angegeben, abändern.

# settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'OPTIONS': {
            'read_default_file': '/path/to/my.cnf',
        },
    }
}

Die my.cnf Datei beinhaltet die Zugangsdaten für die Datenbank,  um eine Verbindung mit der Datenbank herzustellen.

# my.cnf
[client]	
database = NAME
user = USER
password = PASSWORD
default-character-set = utf8

Für jede installierte App in Settings.py, auch wenn es die Standardapps von Django sind, wird eine eigene Tabelle erstellt. Nachdem die API und die Einstellungen angepasst worden sind, kann die eigentliche Django Migration gestartet werden.

sudo python3 manage.py migrate

Die migrate Funktion schaut in der Settings.py nach allen installierten Apps und erstellt eine Tabelle in der Datenbank. Dieser Befehl sollte ohne Unterbrechung durchlaufen.

MySQL ist nach erfolgreicher Migrierung die Hauptdatenbank.

Shell Backup Script erzeugen

Backups sind wohl eines der wichtigsten Dinge auf einem Server und so darf es natürlich auch nicht auf meinem Server fehlen. Statt einer Software Solution habe ich mir meine mit Hilfe von Shell selbst geschrieben. Das Script ist relativ schmal gehalten, erledigt aber alles mit bravour. Es hat eine Vorhalte Zeit von 5 Tagen welches in der Variable mindir abgespeichert ist. Es werden also insgesamt die letzten 5 Backups beibehalten, die älteren Backups werden automatisch gelöscht.  Die For Schleife beinhaltet alle Ordner die gesichert werden sollen.

Das Interessante ist der find Aufruf, um die ältesten Backups zu finden und diese aufzulisten

`find ${dir} -maxdepth 1 -type d -printf '%T+ %p\n' | sort -n | head -n ${t} | awk '{print $2}'`

Dieser Befehl geht in das Verzeichnis, welches in der Variable “dir” angegeben worden ist. Mit maxdepth ist die Tiefe angegeben. Der Befehl bleibt also nur im obersten Ordner. Mit type suchen wir nur nach Ordnern. Anhand printf wird das Format der Ausgabe festgelegt. Diese Informationen werden nach sort gepiped, wo die Informationen noch sortiert werden, währen head dann die Ordner heraussucht, die gelöscht werden dürfen. Das awk sucht dann nur noch den Ordnerpfad aus dem String heraus.

Ich habe im ganzen Code noch jede Stelle einzeln kommentiert, damit es besser verstanden werden kann.

#!/bin/sh

#where to save the backup
dir="/root/backup/"
#min number of backups to keep
mindir=5

d=`date +%Y%m%d`
#get the current number of the backup existing
curnumber=`find /root/backup -maxdepth 1 -type d | wc -l`

#check if root backup dir exists
if [ ! -d "${dir}" ]
then
        #if not create
        mkdir ${dir}
        echo "backupfolder does not exists - created" >> /var/log/backup.log
fi

#check if backupfolder exists
if [ ! -d "${dir}${d}" ]
then
        #if not create
        mkdir ${dir}${d}/
        echo "create folder ${d}" >> /var/log/backup.log
        #add folders to be backed up here. Loop iterates over the folders then to back them up
        for f in "/root/Projekte/" "/root/client-configs/" "/root/openvpn-ca/"
        do
                #copy defined folders into the backup folder
                cp -p -R -P ${f} ${dir}${d}
                echo "cp -p -R -P ${f} ${dir}${d}" >> /var/log/backup.log
        done
fi

#to not overblowing the server capacity the backup system only keeps the last ${mindir} backups.
# checks if the current number is greater than or equal to ${mindir}
if [ $(($curnumber - 1 )) -ge ${mindir} ]
then
        #get the numbers of how many backups can be deleted
        t=$((${curnumber} - ${mindir} -1 ))
        #find the last ${t} backups
        lastbackup=`find ${dir} -maxdepth 1 -type d -printf '%T+ %p\n' | sort -n | head -n ${t} | awk '{print $2}'`
        echo "backups to delete: ${lastbackup}" >> /var/log/backup.log
        #delete those backups
        rm -r -f ${lastbackup}
fi

exit 0

Dieses Script kann dann z.B. als Cronjob angelegt werden, damit regelmäßig Backups gemacht werden. Bei mir läuft das Script z.B. jeden Tag um 0:55, dies ist jedem allerdings selbst überlassen.

Die Ausführung, bzw. die Ausgaben des Scripts werden dann unter /var/log/backup.log abgespeichert. Somit kann im Fehlerfall überprüft werden, was denn zu dem Fehler geführt hat. Diese Log Datei befindet sich in einer sogenannten Rotation, damit die Log nicht aufgebläht wird.

 

 

 

Python Datenbank Klasse Update

In meiner Datenbank Klasse habe ich lange Zeit die Update Funktion missen lassen. Hier gelangt ihr zum alten Artikel. Einfach aus dem Grund weil ich sie vorher noch nicht benötigt habe. Das habe ich hiermit nun nachgezogen(Eigentlich existiert sie schon wesentlich länger, aber ich habe jetzt erst den Artikel verfasst 😉 ). Das Update kann separat aufgerufen werden, macht aber auch Sinn in Zusammenhang mit einem Insert. Falls dort z.B. ein Primärschlüssel schon existiert, kann dieser geupdated werden.

Für ein internes Projekt habe ich folgende Befehlszeilen dafür benutzt

if not DB.QueryInsert("insert into AUX_PROJECTS(%s) VALUES(%s)",{"ID":PList[i][0],PList[0][1]:PList[i][1],"ZINS":str(PList[i][2]).replace(",","."),PList[0][3]:PList[i][3],"ANLAGE":PList[i][4],"STATUS":PList[i][5],"SCORE":PList[i][6],PList[0][7]:PList[i][7],PList[0][8]:PList[i][8],"UNIT":PList[i][9],"UPLOADED_AT":PList[i][10]}, True):
                DB.QueryUpdate("update AUX_PROJECTS SET %s where %s",{PList[0][1]:PList[i][1],"ZINS":str(PList[i][2]).replace(",","."),"ANLAGE":PList[i][4]},"ID", PList[i][0])

Das Insert erwahrtet wie gewohnt die insert Query gefolgt von einem Tupel mit den Namen und den Werten. Als 3.ten Parameter kann noch das Update auf True oder False gesetzt werden, dies triggert allerdings zurzeit nur eine Ausgabe. Wenn dieses Insert nicht funktioniert, bzw. ein False zurückgibt, versucht er ein Update. Der Aufbau ist ähnlich wie beim Insert. Zuerst die Update Query, gefolgt von dem Tupel.

Eine Update könnte dann wie folgt in der Shell angezeigt werden:

Hier der komplette neue Code.

import mysql.connector
from mysql.connector import errorcode
from termcolor import colored

class databaseconnection:
    host=username=password=database=cnx=cursor=None

    def __init__(self,host,username,password,database):
        self.host = host
        self.username = username
        self.password = password
        self.database = database

    def Connect(self):
        config = {
            'user': self.username,
            'password': self.password,
            'host': self.host,
            'database': self.database
            }
        try:
            self.cnx = mysql.connector.connect(**config)
            self.cursor = self.cnx.cursor()
            print(colored("[I] + DB Connection established","green"))
        except mysql.connector.Error as err:
            if err.errno == errorcode.ER_ACCESS_DENIED_ERROR:
                print(colored("[C] + Wrong Username/Password","red"))
            elif err.errno == errorcode.ER_BAD_DB_ERROR:
                print(colored("[C] + Database Error","red"))
            else:
                print(colored("[C] + " + err,"red"))

    def QuerySelect(self,sqlstatement, params, header):
        if (params is not None):
            try:
                self.cursor.execute(sqlstatement, (params,))
                rtnvalue = self.cursor.fetchall()
                print(colored("[I] + executed select with params","green"))
                if header:
                    rtnvalue.insert(0,self.cursor.column_names)
                return rtnvalue
            except:
                print(colored("[C] + something went wrong while executing select with params:","red"))
                return False
        else:
            try:
                self.cursor.execute(sqlstatement)
                rtnvalue = self.cursor.fetchall()
                print(colored("[I] + executed select without params: " + sqlstatement,"green"))
                if header:
                    rtnvalue.insert(0,self.cursor.column_names)
                return rtnvalue
            except:
                print(colored("[C] + something went wrong while executing select without params","red"))
                return None

    def QueryInsert(self, sqlstatement, params, update):
        if (params is not None):
            columns = ', '.join(params.keys())
            values = params.values()
            sql = sqlstatement % (columns, ', '.join(repr(e) for e in values))
            print(colored("[I] + " + sql,"yellow"))
            try:
                self.cursor.execute(sql)
            except mysql.connector.Error as er:
                if er.errno == errorcode.ER_DUP_ENTRY:
                    if(update == True):
                        print(colored("[W] + transaction rolled back: Duplication found - Try update " ,"yellow"))
                    else:
                        print(colored("[W] + transaction rolled back: Duplication found " ,"yellow"))
                    ###########try to make an update##########
                    return False
                else:
                    print(colored("[C] + a criticial error occured " + er.msg,"red"))
                    return False
        else:
            print(colored("[C] + insert execution failed due missing parameters","red"))
            return False

        try:
            self.cnx.commit()
            print(colored("[I] + transaction committed, value added","green"))
            return True
        except mysql.connector.Error as er:
            self.cnx.rollback()
            print(colored("[C] + transaction rolled back: " + er.msg,"red"))
            return False

    # performs an DB update
    # @var sqlstatement (update AUX_PROJECTS SET %s where %s)
    # @var params Data to being updated
    # @var Conditionheader Primary key
    # @var ConditionValue PK agains value
    def QueryUpdate(self, sqlstatement, params, Conditionheader,ConditionValue):
        if (params is not None):
            updatestring = self.CreateUpdateString(params)
            sql = sqlstatement % (updatestring,Conditionheader + " = " + ConditionValue)
            print(colored("[I] + " + sql,"yellow"))
            try:
                self.cursor.execute(sql)
            except mysql.connector.Error as er:
                self.cnx.rollback()
                print(colored("[C] + transaction rolled back: " + er.msg,"red"))
                return False
        else:
            print(colored("[C] + update execution failed due missing parameters","red"))
            return False

        try:
            self.cnx.commit()
            print(colored("[I] + transaction committed, value updated","green"))
            return True
        except mysql.connector.Error as er:
            self.cnx.rollback()
            print(colored("[C] + transaction rolled back: " + er.msg,"red"))
            return False

    def CreateUpdateString(self,params):
        updatestring = ''
        for key,value in params.items():
                updatestring = updatestring + key + '=' + repr(str(value))
                if(key == list(params.keys())[-1]):
                        continue

                updatestring = updatestring + ','

        return str(updatestring)

    def Disconnect(self):
        self.cnx.close()
        print(colored("[I] + Connection closed","green"))

 

 

 

 

 

 

 

matplotlib über putty ssh

Ich habe mich vor ein paar Wochen in ein edx Kurse für Data Scientist eingeschrieben und diesen erfolgreich mit einem Zertifikat bestanden. Die zu verwendete Shell hat damals DataCamp bereitgestellt. IPython kam hierbei zum Einsatz. In dem Kurs habe ich erstes Grundwissen über numpy, matplotlib und Pandas erworben. Dieses Wissen wollte ich nun an meinem vServer anwenden. Dafür habe ich zuerst matplotlib für Python3 installiert.

apt install python3-matplotlib

Zum Testen habe ich das Script von unten(Testbeispiel) benutzt. Führt man das Script direkt aus, sollte folgende Fehlermeldung kommen.

/usr/lib/python3/dist-packages/matplotlib/font_manager.py:273: UserWarning: Matplotlib is building the font cache using fc-list. This may take a moment.
  warnings.warn('Matplotlib is building the font cache using fc-list. This may take a moment.')
/usr/lib/python3/dist-packages/matplotlib/font_manager.py:273: UserWarning: Matplotlib is building the font cache using fc-list. This may take a moment.
  warnings.warn('Matplotlib is building the font cache using fc-list. This may take a moment.')
Traceback (most recent call last):
  File "test.py", line 6, in <module>
    plt.plot(x, x, label='linear')
  File "/usr/lib/python3/dist-packages/matplotlib/pyplot.py", line 3147, in plot
    ax = gca()
  File "/usr/lib/python3/dist-packages/matplotlib/pyplot.py", line 928, in gca
    return gcf().gca(**kwargs)
  File "/usr/lib/python3/dist-packages/matplotlib/pyplot.py", line 578, in gcf
    return figure()
  File "/usr/lib/python3/dist-packages/matplotlib/pyplot.py", line 527, in figure
    **kwargs)
  File "/usr/lib/python3/dist-packages/matplotlib/backends/backend_tkagg.py", line 84, in new_figure_manager
    return new_figure_manager_given_figure(num, figure)
  File "/usr/lib/python3/dist-packages/matplotlib/backends/backend_tkagg.py", line 92, in new_figure_manager_given_figure
    window = Tk.Tk()
  File "/usr/lib/python3.5/tkinter/__init__.py", line 1871, in __init__
    self.tk = _tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use)
_tkinter.TclError: no display name and no $DISPLAY environment variable

Der Fehler besagt, dass kein “Display” gefunden wird, in dem der Graph angezeigt werden kann.
Wir rufen das Script ja schließlich über die Shell auf. Es fehlt dementsprechend ein xServer, der den Output zu einem Client tunnelt. Dazu sind ein paar Anpassungen auf dem Server als auch am Client notwendig.

Server-Seite

Auf dem Server muss unter anderem das xauth Paket installiert werden.

apt install xauth

Neben dem Paket ist auch eine Änderung an der sshd_config notwendig, damit ssh den eigentlichen Output auch weiterleitet. Dazu fügt die 2 Parameter hinzu oder ändert sie ab.

vi /etc/ssh/sshd_config

X11Forwarding yes
X11DisplayOffset 10

 

Client-Seite

Auf der Client Seite sollte ein xServer installiert sein. Sehr beliebt ist hier Xming. Xming ist hier zu bekommen.

Die Installation ist schnell und einfach. Nach ein paar Einstellungen die zu treffen sind, ist der xServer auch schnell installiert.

 

 

 

 

 

Auf der Putty Shell im Client muss der X11 noch aktiviert werden. Dazu tickt den “Enable X11 forwarding” an.

 

TestBeispiel

Hier das Testbeispiel, welches ich benutzt habe, um den xServer zu testen. Das Scipt habe ich dabei von hier: https://matplotlib.org/3.1.1/tutorials/introductory/usage.html#sphx-glr-tutorials-introductory-usage-py

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 2, 100)

plt.plot(x, x, label='linear')
plt.plot(x, x**2, label='quadratic')
plt.plot(x, x**3, label='cubic')

plt.xlabel('x label')
plt.ylabel('y label')

plt.title("Simple Plot")

plt.legend()

plt.show()

 

Ist der xServer auf dem Client installiert und gestartet, sollte der Graphic Output nun lokal auf dem Rechner zu sehen sein, auf dem der Command ausgeführt wurde.

 

IPs mittels iptables automatisch blockieren lassen

Eigentlich wollte ich meinen vServer noch sicherer machen, indem ich von Seiten wie myip.ms eine vorgefertigte Blackliste herunterlade. Die gezogenen IPs sollten mittels iptables blockiert werden. Ich hatte auch schon soweit alles fertig geschrieben, ein Python3 Script, ein Shell Script und die SQL Scripte zum Anlegen der Datenbank.
Allerdings habe ich nach ein “paar” Einträgen folgende Nachricht von iptables bekommen: iptables: Memory allocation problem.

Nach ein bisschen Internet Recherche fand ich heraus, dass mich mein Hoster auf eine bestimmte Anzahl an iptable Einträgen begrenzt.

Die maximal Möglichen Einträge findet ihr mit diesem Befehl.

egrep "failcnt|numiptent" /proc/user_beancounters

numiptent steht dabei für die NETFILTER Einträge. In diesem Beitrag steht, dass diese dem Schwellenwert von 200 – 300 Einträgen ebenfalls aufgrund eventueller Performanzprobleme nicht überschreiten sollte. Da ich von myip.ms über 500 Einträge übertragen habe, war ich auch schon weit über das Limit. Ebenfalls habe ich noch diesen Artikel gefunden: https://www.danami.com/clients/knowledgebase/92/How-can-I-fix-the-error-The-VPS-iptables-rule-limit-numiptent-is-too-low.html

Naja, ganz abschreiben wollte ich es noch nicht und vielleicht fällt mir auch noch ein automatischer Prozess ein, den ich integrieren kann. Ich möchte jedoch schon den bis dato geschriebenen Code veröffentlichen.

Da ist zum einen der Shell Code, der die beiden SQL Dateien CREATE_FIREWALL.sql und FIREWALL_SCHEMA.sql aufruft.

#!/bin/bash

if [ -f "CREATE_FIREWALL.sql" ] && [ -f "FIREWALL_SCHEMA.sql" ]
then
                mysql -uuser -ppassword < "CREATE_FIREWALL.sql"
                mysql -uuser -ppassword < "FIREWALL_SCHEMA.sql"
else
        echo "[C] CREATE_FIREWALL.sql OR FIREWALL SCHEMA.sql missing"
fi

in der CREATE_FIREWALL steht die Einrichtung der Datenbank,

CREATE Database IF NOT EXISTS Firewall;
GRANT ALL PRIVILEGES ON Firewall.* TO 'user'@'localhost' IDENTIFIED BY 'password';

während in der FIREWALL_SCHEMA das Schema der Tabelle, der gerade erzeugten DB, beinhaltet.

use Firewall;

SET FOREIGN_KEY_CHECKS = 0;

DROP TABLE IF EXISTS FW_IPS;

SET FOREIGN_KEY_CHECKS = 1;

CREATE TABLE IF NOT EXISTS FW_IPS(
IP varchar(16) NOT NULL,
ORIGIN varchar(100),
SOURCE varchar(100),
STATUS boolean not null default 0,
NEW boolean not null default 0,
PRIMARY KEY(IP)
);

Kurz eine kleine Beschreibung der einzelnen Spalten:

IP zu blockierende IP
ORIGIN Herkunftsland der IP
SOURCE Resource wo diese IP steht, in diesem Fall myip.ms
STATUS Soll dieser Block aktiv sein?(True/False)
NEW Ist diese IP neu dazu gekommen?(True/False)
Python Code

Kommen wir hier zu dem Python Code, der eine Verbindung zu myip.ms aufbaut. Der Aufbau wird mit dem urllib.request Module vollzogen. Als Response bekomme ich eine Byte String, den ich mit .data.read().decode() umkonvertiere. In der Schleife benutze ich regex und entferne bloße Kommentare(gekennzeichnet mit #) oder IPv6 Einträge. Somit bleiben nur die von mir erforderlichen IPv4 Einträge. Diese verbleibenden Einträge kippe ich dann in die Firewall Datenbank. Das PDB ist ein von mir geschriebenes Modul. Die QueryInsert Methode benötigt die SQL-Abfrage, die Werte als Tupel und als letztes ein FLAG, das bestimmt ob ein Update versucht wird, wenn der Private Schlüssel schon existiert. Jenes ist jedoch nicht der Fall, daher habe ich ihn auf False gesetzt. Der NEW FLAG wird bei den Werten als 1 übergeben, da es sich hier immer um eine neue IP handeln wird, wenn die IP in die DB geschrieben wird.

try:
        import urllib.request as req
        import re
        import PDB
except:
        print("could not load modules")
        exit(1)
try:
        #try to open URL
        data = req.urlopen("https://myip.ms/files/blacklist/csf/latest_blacklist.txt")
except urllib.error.URLError as e: print(e.reason)

#decode received data from URL
ips=data.read().decode()

#Open DB Connection
DB= PDB.databaseconnection('localhost','user','password','Firewall')
DB.Connect()

#for each line in ip collected from URL
for line in ips.splitlines():
        #reject lines with a # and a total length of < 15
        if  not re.findall("\A#", line) and len(line) < 15:
                #also check if not empty
                if not line == '':
                        #DB Insert without update process
                        DB.QueryInsert("insert into FW_IPS(%s) VALUES(%s)",{"IP":line,"SOURCE":"myip.ms","STATUS":0,"NEW":1},False)
#Close DB
DB.Disconnect()
Shell Code

In dem Shell Script lege ich zuerst die BLACKLIST Chain an und platziere die bei der INPUT Chain an erster Stelle. Danach geht die Loop über die Einträge in der DB, die das FLAG NEW noch auf 1 stehen haben. Sobald es die IP erfolgreich mit iptables registriert hat, kümmern sich 2 weitere SQL Befehle darum, die FLAGS STATUS und NEW auf jeweils 1 bzw. 0 zu setzen.

 

iptables -N BLACKLIST 
iptables -I INPUT 1 -j BLACKLIST 
for ip in `mysql -s -N -e "select IP from Firewall.FW_IPS where NEW = 1"` 
do 
  iptables -A BLACKLIST -s $ip -j DROP 
  if [ $? -eq 0 ] 
  then 
    mysql -s -N -e "update Firewall.FW_IPS set STATUS = 1 where IP = '${ip}'" 
    mysql -s -N -e "update Firewall.FW_IPS set NEW = 0 where IP = '${ip}'" 
  fi 
done

Der komplette Prozess hat wie gesagt funktioniert, jedoch durch den Lock mehr oder weniger unbrauchbar. Zusätzlich verwende ich nämlich noch fail2ban, der auch Einträge in die iptables tätigt. Alles in allem muss ich mir etwas neues überlegen, damit ich die IPs von bestimmten Herkunftsländern blockieren kann.

 

 

SSH Email bei Client Verbindung

Mein vServer hat zurzeit den SSH Port offen. Dieser befindet sich zwar nicht mehr standardmäßig auf dem Port 22, um sogenannte Bots größtenteils vom Server zu halten, allerdings wollte ich bei jedem Connect informiert werden, damit ich im Falle einer Kompromittierung direkt reagieren kann.

Der Port kann in der /etc/ssh/sshd_config angepasst werden.

Die eigentliche Mail Benachrichtigung, in der es in diesem Beitrag auch geht, muss in pam.d eingerichtet werden. Eine, wie ich finde gute Seite, die pam.d beschreibt, ist

https://web.archive.org/web/20180303034326/http://www.tuxradar.com/content/how-pam-works

Kommen wir weiter zu den Änderungen der Datei.

Öffnen wir die ssh Datei mit einem beliebigen Editor, in meinem Fall ist es der vim Editor.

vi /etc/pam.d/sshd

Dort muss folgende Zeile eingetragen werden.

session optional pam_exec.so seteuid /etc/ssh/login-notify.sh

Am Anfang sollte der Flag “optional” benutzt werden, damit bei falscher Konfiguration eine Verbindung trotzdem noch möglich ist. Als letztes Argument wird das Script übergeben.

Das Script zum Versenden der Mail sieht schließlich wie folgt aus.

#!/bin/bash

if [ "$PAM_TYPE" != "close_session" ]; then

body="<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN' 'http://www.w3.org/TR/html4/loose.dtd'>
    <html>
    <head><title>SSH Mail</title>
    </head>
    <body>
    <table>
    <tr>
    <th>Tag</th>
    <th>Value</th>
    </tr>
    <tr>
    <td>service</td>
    <td>SSH</td>
    </tr>
    <tr>
    <td>direction</td>
    <td>connected</td>
    </tr>
    <tr>
    <td>Timestamp</td>
    <td>`date '+%T %F'`
    </tr>
    </table>
    <p></p>
    <p>`env`</p>
    </body>
    </html>"

echo $body | /usr/bin/mail -a "From: ssh@server.com" -a "MIME-Version: 1.0" -a "Content-Type: text/html" -s "SSH Client Connection established" mail@dns.de >> /dev/null
fi
exit 0

Verbindung sich nun jemand per SSH auf dem Server, wird direkt eine Mail generiert und versendet.

Der Mail Body sieht dann genau so aus, wobei RHOST die Remote Addresse enthält.

Metabase Startup Script

Damit Metabase automatisch starten, habe ich dieses Script geschrieben. Dafür überprüft es, ob Metabase, bzw. eine Java Instanz schon auf Port 3000 lauscht. Falls dies der Fall ist, gehe ich davon aus, dass Metabase schon läuft und schreibe es in die Variable alreadystarted. In die eventuallystarted schreibe ich, falls schon ein Java Dämon existiert. Dies könnte unter Umständen bedeuten, dass Metabase im Boot Mode ist und gerade gestartet wurde. In diesem Fall bricht das Script ab und man muss es manuell ausführen. Metabase ist definitiv nicht am Laufen, wenn beide Variablen nicht gesetzt sind. Dementsprechend sind die export Variablen für den Metabase Mysql Startup. Die STDOUT von Metabase wird in eine Log Datei weitergeleitet.

alreadystarted=`netstat -tapen | grep LISTEN | grep 3000 | awk '{print $9}'`
eventuallystarted=`ps -Al | grep java | awk '{print $4"/"$14}'`


echo "JAVA,METABASE:${eventuallystarted},${alreadystarted}"


if [ ! -z "$eventuallystarted" ]
then
if [ ! -z "$alreadystarted" ]
then
echo "[I] A Metabase Instance is already running!"
exit 0
fi
echo "[I] It may be that a Metabase Instance is already running. Please check and run manually!"
exit 1
fi


if [ -z "$alreadystarted" ]
then
export MB_DB_TYPE=mysql
export MB_DB_DBNAME=db
export MB_DB_PORT=3306
export MB_DB_USER=user
export MB_DB_PASS=password
export MB_DB_HOST=localhost
java -jar metabase.jar >> /var/log/Metabase/metabase`date +%V`.log &
else
echo "[I] A Metabase Instance is already running!"
exit 0
fi

 

User löschen in Mysql

Einen Benutzer in Mysql zu löschen ist nicht sonderlich schwer. Es wird lediglich ein root Benutzer auf der Mysql Datenbank benötigt.

mysql -uroot -ppasswort

Sobald man sich erfolgreich angemeldet hat, tippe folgenden Befehl ein, um all Benutzer der Datenbank zu sehen.

select user,host from mysql.user;
Mysql User
Mysql User

Sucht dabei euren Benutzer aus, den ihr löschen wollt und merkt euch den User + Host. Diese Informationen werden beim Entfernen der Privilegien und des eigentlichen Benutzers benötigt.
Ich hab mir den remote User ausgesucht. Ihr müsst unbedingt aufpassen, dass ihr keinen Mysql oder den Benutzer einer anderen DB erwischt! Das % Zeichen bedeutet im übrigen, dass sich dieser Benutzer von überall anmelden darf. Zumeist ist localhost vorzufinden.

Löschen wir nun mit dem REVOKE Befehl alle Privilegien vom Benutzer.
Der Aufruf des REVOKE sieht wie folgt aus.

 REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'user'@'host';

Es kommen nun die User und Host Informationen ins Spiel, die wir uns zuvor gemerkt hatten.
Bei mir würde es wie folgt aussehen

 REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'remote'@'%';

Falls alles richtig war, wird dieser Befehl mit einem OK quittiert.

Query OK, 0 rows affected (0.00 sec)

Gefolgt wird das Entfernen der Privilegien vom Löschen des Benutzers. Auch hier muss der User und Host angegeben werden.

DROP USER 'remote'@'%';

So wie der vorherige Befehl, wird auch der DROP User quittiert.

Query OK, 0 rows affected (0.00 sec)

Zeigt man sich nun die SQL Benutzer an, so sollte der gelöschte Benutzer nicht mehr auftauchen.

select user,host from mysql.user;

War die Benutzer Löschung im Zuge einer Entfernung von WordPress oder ähnliches, kann die vollständige WordPress Datenbank mit folgenden Befehl gelöscht werden.

DROP DATABASE <dbname>

 

OpenVPN Email bei Clientverbindung

Ich benutze OpenVPN auf meinem vServer, um eigene Services von der Außenwelt abzuschotten. Sobald ich mich allerdings mit OpenVPN authentifiziert habe, sollen eben jene Services erreichbar sein. Der Vorteil dieser Lösung ist, dass ich nur einen Port, den von OpenVPN, öffentlich stellen muss. Alle anderen Dienste verweilen im internen Netz und sind soweit von außen nicht mehr erreichbar. Dies habe ich durch Firewall regeln festgelegt. Für meinen SSH Dienst hatte ich schon einen Prozess geschrieben, der mich per Mail informiert, sobald eine Verbindung erfolgreich hergestellt wurde. Dies wollte ich nun auch für OpenVPN nachziehen.

Dafür installieren wir eine Abhängigkeit:

apt install auditd

In der server.conf von OpenVPN fügen wir folgende Zeile hinzu

client-connect /etc/openvpn/client-connected.sh

client-connected.sh ist das untere Script, welches ich für die Benachrichtigung geschrieben habe. Da es zu OpenVPN gehört, habe ich es in das Verzeichnis von OpenVPN gelegt.

Zum Erstellen des Scripts kann z.B. der vim Editor benutzt werden. Nano oder andere würden natürlich auch gehen, je nach belieben (Ich benutze meistens vim).

Erstellen wir nun das Script

vi /etc/openvpn/client-connected.sh

und fügen diesen Code Snippet ein. Bitte beachtet, dass ich die Mail Adressen aus Sicherheitsgründen umbenannt habe. Diese müssen nach euren Gegebenheiten angepasst werden.

#!/bin/bash
body="<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN' 'http://www.w3.org/TR/html4/loose.dtd'>
<html>
<head><title>OpenVPN Mail</title>
</head>
<body>
<table>
<tr>
<th>Tag</th>
<th>Value</th>
</tr>
<tr>
<td>service</td>
<td>openVPN</td>
</tr>
<tr>
<td>direction</td>
<td>connected</td>
</tr>
<tr>
<td>Timestamp</td>
<td>`date '+%T %F'`
</tr>
</table
</body>
</html>"

echo $body | /usr/bin/mail -a "From: mail@dns.com" -a "MIME-Version: 1.0" -a "Content-Type: text/html" -s "OpenVPN Client Connection established" mailadresse@mail.de >> /dev/null

exit 0

Fehlt nur noch der Neustart des OpenVPN Daemon`s

systemctl restart openvpn@server

Nun sollte Mails verschicken werden, sobald eine Verbindung erfolgreich hergestellt wurde. Da es sich um einen HTML Email Body handelt, kann dieser nach belieben angepasst werden.