SSH Tunnel zum MySQL Server

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
rpi-joe
User
Beiträge: 23
Registriert: Mittwoch 27. Januar 2021, 15:27

Hallo
habe hier schon viel gelesen und einige Tips "mitgenommen". Jetzt hab ich mich registriert, um auch eine Frage zu einem kniffligen Problem zu stellen...
Habe einen Raspi Zero mit Kameramodul auf meine Wasseruhr gehängt und möchte den Verbrauch auf meinen Server im LAN in einer MySQL speichern.

Die MySQL besteht schon seit der Nextcloud Installation. PHPmyAdmin habe ich auch zum Laufen bekommen und eine neue Datenbank angelegt.

Das Programm auf dem Pi ist fast fertig, die Bildauswertung funktioniert und ich habe einen Zähler für die Liter. Das Schreiben auf den Server funktioniert im Test,
Das Programm hatte ich zum Test abgeändert, sodass ein Zählerwert immer gesendet wird. Klappt auch.

Allerdings muß ich den SSH Tunnel vorher von einem Notebook im Terminal starten:

Code: Alles auswählen

 ssh -N -L 3308:127.0.0.1:3306 192.168.1.100 
Wenn ich den Rechner ausschalte, wird der Tunnel nach ca 1-2 Std. geschlossen :-(
Und als bash auf dem Pi und mit cron gestartet, kommt der Tunnel auch nicht zustande.

Habe einige Beiträge hier im Forum verfolgt, die meist übers Internet gehen. Das ist nicht meine Konstellation. Dann habe ich die Themen zu Paramiko durchgelesen,
da fehlt mir der Hintergrund, wie das funktionieren soll und was man vorher einstellen muss.

Könnte ich bitte einen kurzen zielführenden Tipp bekommen, wie ich einen stabilen Tunnel hinbekomme? Danke für jeden Tipp.

Hier zur Info mein Programm:

Code: Alles auswählen

import cv2
import numpy as np
import datetime
import os
from picamera.array import PiRGBArray
from picamera import PiCamera
import time
import pymysql

indicator = False
liter = "Liter.txt"


# init mySQL
mydb = pymysql.connect(
    host="127.0.0.1",
    port=3308,
    user="messi",
    password="mess",
    database="smarthome"
)

mycursor = mydb.cursor()
mycursor.execute("SELECT counter_w FROM messwerte_wasser ORDER BY Datum DESC LIMIT 1")
last = mycursor.fetchone()
counter = last[0]

# initialize the camera and grab a reference to the raw camera capture
camera = PiCamera()
camera.resolution = (864, 656)

while True:

    # grab an image from the camera
    rawCapture = PiRGBArray(camera, size=(864, 656))
    camera.capture(rawCapture, format="bgr")
    image = rawCapture.array
    # turn picture (180grad)
    img = image
    dimensions = img.shape
    rows = dimensions[0]
    cols = dimensions[1]
    # cols-1 and rows-1 are the coordinate limits.
    M = cv2.getRotationMatrix2D(((cols - 1) / 2.0, (rows - 1) / 2.0), 180, 1)
    dst = cv2.warpAffine(img, M, (cols, rows))
    image = dst
    # save pictures for testing
    # cv2.imwrite(datetime.datetime.now().strftime("%Y, %m, %d, %H, %M, %S").replace(", ", "-") + ".jpg", image)

    # ROI colour detection
    y = 531
    x = 428
    region_of_interest = image[y:y + 6, x:x + 6]
    mean_blue = np.mean(region_of_interest[:, :, 0])
    mean_green = np.mean(region_of_interest[:, :, 1])
    mean_red = np.mean(region_of_interest[:, :, 2])
    # action if red arrow
    # if mean_red >= 25 and mean_green <= 50:
    if mean_red > mean_green:
        if not indicator:
            # increase counter liter
            counter += 10
            # send to mySQL
            mycursor.execute("INSERT INTO messwerte_wasser (counter_w) VALUE ( %s);" % (counter))
            mydb.commit()
            mycursor.close()
            mydb.close()

        else:
            pass
        # set indicator red
        indicator = True
    # reset action no red arrow
    # if mean_red <= 25 and mean_green >= 50:
    if mean_red < mean_green:
        # unset indicator red
        indicator = False

    # only testing
    print(f" {indicator:2}, Counter: {counter:3d},    R:{mean_red:5.0f},   G:{mean_green:5.0f},   B:{mean_blue:5.0f}")

    image = []
    time.sleep(1)

# Break  --- comming soon

camera.close()
exit(0)
nezzcarth
User
Beiträge: 1633
Registriert: Samstag 16. April 2011, 12:47

Ich muss mal gegen fragen: Befinden sich beide Systeme in demselben privaten Netzwerk? Wenn ja, weshalb möchtest du in einem lokalen Netzwerk per SSH-Tunnel auf Mysql zugreifen statt direkt? Wenn man nicht gerade besondere Netzwerkbedingungen hat, braucht man dafür eigentlich keinen SSH Tunnel. Ansonsten ist es so, dass SSH-Tunnel durchaus auch mal brechen können, aus verschiedenen Gründen; ein bisschen was kann man über die Konfiguration abfangen (Server und Client-seitig), aber alles eben nicht.
Benutzeravatar
__blackjack__
User
Beiträge: 13061
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@nezzcarth: Ein Grund könnte sein, dass man die DB so konfiguriert hat, dass sie nur Verbindungen vom Rechner auf dem sie läuft erlaubt und keine von aussen, selbst wenn aussen das gleiche Netz ist. Könnte man natürlich auch ändern…
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
rpi-joe
User
Beiträge: 23
Registriert: Mittwoch 27. Januar 2021, 15:27

Das Netzwerk ist bei mir zuhause. Das ist mit der Zeit so gewachsen, einmal weil ich mich mit Linux beschäftige/nutze,
dann haben wir noch einen Windows-Rechner im Netz, dem ich gar nicht traue - Viren und so....
Und mein Sohn hat eine LED-Lampe von Nanoleaf, die nach Hause telefoniert. Die ist deshalb nur im Gastnetz der FritzBox.

Von außen sollte erst mal nichts erreichbar sein, der CT Test zum Scannen der Ports ist jedenfalls gut ausgefallen :-)
Da alle ein VPN vom Handy nutzen wegen Nextcloud ist das sicher - glaub ich.

