αὐτάρκης

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. 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 install libpq-dev apt install psycopg2

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=# 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.