Gut ein Jahr nach dem Rückbau des Webstacks auf meinem hessischen Server siegt mein Basteltrieb über den Homo oeconomicus, und ich repliziere meine Django-Betriebsumgebung auf einem lokalen Ubuntu-Server. Auf eine Festplattenverschlüsselung verzichte ich angesichts der notwendigen Prozedur für kopflose Systeme. Bei dieser Gelegenheit lerne ich, dass eine Umstellung von Ubuntu Server auf Ubuntu Desktop nachträglich mit der Befehlsfolge apt update; apt install ubuntu-desktop; reboot now
bewerkstelligt werden kann, so dass nicht das wesentlich umfangreichere Desktop-Image benötigt wird – wenn man eine Desktop-Umgebung haben möchte. Die Installation des Webstacks gestaltet sich zunächst sehr einfach –
apt install nginx apt install postgresql
– bis der Datenbankkonnektor über eine fehlende Header-Bibliothek stolpert. Daher:
apt install libpq-dev apt install python3-psycopg2 apt install python3-pip
In Bezug auf das Sorgenkind uwsgi bietet apt (anders als Homebrew) ein Plugin-Paket für Python 3, so dass ich nicht auf pip und die Definition einer eigenen Service Unit zurückgreifen muss:
apt install uwsgi apt install uwsgi-plugin-python3
Die initiale Anmeldung am PostgreSQL-Server erfolgt neuerdings über das Peer-Verfahren, so dass ich zunächst das Systempasswort für postgres
ändern muss, um mich anschließend in der Rolle dieser Nutzerin passwortlos anmelden zu können:
sudo passwd postgres su - postgres psql postgres=# CREATE USER myuser WITH PASSWORD 'snafu'; postgres=# CREATE DATABASE db1; postgres=# GRANT ALL ON DATABASE db1 TO myuser; postgres=# \q exit psql db1 < ~/var/dbdump/db1.sql
Die Änderung der Authentifikationsmethode von peer
auf scram-sha-256
in /etc/postgresql/14/main/pg_hba.conf
für lokale Nutzerinnen ermöglicht die Verwendung des Passworts snafu
auch ohne eine Systemnutzerin myuser
.
Nun folgt die Installation des Webframeworks –
pip3 install Django
und der Transfer der Projektdaten auf den Server. Abschließend müssen lediglich geeignete Konfigurationsdateien in /etc/uwsgi/apps-enabled
bzw. /etc/nginx/sites/enabled
hinterlegt und alle relevanten Services gestartet, ~/.pgpass
angelegt und die Umgebungsvariablen PGHOST
, PGUSER
und PGDATABASE
in ~/.bashrc
eingetragen werden.
Natürlich sollte die Synchronisation des Datenbestandes zwischen Redaktionssystem und Server weitgehend automatisiert ablaufen:
#!/usr/local/bin/python3 import os import subprocess import argparse import paramiko local_dir = '/Users/snafu/website/dbdump/' remote_dir = '/var/www/djangosite/dbdump/' db = 'mydb' dbuser = 'myuser' extension = '.sql' dumpfile = local_dir + db + extension targetfile = remote_dir + db + extension configparser = paramiko.SSHConfig() configparser.parse(open('/Users/snafu/.ssh/config')) ssh_config = configparser.lookup('myserver') ssh_key = paramiko.ECDSAKey.from_private_key(open(ssh_config['identityfile'][0])) def print_status(return_code): if return_code == 0: print('done.') else: print('failed!') def dump_db(): print('Dumping local database...') with open(dumpfile, 'w') as writefile: completed_process = subprocess.run(['pg_dump', '-E', 'UTF8', '-c', db], stdout=writefile) print_status(completed_process.returncode) def transfer_db(): print('Sending database dump to server ...') transport = paramiko.Transport((ssh_config['hostname'],int(ssh_config['port']))) transport.connect(username=ssh_config['user'], pkey=ssh_key) sftp = paramiko.SFTPClient.from_transport(transport) try: sftp.put(dumpfile,targetfile) print_status(0) except: print_status(1) if sftp: sftp.close() if transport: transport.close() def update_db(): print('Updating remote database ...') client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(ssh_config['hostname'], username=ssh_config['user'], port=ssh_config['port'], key_filename=ssh_config['identityfile'][0]) stdin, stdout, stderr = client.exec_command(f'psql -U {dbuser} {db} < {targetfile}') return_code = stdout.channel.recv_exit_status() print_status(return_code) client.close() def call_func(function_name): dispatcher = { 'dump_db' : dump_db, 'transfer_db' : transfer_db, 'update_db' : update_db } return dispatcher[function_name]() def main(): p = argparse.ArgumentParser(description='Synchronize website database') p.add_argument('-d', '--dump', dest='actions', action='append_const', const='dump_db') p.add_argument('-t', '--transfer', dest='actions', action='append_const', const='transfer_db') p.add_argument('-u', '--update', dest='actions', action='append_const', const='update_db') arguments = p.parse_args() if arguments.actions: arguments.actions.sort() for action in arguments.actions: call_func(action) if __name__=='__main__': main()
Die Abfrage des return codes (return_code = stdout.channel.recv_exit_status()
) in update_db()
dient nicht nur dazu, das Skript kommunikativer zu gestalten, sondern wandelt exec_command()
auch in einen blocking call (alternativ könnte man den psql
-Aufruf auch mit nohup
dekorieren, damit die Datenbankaktualisierung auch nach dem Schließen der SSH-Verbindung ausgeführt wird). Warum Transport()
einen bereits ausgelesenen SSH-Schlüssel als Parameter benötigt, SSHClient()
aber nur den Dateinamen, bleibt dagegen das Geheimnis der Paramiko-Entwicklerinnen.