Die Raspberrys sind irgendwann hobbymäßig dazugekommen und konnten auch SSH.
Ob der Server jetzt ohne SSH arbeiten wird, kann ich gar nicht sagen. An die sshd-config möchte ich nicht dran, ist ziemliches gefrickel gewesen.

Über Nacht hatte ich den rpi mit der Kamera laufen. Pythonprogramm im Terminal mit & gestartet und Rechner ausgeschaltet,
ssh auf dem rpi testweise in einer bash mit cronjob stündlich gestartet.
War wohl nicht so gut, jetzt habe ich ca 10 ssh-jobs aktiv. (TOP im Terminal) Python3 läuft auch noch, aber keine Daten auf der MySQL.

Werd die bash nur einmal beim Systemstart starten, mal sehen.
nezzcarth
User
Beiträge: 1633
Registriert: Samstag 16. April 2011, 12:47

@rpi-joe: Ich wollte darauf hinaus, dass MySQL bereits ein eigener Serverprozess ist, den du aus dem Netzwerk heraus direkt ansteuern kannst (und, finde ich, auch solltest). Dafür wird kein Umweg über einen SSH-Tunnel benötigt. Das mit dem SSH-Tunnel macht man, wie gesagt, in ganz speziellen Situationen, wenn nötig ist. Aber in einem Szenario wie dem von dir geschilderten, in dem man sich in einem privaten Netzwerk befindet und in dem es um die Kommunikation zwischen zwei Servern geht, bietet es keinen wirklichen Mehrwert, den MySQL nur lokal bzw. per Tunnel zugänglich zu machen (wie __blackjack_ vermutete); das bisschen zusätzliche "Sicherheit" ist es m.M.n. nicht wert, zumal du in MySQL üblicherweise ja eh noch mal eine Authentifizierung hast.

