====== OpenEats ======
Bei [[http://www.openeats.org|OpenEats]] handelt es sich um einen in Python geschriebenen Django-Server zur Verwaltung von Rezepten. Da das Projekt von dem ursprünglichen Entwickler nicht mehr fortgeführt wurde, sind zahlreiche Forks entstanden. Dabei viel mir [[https://github.com/RyanNoelk/OpenEats|einer]] ins Auge, der optisch und von der Funktionalität her ansprechend gestaltet war, aber leider auch das JavaScript-Framework [[https://reactjs.org/|React]] übergestülpt bekam. Trotz meiner Vorbehalte gegenüber Node.js ((Mich stören die vielen (''npm install'' -- may take several minutes...) teils sinnlosen Abhängigkeiten ([[https://www.theregister.co.uk/2016/03/23/npm_left_pad_chaos/|2016 beschloss ein Entwickler, seine Module von NPM zu entfernen]], darunter ein Paket mit 11 Zeilen, um einen String links mit Nullen oder Leerzeichen aufzufüllen -- dieses Paket war eine Abhängigkeit sehr vieler Projekte, die plötzlich nicht mehr funktionierten) und die daraus resultierenden aufgeblasenen Projekte, sowie die Tatsache, JavaScript auf Server-Seite zu verlagern, während viele auf Node-Pakete und Frameworks zurückgreifen, um ihr Unverständnis der Sprache oder Unwillen zum selbst Nachdenken zu kaschieren.)) und JavaScript im Allgemeinen habe ich diesem Fork eine Chance gegeben.
===== Vorbereitung =====
Der Python-Part wird in einer virtuellen Umgebung eingerichtet, wozu die folgenden Pakete installiert werden müssen, sofern nicht bereits vorhanden:
$ sudo pacman -S python python-pip python-virtualenv
Für den Javascript-Teil benötigen wir den Node.js Paket-Manager ''npm'':
$ sudo pacman -S npm
==== Datenbank anlegen ====
Für die Datenbank verwende ich MySQL in Form von MariaDB. Dort wird die Datenbankstruktur angelegt und ein Benutzer angelegt.
# mysql -u root -p
mysql> CREATE DATABASE openeats;
mysql> CREATE USER openeats@localhost IDENTIFIED BY 'MYSUPERSECRETPASSWORD';
mysql> GRANT ALL PRIVILEGES ON openeats.* TO 'openeats'@'localhost' IDENTIFIED BY 'MYSUPERSECRETPASSWORD';
mysql> FLUSH PRIVILEGES;
mysql> EXIT
Dabei sollte natürlich ein sicheres Passwort verwendet werden.
==== nginx ====
''nginx'' fungiert später als Reverse Proxy, um die Seiten der lokal laufenden Djando-, bzw. Node-Server auszuliefern.
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name food.domain.tld;
ssl_certificate /etc/letsencrypt/live/domain.tld/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/domain.tld/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/domain.tld/fullchain.pem;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect http://localhost:8080 https://food.domain.tld;
}
location /admin/ {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect http://localhost:8000 https://food.domain.tld;
}
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /static-files/ {
autoindex on;
root /usr/share/webapps/openeats/api/;
try_files $uri /static-files/$uri;
}
location /site-media/ {
autoindex on;
root /usr/share/webapps/openeats/api/;
try_files $uri /site-media/$uri;
}
}
Die Einrichtung der Zertifikate für die TLS-Verschlüsselung ist [[server:letsencrypt|hier]] beschrieben.
==== Projekt klonen ====
Ich arbeite im Folgenden als ''root'', mit ''su'' Benutzer wechseln. Installieren will ich das Projekt in ''/usr/share/webapps''.
# cd /usr/share/webapps/
# git clone https://github.com/RyanNoelk/openeats.git
# cd openeats/
===== Django einrichten =====
Zuerst wird die virtuelle Umgebung angelegt, in der der Python-Teil läuft. Ich habe sie hier exemplarisch ''virt_env'' genannt:
# mkdir virt_env
# virtualenv virt_env/
# source virt_env/bin/activate
Nun befinden wir uns in der virtuellen Umgebung.
Bevor die Python-Pakete installiert werden können, muss noch ein Fehler behoben werden, der bei der Verwendung von Python 3 wichtig sind. Das in ''api/base/requirements.txt'' angegebene ''MySQL-python'' ist nicht kompatibel mit Python 3. Ein Fix ist, dies durch ''mysqlclient'' zu ersetzen, einem Fork von ''MySQL-python'', der kompatibel mit Python 3 ist. Dazu in der ''requirements.txt'' die Zeile
MySQL-python==1.2.5
(Zeile 16, Stand Oktober/November 2017) austauschen mit
mysqlclient==1.3.12
Nun können die Pakete installiert werden:
# pip install -r api/base/requirements.txt
Als nächstes legen wir eine Datei mit unseren lokalen Einstellungen an
# vim api/base/local_settings.py
mit folgendem Inhalt:
SECRET_KEY = 'SUPERSECURERANDOMSECRETKEY'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'openeats',
'USER': 'openeats',
'PASSWORD': 'MYSUPERSECRETPASSWORD',
'HOST': '',
'PORT': '3306',
'OPTIONS': {
'charset': 'utf8mb4'
}
}
}
ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'node', 'food.domain.tld']
LANGUAGE_CODE = 'de-DE'
Einen Secret Key kann man sich beispielsweise auf [[https://www.miniwebtool.com/django-secret-key-generator/|dieser Seite]] generieren.
Damit die lokalen Einstellungen, die wir soeben angelegt haben, auch gefunden werden, muss noch ein Fehler in der ''settings.py'' behoben werden. Dazu ganz am Ende der Datei in der Zeile
from local_settings import *
einen Punkt vor ''local_settings'' machen. Dies ist nötig, da Python 3 keine implizite relative Imports durchführt. ''from local_settings'' ist in Python 3 ein absoluter Import. ''from .local_settings'' hingegen ist ein expliziter relativer Import, welcher ebenfalls mit Python 2.7 funktioniert, s. [[http://legacy.python.org/dev/peps/pep-0328/|PEP 328]].
# vim api/base/settings.py
Nachdem dieser Bug beseitigt ist, sollte alles vorbereitet sein und die eigentliche Einrichtung kann beginnen.
Um die Datenbank zu befüllen, führen wir migrate mit Django aus:
# ./api/manage.py migrate
Als nächstes erstellen wir einen Superuser als Admin für die Seite und befolgen die Anweisungen:
# ./api/manage.py createsuperuser
Mit ''loaddata'' werden ein paar Informationen in die Datenbank gefüllt.
# ./api/manage.py loaddata course_data.json
# ./api/manage.py loaddata cuisine_data.json
Nun sammeln wir noch die statischen Dateien für das Django REST Framework:
# ./api/manage.py collectstatic
Zum Testen können wir nun den Server mit Django starten
# ./api/manage.py runserver
Im Browser sollte unter der URL https://food.domain.tld/admin/ nun der Login in den Admin-Bereich des Django-Servers erscheinen.
__Einige Anmerkungen:__
* Mit ''DEBUG = True'' liefert Django die statischen Files aus, bei ''DEBUG = False'' nicht, dazu sollte der verwendete Webserver verwendet werden (Infos dazu: https://stackoverflow.com/a/12773084)
* Bad Request (400) kann sein, dass die falsche Domain in ''ALLOWED_HOSTS'' eingetragen ist
Wenn alles klappt und man sich über das Django-Interface einloggen und Daten editieren kann, hat für das Python-Backend alles funktioniert. Als nächstes folgt das JavaScript-Frontend React, welches als nächstes eingerichtet wird.
===== React einrichten =====
Dazu am besten ein neues Terminal verwenden und den Django-Server weiter laufen lassen, da dieser über dessen API zur Abfrage der Daten benötigt wird.
Als root (''su'') in den für das JavaScript-Frontend vorgesehenen Ordner wechseln und die Node.js-Pakete installieren:
# cd /usr/share/webapps/openeats/frontend
# npm install
Der Befehl ''npm install'' dauert eine Weile; dabei ist es wichtig, ihn als root und nicht mit sudo auszuführen, da die Installation der Pakete sonst aufgrund fehlender Schreibrechte fehlschlägt, bzw. in eine Endlosschleife laufen kann!
Ist die Installation abgeschlossen, muss in der Konfiguration von ''webpack'' noch ein Problem behoben werden. Es wird für die Ausgabe ein absoluter Pfad erwartet, allerdings stimmt dieser nicht, somit wird die erzeugte Datei ''bundle.js'' nicht in ein Verzeichnis geschrieben. Dazu in der Datei ''webpack.config.js'' im aktuellen Verzeichnis ganz oben bei den ''const'' Ausdrücken die Zeile
const path = require('path')
anfügen sowie weiter unten im Teil ''module-exports = {'' unter dem Punkt ''output: {'' die Zeile
path: '/code/public',
abändern zu
path: path.resolve(__dirname, 'public'),
Damit können wir nun den Node-Server starten. Dabei ist das Setzen der Umgebungsvariablen wichtig, da ''webpack'' (js Kram...) sonst Fehler liefert:
# NODE_ENV=production NODE_LOCALE=de NODE_API_URL=https://food.domain.tld npm start
Nun sollte alles funktionieren und mit dem Aufruf der URL https://food.domain.tld im Browser sollte die eigentliche Oberfläche zur Anzeige der Rezepte erscheinen.
__Hinweis__: Sollte es beim Starten zu einem Fehler kommen,
>ERROR in ./modules/common/history.js
>Module not found: Error: Can't resolve 'history' in '/usr/share/webapps/openeats/frontend/modules/common'
muss das ''history''-Modul explizit installiert werden:
# npm install --save history
siehe [[server:openeats#history-modul_nicht_gefunden|Troubleshooting]].
----
Quelle hierfür mit mieser Dokumentation, die nicht komplett funktioniert / die Hälfte fehlt: https://github.com/RyanNoelk/OpenEats/blob/master/docs/Running_without_Docker.md
Einfacher wäre es sicherlich mit Docker, aber auf meinem Server läuft Linux mit dem Kernel 3.8. Für Docker wird aber mindestens 3.10 benötigt, da erst dort die nötigen Infrastrukturen existieren.
===== Server manuell starten =====
Um die Server manuell zu starten, sind zwei Terminal-Sitzungen nötig: Eine für den Django-Server und eine für Node.js.
Django-Server starten:
# cd /usr/share/webapps/openeats
# source virt_env/bin/activate
# ./api/manage.py runserver
Node-Server starten:
# cd /usr/share/webapps/openeats/frontend
# NODE_ENV=production NODE_LOCALE=de NODE_API_URL=https://food.domain.tld npm start
===== Autostart umsetzen =====
Die vorhin gestarteten Server werden nun nicht mehr benötigt, da der Startprozess für die beiden Server nun automatisiert wird. Am besten eine neue Terminal-Session starten, um sicher zu sein, dass keine bereits gesetzten Umgebungsvariablen fehlen und der Start der Server fehlschlägt.
==== Django ====
Über die ''requirements.txt'' wurde bereits das ''gunicorn'' Python-Modul installiert. Dieses wird verwendet, um den Django-Server als Daemon zu starten. Dazu ändern wir die Datei ''api/base/gunicorn_start.sh'' ab mit folgendem Inhalt:
#!/usr/bin/env sh
NAME="OpenEats"
DJANGODIR=/usr/share/webapps/openeats/api
USER=root
GROUP=root
NUM_WORKERS=4
DJANGO_SETTINGS_MODULE=base.settings
DJANGO_WSGI_MODULE=base.wsgi
API_URL=localhost:8000
echo "Starting $NAME as `whoami`"
# Activate the virtual environment
source /usr/share/webapps/openeats/virt_env/bin/activate
cd $DJANGODIR
export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
export PYTHONPATH=$DJANGODIR:$PYTHONPATH
# Start your Django Unicorn
# Programs meant to be run under supervisor should not daemonize themselves (do not use --daemon)
exec gunicorn ${DJANGO_WSGI_MODULE}:application \
--name $NAME \
--workers $NUM_WORKERS \
--user=$USER --group=$GROUP \
--bind=$API_URL
Zum Testen am besten ausprobieren, ob der Server ohne Fehler startet:
# ./api/base/gunicorn_start.sh
Funktioniert alles, kümmern wir uns als nächstes darum, dass der Startprozess automatisch von ''systemd'' übernommen wird. Für Django/gunicorn erstellen wir die Datei ''/usr/lib/systemd/system/openeats_gunicorn.service'' mit folgendem Inhalt:
[Unit]
Description=OpenEats gunicorn daemon
[Service]
Type=simple
User=root
Group=root
ExecStart=/usr/share/webapps/openeats/api/base/gunicorn_start.sh
[Install]
WantedBy=multi-user.target
Das ganze wird mit
# systemctl enable openeats_gunicorn
aktiviert. Noch ein
# systemctl start openeats_gunicorn
und der Service ist gestartet und sollte nun unter https://food.domain.tld/admin erreichbar sein. Um zu testen, dass der Autostart funktioniert, am besten den Computer neustarten und mit ''systemctl status openeats_gunicorn'' testen, ob der Service läuft.
----
Quelle mit Infos zu gunicorn als Daemon: http://tutos.readthedocs.io/en/latest/source/ndg.html#gunicorn-and-daemonizing-it
==== Node ====
Als nächstes erstellen wir die Datei ''/usr/lib/systemd/system/openeats_node.service'' für den Node-Server mit dem Inhalt:
[Unit]
Description=OpenEats node daemon
After=syslog.target network.target openeats_gunicorn.service
Requires=openeats_gunicorn.service
[Service]
Restart=always
User=root
Group=root
Environment=PATH=/usr/bin:/usr/local/bin
Environment=NODE_ENV=production
Environment=NODE_LOCALE=de
Environment=NODE_API_URL=https://food.domain.tld
WorkingDirectory=/usr/share/webapps/openeats/frontend
ExecStart=/usr/bin/npm start
[Install]
WantedBy=multi-user.target
Wie vorhin den Serive mit
# systemctl enable openeats_node
aktivieren und zum Testen mit ''systemctl start openeats_node'' starten, um zu sehen, ob alles funktioniert und unter https://food.domain.tld der erwartete Inhalt erscheint. Dabei beachten, dass der Server eine Weile zum Starten braucht.
Nun sollte alles nach einem Neustart automatisch gestartet werden, wobei die entsprechende Startzeit für den Node-Server beachtet werden sollte.
===== Troubleshooting =====
==== Inkompatibles / nicht vorhandenes npm-Modul ====
Sollte es, aus welchen Gründen auch immer, ein Problem mit einem Modul geben, da es beispielsweise für die Architektur nicht angeboten wird, kann es auch einfach selbst aus den Quellen gebaut werden. Dies kann sich unter anderem beim Starten des npm-Servers in einem Fehlerstring wie beispielsweise
> ERROR in Node Sass does not yet support your current environment: Linux Unsupported architecture (arm) with Unsupported runtime (59)
äußern. In einem solchen Fall kann das Modul, in diesem Falle ''node-sass'', einfach mit einem
# npm rebuild node-sass
selbst für die aktuell verwendete Plattform kompiliert werden.
==== Update node.js ====
Wurde ''nodejs'' aktualisiert, kann es leicht Probleme mit bereits vorhandenen Modulen geben, insbesondere bei einem Update zu einer neuen Major-Version. Um dies zu vermeiden, ist es sinnvoll, den kompletten Ordner mit den Modulen zu löschen und sie neu zu installieren:
# cd /usr/share/webapps/openeats/frontend
# rm -rf node_modules
# npm install
==== history-Modul nicht gefunden ====
Speziell nach dem Update von ''nodejs'' auf Version 9.2 und einem einhergehenden Pull des aktuellen OpenEats-Repository trat bei mir während des Starten des node-Servers der folgende Fehler auf:
> ERROR in ./modules/common/history.js
> Module not found: Error: Can't resolve 'history' in '/usr/share/webapps/openeats/frontend/modules/common'
Scheinbar wurde das ''history''-Modul von React nicht mitinstalliert, was bisher (und vernünftige Konfiguration vorausgesetzt standardmäßig) der Fall sein sollte. Um dieses Problem zu beheben, kann das Paket manuell mittels
# npm install --save history
der Installation hinzugefügt werden.
==== Update Python, Upgrade der virtuellen Umgebung ====
Die angelegte virtuelle Umgebung unter ''virt_env'' muss unter Umständen aktualisiert werden, beispielsweise nach einem Upgrade von Python selbst auf ein neues Release. Dies kann dazu führen, dass beim Versuch, den Server innerhalb der virtuellen Umgebung mit ''./api/manage.py runserver'' manuell zu starten, zu Fehlern wie
> python: error while loading shared libraries: libpython3.6m.so.1.0: cannot open shared object file: No such file or directory
erscheint. Grund ist hier das Upgrade von Python 3.6 auf 3.7.
Um ein Update durchführen zu können, ermitteln wir der Einfachheit halber alle installierten Module, um sie nachher einfach wieder zu installieren. Dazu muss die Python-Version mittels Downgrade auf die letzte funktionierende Version gesetzt werden. Am besten wurde der Cache der ''pacman''-Pakete nicht geleert, so kann in ''/var/cache/pacman/pkg'' nach den vorhandenen alten Python-Paketen gesucht werden, in meinem Fall die Version 3.6.6. Diese installieren wir:
# pacman -U /var/cache/pacman/pkg/python-3.6.6-1-armv7h.pkg.tar.xz
Nun kann die virtuelle Umgebung aktiviert werden, um die verwendeten Module zu ermitteln:
# source virt_env/bin/activate
# pip freeze > requirements.txt
# deactivate
# mv virt_env virt_env_bak
Bei dem letzten Schritt wird die Umgebung als Backup in einen neuen Ordner verschoben.
Anschließend kann Python wieder aktualisiert werden:
# pacman -S python
Bei mir ist in der Liste der Module in ''requirements.txt'' das Modul ''openeats-recipe-scraper'' aufgetaucht. Dies gehört zu OpenEats und kann entsprechend nicht mit ''pip'' installiert werden. Die Zeile mit diesem Modul muss noch aus der Textdatei gelöscht werden.
Die neue Umgebung kann nun eingerichtet werden:
# mkdir virt_env
# virtualenv virt_env/
# source virt_env/bin/activate
# pip install --upgrade -r requirements.txt
Wird nun versucht, den Server manuell mit ''./api/manage.py runserver'' zu starten, sollte es keine Fehlermeldung mehr geben. Wir können die virtuelle Umgebung mit ''deactivate'' wieder verlassen und das Backup ''virt_env_bak'' kann entsprechend gelöscht werden, wenn alles funktioniert hat.
==== Update MariaDB, Django-Server startet nicht ====
Es kann passieren, dass es nach einem größeren Update von MariaDB, welches MySQL zur Verfügung stellt, Probleme beim Start des Django-Servers gibt. Dies zeigt sich darin, dass keine Bilder oder Rezepte geladen/gefunden werden. Wird wie weiter oben beschrieben versucht, den Server manuell mittels ''./api/manage.py runserver'' zu starten, tauchen in diesem Fall sehr viele Probleme auf sowie ganz am Ende etwas wie
> django.core.exceptions.ImproperlyConfigured: Error loading MySQLdb module: libmysqlclient.so.18: cannot open shared object file: No such file or directory
In diesem Fall reicht es, das Python-Paket ''mysqlclient'' neu zu installieren, damit die neue Version die im System vorhandene Bibliothek wieder findet. Zur Sicherheit kann analog zum vorherigen Punkt ein Backup angelegt werden. Für das Update selber wechseln wir ins passende OpenEats-Verzeichnis, wechseln in die angelegte virtuelle Umgebung und führen das Paket-Update mittels ''pip'' durch:
# source virt_env/bin/activate
# pip uninstall mysqlclient
# pip install mysqlclient
# deactivate
Mit dem letzten Befehl wird die virtuelle Umgebung wieder verlassen. Hat alles geklappt, sollte der Aufruf von ''./api/manage.py runserver'' nun zum gewünschten Resultat führen.