SSHd selbst würde ich natürlich nicht deaktivieren. Der ist für sich selbst nützlich und wird auf einem Linux Server permanent benötigt. Aber du brauchst ihn halt nicht für den Zugriff MySQL. Sorg dafür, dass MySQL auf dem entsprechenden Port und auf dem entsprechenden Netzwerkinterface lauscht, und dass die Authentifizierung vernünftig eingestellt ist, und das sollte für deine Zwecke eigentlich vollkommen reichen.
rpi-joe
User
Beiträge: 23
Registriert: Mittwoch 27. Januar 2021, 15:27

@nezzcarth: ok, danke für den Hinweis.
In der mysqld.cnf ist der bind-address auskommentiert. Sollte der rpi damit schon direkt auf MySQL schreiben können?
rpi-joe
User
Beiträge: 23
Registriert: Mittwoch 27. Januar 2021, 15:27

so, jetzt habe ich den Tunnel beendet. Im Programm hab ich die Zugangsdaten verändert:

Code: Alles auswählen

mydb = pymysql.connect(
    host="192.168.1.100",
    port=3306,
    user="messi",
    password="mess",
    database="smarthome"
und bekomme den Fehler:

Code: Alles auswählen

pymysql.err.OperationalError: (1045, "Access denied for user 'messi'@'pizeroCamera' (using password: YES)")
Ist die Verbindung nicht direkt auf die MySQL ? Die müsste doch auf Port 3306 erreichbar sein und das Passwort kennen.
Mit PHPmyAdmin klappt es doch.
nezzcarth
User
Beiträge: 1633
Registriert: Samstag 16. April 2011, 12:47

Bei MySQL werden Benutzer normalerweise in Abhängigkeit von dem System, von dem aus der Zugriff erfolgen soll, definiert. Falls es den Benutzer "'messi'@'pizeroCamera' " nicht gibt, musst du ihn anlegen. Best Practice ist auch, den Zugriff auf eine konkrete Datenbank zu beschränken und ggf. nur die Rechte zu erteilen, die benötigt werden.Vielleicht hilft dir das zum Einstieg weiter: https://www.digitalocean.com/community/ ... s-in-mysql
rpi-joe
User
Beiträge: 23
Registriert: Mittwoch 27. Januar 2021, 15:27

Danke für den Tipp, habe die Rechte der User gescheckt, neuen messi@pizeroCamera angelegt, auch Rechte vergeben,
die aber nicht erscheinen, die DB neu gestartet und das Programm immer wieder neu gestartet, der Fehler bleibt.

Code: Alles auswählen

mysql> SHOW GRANTS FOR 'messi'@'%';
+-----------------------------------------------------------------------+
| Grants for messi@%                                                    |
+-----------------------------------------------------------------------+
| GRANT USAGE ON *.* TO `messi`@`%`                                     |
| GRANT ALL PRIVILEGES ON `smarthome`.* TO `messi`@`%`                  |
| GRANT ALL PRIVILEGES ON `smarthome`.`messwerte_har` TO `messi`@`%`    |
| GRANT ALL PRIVILEGES ON `smarthome`.`messwerte_wasser` TO `messi`@`%` |
+-----------------------------------------------------------------------+

Code: Alles auswählen

mysql> SHOW GRANTS FOR 'messi'@'pizeroCamera';
ERROR 1141 (42000): There is no such grant defined for user 'messi' on host 'pizerocamera'
Entweder mag die DB den user nicht- angelegt ist er als 'messi'@'pizeroCamera' Meldung kommt 'messi'@'pizerocamera' ( kleines 'C' )
oder Pymysql sendet den Namen falsch :-(

messi@localhost hat jedoch die Rechte:

Code: Alles auswählen

mysql> SHOW GRANTS FOR 'messi'@'localhost';
+-------------------------------------------+
| Grants for messi@localhost                |
+-------------------------------------------+
| GRANT USAGE ON *.* TO `messi`@`localhost` |
+-------------------------------------------+
Warum ist der messi in dem Programm nicht @localhost? Dann sollte es doch gehen
Ich verstehe es momentan nicht....
rpi-joe
User
Beiträge: 23
Registriert: Mittwoch 27. Januar 2021, 15:27

Ich schau mich mal im MySQL Forum um, evtl mach ich dort ein neues Thema auf...
rpi-joe
User
Beiträge: 23
Registriert: Mittwoch 27. Januar 2021, 15:27

@nezzcarth: Nochmals vielen Dank für deinen Hinweis :D

Es lag doch am user auf der Datenbank. Ich habe zuletzt alle messi gelöscht und einen neu mit allen Berechtigungen angelegt.
messi@% und Zugriff *.* Damit läuft es jetzt. Für die nächsten Programmschritte ist es erstmal ok, die Rechte sollte man jedoch
auf die betreffende Datenbank einschränken - aus Sicherheitsgründen.
rpi-joe
User
Beiträge: 23
Registriert: Mittwoch 27. Januar 2021, 15:27

So, nach einigen Feinabstimmgen läuft es rund, das Programm startet beim reboot des rpi automatisch.
Nicht per crontab -e aber in der /etc/rc.local > Python3 /Pfad zum Programm/Red_Indicator4reboot.py

Im MySQL Zugriff im Programm weiter oben hatte ich noch einen Fehler, sodass das Programm beim 2.Versuch Daten zu schreiben, abbrach.
Jetzt funktioniert es, wie es soll, war eine ziemliche Anstrengung für mich.

Code: Alles auswählen

# Counts Water on Water meter with Raspberry + Camera
# Python3 - request in /etc/rc.local for Autostart

import cv2
import numpy as np
from picamera.array import PiRGBArray
from picamera import PiCamera
import time
import pymysql
import keyboard

indicator = False

# init mySQL    direct to Server ..1.100 , no ssh
mydb = pymysql.connect(
    host='192.168.1.100',
    user='messi',
    password='mess',
    database='smarthome'
)

# get last counter from MySQL 
mycursor = mydb.cursor()
mycursor.execute('SELECT counter_w FROM messwerte_wasser ORDER BY Datum DESC LIMIT 1')
last = mycursor.fetchone()
counter = last[0]

# initialize the camera and grab a reference to the raw camera capture
camera = PiCamera()
camera.resolution = (864, 656)

try:
    while True:

        # grab an image from the camera
        rawCapture = PiRGBArray(camera, size=(864, 656))
        camera.capture(rawCapture, format="bgr")
        imageturn = rawCapture.array

        # turn picture (180grad)
        img = imageturn
        dimensions = img.shape
        rows = dimensions[0]
        cols = dimensions[1]

        # cols-1 and rows-1 are the coordinate limits.
        M = cv2.getRotationMatrix2D(((cols - 1) / 2.0, (rows - 1) / 2.0), 180, 1)
        dst = cv2.warpAffine(img, M, (cols, rows))
        image = dst

        # save pictures for testing
        # cv2.imwrite(datetime.datetime.now().strftime("%Y, %m, %d, %H, %M, %S").replace(", ", "-") + ".jpg", image)

        # ROI colour detection
        y = 531
        x = 428
        region_of_interest = image[y:y + 6, x:x + 6]
        mean_blue = np.mean(region_of_interest[:, :, 0])
        mean_green = np.mean(region_of_interest[:, :, 1])
        mean_red = np.mean(region_of_interest[:, :, 2])

        # action if red arrow is detected on water meter 
        if mean_red > mean_green:
            if not indicator:

                # increase counter liter
                counter += 10

                # send to MySQL
                mycursor.execute("INSERT INTO messwerte_wasser (counter_w) VALUE ( %s);" % counter)
                mydb.commit()

            else:
                pass
            # set indicator red
            indicator = True

        # reset action no red arrow
        if mean_red < mean_green:

            # unset indicator red
            indicator = False

        # only testing
        # print(f" {indicator:2}, Counter: {counter:3d},    R:{mean_red:5.0f},   G:{mean_green:5.0f},   B:{mean_blue:5.0f}")

        image = []
        time.sleep(1)

except KeyboardInterrupt:

    mycursor.close()
    mydb.close()
    camera.close()
    exit(0)
Wer es ausprobieren möchte...
(aktueller Stand 2021-01-31, Raspian buster , Python3.7.3 )
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

keyboard wird importiert, aber nicht verwendet.
Auch Spalten in Namen sollten sinnvolle Namen haben, was soll das w bei counter_w denn bedeuten?
Warum schiebst Du Werte von einer Variable zur anderen? Warum nennst Du das Ding nicht gleich img oder bleibst bei imageturn? cv2 zu benutzen, nur um ein Bild um 180˚ zu drehen, ist ziemlich umständlich. Das geht mit Numpy auch deutlich effizienter.
Und wieder wird eine Variable in eine andere umbenannt.
mean_blue wird gar nicht verwendet. Ein `else: pass` kann einfach weg.
Niemals Werte in eine SQL-Statement hineinformatieren, dafür gibt es Parameter.
Dass image an eine Leere Liste gebunden wird, wird niemals verwendet, kann also auch weg.
Im except-Block haben die ganzen close nichts verloren, die gehören in einen finally-Block.
Das `exit´ wird nirgends definiert und ist eh unnötig und kann weg.

Code: Alles auswählen

import numpy as np
from picamera.array import PiRGBArray
from picamera import PiCamera
import time
import pymysql

def capture_region(camera, x, y):
    # grab an image from the camera
    raw = PiRGBArray(camera, size=camera.resolution)
    camera.capture(raw, format="bgr")
    image = raw.array[::-1, ::-1, :]

    # ROI colour detection
    return image[y:y + 6, x:x + 6]

def main():
    indicator = False

    # init mySQL    direct to Server ..1.100 , no ssh
    db = pymysql.connect(
        host='192.168.1.100',
        user='messi',
        password='mess',
        database='smarthome'
    )

    # get last counter from MySQL 
    cursor = db.cursor()
    cursor.execute('SELECT counter_w FROM messwerte_wasser ORDER BY Datum DESC LIMIT 1')
    last = cursor.fetchone()
    counter = last[0]

    # initialize the camera and grab a reference to the raw camera capture
    camera = PiCamera()
    camera.resolution = (864, 656)

    try:
        while True:
            region_of_interest = capture_region(camera, 438, 531)
            mean_green = np.mean(region_of_interest[:, :, 1])
            mean_red = np.mean(region_of_interest[:, :, 2])

            # action if red arrow is detected on water meter 
            if mean_red > mean_green and not indicator:
                # increase counter liter
                counter += 10
                mycursor.execute("INSERT INTO messwerte_wasser (counter_w) VALUE (%s)", [counter])
                mydb.commit()
            indicator = mean_red >= mean_green
            time.sleep(1)
    except KeyboardInterrupt:
        pass
    finally:
        mycursor.close()
        mydb.close()
        camera.close()

if __name__ == '__main__':
    main()
rpi-joe
User
Beiträge: 23
Registriert: Mittwoch 27. Januar 2021, 15:27

@Sirius3: Danke dir für die vielen Tips zur Optimierung des Codes. Ich dachte schon, dass ich vieles berücksichtigt habe, was ich hier gelesen habe...
Klar, das counter _w ist zur Unkenntlichkeit gekürzt, sollte besser counter_water heißen.
Warum schiebst Du Werte von einer Variable zur anderen?
Da hatte PyCharm gemeckert, dass ich Variablen doppelt initialisiere... da ich noch nicht so fit bin, muß ich das glauben :? und hab sie umgenannt.

Da das Prog zumindest mal durchläuft, bin ich einen Schritt weiter. Das es so schlecht ist hätte ich nicht gedacht. Ich lese mir morgen deinen Code intensiv durch, um alle Schritte zu verstehen und gehe ihn im Debugmodus durch.

Danke nochmal für deine Unterstützung auf dem Weg zu ordentlichem Code.
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich würde bei der gewählten Methode ja die Rotation komplett in Frage stellen. Beim Vorgehen durch den Farbton ist die Orientierung ja egal. Das würde sich natürlich ändern, wenn man die Zahlen versucht zu erkennen. Dafür gibt es auch ein Open Source Projekt: https://www.thingiverse.com/thing:3860911
rpi-joe
User
Beiträge: 23
Registriert: Mittwoch 27. Januar 2021, 15:27

@__deets__: Genau das habe ich auch überlegt, als ich die Tipps von Sirius las. Der Dreh kommt aus der Vorversion mit Zahlenerkennung des Zählwerks. Da waren mehrere cv2 Funktionen und tesseract drin.
Das Bild bräuchte gar nicht mehr gedreht werden. Es reicht, die Koordinaten des ROI zu spiegeln :-)
Info zu dem Modul wäre toll. Wo gibts ein deutsches Tutorial?

Auch das keyboard Modul war aus der Anfangsphase, versuch, das Programm geordnet zu beenden.
mean_blue wird gar nicht verwendet.
Es müssen nicht alle drei Farben (RGB) verarbeitet werden? ok.
Ein `else: pass` kann einfach weg.
Ist von PyCharm reingekommen,ok.
Niemals Werte in eine SQL-Statement hineinformatieren, dafür gibt es Parameter.
Welche Zeile ist gemeint? Das möchte ich verstehen.

Die anderen Punkte hab ich auch verinnerlicht.

Hab noch das my vor den restlichen Aufrufen mycursor, mydb entfernt :-) jetzt läufts auf dem rpi.

Auf der Suche nach Infos bleibe ich oft in Foren wie Stackoverflow hängen. Nach 5 langen Threads brauche ich eine Pause.
Irgendwann gibt man dann auf, wenn die Infos aus 2005- 2015 sind, die nicht mehr aktuell sind :-(

Um so professionell wie ihr zu werden, braucht es viel Übung - bin neidisch ;-)

Auf dem rpi wird das Programm aus der /etc/rc.local gestartet. Da habe ich parallel hier gestöbert und das Für und Wider für einen Systemd gelesen und das rc.local veraltet sei. Soll ich das noch ändern?

Und vielen Dank für eure Hilfe.
nezzcarth
User
Beiträge: 1633
Registriert: Samstag 16. April 2011, 12:47

… /etc/rc.local gestartet …
Nebenbemerkung dazu: rc.local wird zwar oft erwähnt, ist aber eigentlich schon lange deprecated und sollte nicht mehr verwendet werden. Dienste, die automatisch starten sollen, startet man auf systemd-basierten Systemen heute über eine Service-Unit. Einige Distros unterstützen das gar nicht mehr, andere verwenden einen Wrapper, der das im Hintergrund auf Systemd umbiegt.
rpi-joe
User
Beiträge: 23
Registriert: Mittwoch 27. Januar 2021, 15:27

@nezzcarth: Hab ich mir gedacht. Habe einen Vorschlag entdeckt, weiss nur nicht ob auch ein Python-Programm damit gestartet werden kann:

Code: Alles auswählen

[Unit]
Description=CounterWater
After=multi-user.target
 
[Service]
ExecStart=/home/pi/counter/Red_Indicator5Forum.py &
 
[Install]
WantedBy=multi-user.target
Original war eine Shell drin in der Zeile

Code: Alles auswählen

ExecStart=/home/pi/counter/ XXXXX.sh &
Kann da mein Python stehen bleiben?

Das dann nach /lib/systemd/system/counter.service speichern und ausführbar machen.
Dann mit

Code: Alles auswählen

sudo systemctl enable counter
autostarten - fertig.?

P.S. wir sind schon lange am Thema vorbei, ist das ok so oder sollen wir ein neues Thema aufmachen?
__deets__
User
Beiträge: 14522
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das geht auch mit Python. Dazu einen shebang angeben, und das Skript ausführbar machen. Oder explizit zb /usr/bin/python /voller/pfad/zum/skript.py abgeben. Das & ist IMHO überflüssig.
Benutzeravatar
__blackjack__
User
Beiträge: 13061
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Ich frage mich ob das nicht nur überflüssig ist sondern vielleicht sogar ein Problem wenn man den Dienst beispielsweise wieder stoppen möchte.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten