Compare commits

..

No commits in common. "020ef6ae0c8d7c8b7dd002dc9a06f7917003ecfa" and "a1bd01f0c1db803a482ad9f647d3e268ba1f9cca" have entirely different histories.

21 changed files with 517 additions and 957 deletions

View File

@ -1064,7 +1064,7 @@ Terminal login : sudo machinectl login fbx-{distribution}
Open a root shell : sudo machinectl shell fbx-{distribution}
Shutdown : {script} stop {options}
Destroy : {script} destroy {options}
Reset : {script} destroy {options}; {script} start {options}'''
Reset : {script} destroy {options}; {script} up {options}'''
logger.info(message)

View File

@ -369,6 +369,7 @@ class JSONEncoder(json.JSONEncoder):
def _setup_thread_storage():
"""Setup collection of stdout/stderr from any process in this thread."""
global thread_storage
thread_storage.stdout = b''
thread_storage.stderr = b''
@ -379,12 +380,14 @@ def _clear_thread_storage():
Python documentation is silent on whether thread local storage will be
cleaned up after a thread terminates.
"""
global thread_storage
thread_storage.stdout = None
thread_storage.stderr = None
def get_return_value_from_exception(exception):
"""Return the value to return from server when an exception is raised."""
global thread_storage
return_value = {
'result': 'exception',
'exception': {

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-03 01:14+0000\n"
"PO-Revision-Date: 2026-02-09 19:09+0000\n"
"PO-Revision-Date: 2025-12-17 07:00+0000\n"
"Last-Translator: Jiří Podhorecký <j.podhorecky@volny.cz>\n"
"Language-Team: Czech <https://hosted.weblate.org/projects/freedombox/"
"freedombox/cs/>\n"
@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2);\n"
"X-Generator: Weblate 5.16-dev\n"
"X-Generator: Weblate 5.15.1-dev\n"
#: plinth/config.py:103
#, python-brace-format
@ -9934,14 +9934,15 @@ msgid "Not configured yet."
msgstr "Zatím není nakonfigurováno."
#: plinth/modules/wireguard/templates/wireguard.html:59
#, python-format
#, fuzzy, python-format
#| msgid "Public key for this %(box_name)s:"
msgid "Endpoints for this %(box_name)s:"
msgstr "Endpointy pro tento %(box_name)s:"
msgstr "Veřejný klíč pro tento %(box_name)s:"
#: plinth/modules/wireguard/templates/wireguard.html:75
#: plinth/modules/wireguard/templates/wireguard.html:77
msgid "Start WireGuard Server"
msgstr "Spustit WireGuard Server"
msgstr ""
#: plinth/modules/wireguard/templates/wireguard.html:81
msgid "Add a new peer"
@ -9980,8 +9981,10 @@ msgid "Add Connection to Server"
msgstr "Přidat připojení k serveru"
#: plinth/modules/wireguard/templates/wireguard_add_client.html:21
#, fuzzy
#| msgid "IP address to use for client:"
msgid "IP address that will be assigned to this client"
msgstr "IP adresa, která bude přiřazena k tomuto klientovi"
msgstr "IP adresa, která se použije pro klienta:"
#: plinth/modules/wireguard/templates/wireguard_add_client.html:31
msgid "Add Client"
@ -10129,12 +10132,14 @@ msgid "Server deleted."
msgstr "Server smazán."
#: plinth/modules/wireguard/views.py:286
#, fuzzy
#| msgid "Password changed successfully."
msgid "WireGuard server started successfully."
msgstr "Server WireGuard se úspěšně spustil."
msgstr "Heslo úspěšně změněno."
#: plinth/modules/wireguard/views.py:290
msgid "Failed to start WireGuard server: {}"
msgstr "Spuštění serveru WireGuard selhalo: {}"
msgstr ""
#: plinth/modules/wordpress/__init__.py:20
msgid ""

View File

@ -10,8 +10,8 @@ msgstr ""
"Project-Id-Version: FreedomBox UI\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-03 01:14+0000\n"
"PO-Revision-Date: 2026-02-05 11:01+0000\n"
"Last-Translator: Dietmar <sagen@permondes.de>\n"
"PO-Revision-Date: 2026-01-25 14:02+0000\n"
"Last-Translator: Ettore Atalan <atalanttore@googlemail.com>\n"
"Language-Team: German <https://hosted.weblate.org/projects/freedombox/"
"freedombox/de/>\n"
"Language: de\n"
@ -10156,14 +10156,17 @@ msgid "Not configured yet."
msgstr "Noch nicht konfiguriert."
#: plinth/modules/wireguard/templates/wireguard.html:59
#, python-format
#, fuzzy, python-format
#| msgid "Public key for this %(box_name)s:"
msgid "Endpoints for this %(box_name)s:"
msgstr "Endpunkte für diese %(box_name)s:"
msgstr "Öffentlicher Schlüssel für diese %(box_name)s:"
#: plinth/modules/wireguard/templates/wireguard.html:75
#: plinth/modules/wireguard/templates/wireguard.html:77
#, fuzzy
#| msgid "Standard Services"
msgid "Start WireGuard Server"
msgstr "WireGuard-Server starten"
msgstr "Standarddienste"
#: plinth/modules/wireguard/templates/wireguard.html:81
msgid "Add a new peer"
@ -10202,8 +10205,10 @@ msgid "Add Connection to Server"
msgstr "Verbindung zum Server hinzufügen"
#: plinth/modules/wireguard/templates/wireguard_add_client.html:21
#, fuzzy
#| msgid "IP address to use for client:"
msgid "IP address that will be assigned to this client"
msgstr "IP-Adresse, die diesem Client zugewiesen wird"
msgstr "IP-Adresse, die für den Client verwendet werden soll:"
#: plinth/modules/wireguard/templates/wireguard_add_client.html:31
msgid "Add Client"
@ -10352,12 +10357,14 @@ msgid "Server deleted."
msgstr "Server gelöscht."
#: plinth/modules/wireguard/views.py:286
#, fuzzy
#| msgid "Password changed successfully."
msgid "WireGuard server started successfully."
msgstr "Der WireGuard-Server wurde erfolgreich gestartet."
msgstr "Passwort erfolgreich geändert."
#: plinth/modules/wireguard/views.py:290
msgid "Failed to start WireGuard server: {}"
msgstr "Starten des WireGuard-Servers fehlgeschlagen: {}"
msgstr ""
#: plinth/modules/wordpress/__init__.py:20
msgid ""

View File

@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-03 01:14+0000\n"
"PO-Revision-Date: 2026-02-06 23:01+0000\n"
"PO-Revision-Date: 2026-02-03 01:07+0000\n"
"Last-Translator: Pierfrancesco Passerini <p.passerini@gmail.com>\n"
"Language-Team: Italian <https://hosted.weblate.org/projects/freedombox/"
"freedombox/it/>\n"
@ -10026,14 +10026,15 @@ msgid "Not configured yet."
msgstr "Non ancora configurato."
#: plinth/modules/wireguard/templates/wireguard.html:59
#, python-format
#, fuzzy, python-format
#| msgid "Public key for this %(box_name)s:"
msgid "Endpoints for this %(box_name)s:"
msgstr "Punti di accesso per %(box_name)s:"
msgstr "Chiave pubblica di %(box_name)s:"
#: plinth/modules/wireguard/templates/wireguard.html:75
#: plinth/modules/wireguard/templates/wireguard.html:77
msgid "Start WireGuard Server"
msgstr "Avvia WireGuard server"
msgstr ""
#: plinth/modules/wireguard/templates/wireguard.html:81
msgid "Add a new peer"
@ -10072,8 +10073,10 @@ msgid "Add Connection to Server"
msgstr "Aggiungi Connessione a Server"
#: plinth/modules/wireguard/templates/wireguard_add_client.html:21
#, fuzzy
#| msgid "IP address to use for client:"
msgid "IP address that will be assigned to this client"
msgstr "Indirizzo IP che sarà assegnato al client"
msgstr "Indirizzo IP da utilizzare sul client:"
#: plinth/modules/wireguard/templates/wireguard_add_client.html:31
msgid "Add Client"
@ -10221,12 +10224,14 @@ msgid "Server deleted."
msgstr "Server cancellato."
#: plinth/modules/wireguard/views.py:286
#, fuzzy
#| msgid "Password changed successfully."
msgid "WireGuard server started successfully."
msgstr "Il server WireGuard è stato correttamente avviato."
msgstr "La password è stata aggiornata."
#: plinth/modules/wireguard/views.py:290
msgid "Failed to start WireGuard server: {}"
msgstr "Avvio del server WireGuard non riuscito: {}"
msgstr ""
#: plinth/modules/wordpress/__init__.py:20
msgid ""

View File

@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-03 01:14+0000\n"
"PO-Revision-Date: 2026-02-06 23:01+0000\n"
"Last-Translator: Daniel Wiik <d.wiik@outlook.com>\n"
"PO-Revision-Date: 2024-07-30 01:31+0000\n"
"Last-Translator: bittin1ddc447d824349b2 <bittin@reimu.nl>\n"
"Language-Team: Swedish <https://hosted.weblate.org/projects/freedombox/"
"freedombox/sv/>\n"
"Language: sv\n"
@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.16-dev\n"
"X-Generator: Weblate 5.7-dev\n"
#: plinth/config.py:103
#, python-brace-format
@ -25,9 +25,10 @@ msgid "Static configuration {etc_path} is setup properly"
msgstr "Statisk konfiguration {etc_path} är inställd korrekt"
#: plinth/container.py:140
#, python-brace-format
#, fuzzy, python-brace-format
#| msgid "Service {service_name} is running"
msgid "Container {container_name} is running"
msgstr "Tjänsten {container_name} körs"
msgstr "Tjänsten {service_name} körs"
#: plinth/context_processors.py:21 plinth/views.py:175
msgid "FreedomBox"
@ -71,8 +72,10 @@ msgid "Repository to backup to"
msgstr "Repository att säkerhetskopiera till"
#: plinth/forms.py:62
#, fuzzy
#| msgid "None"
msgid "(None)"
msgstr "(Ingen)"
msgstr "Ingen"
#: plinth/forms.py:68
msgid "Select a domain name to be used with this application"
@ -140,17 +143,22 @@ msgid "System is possibly under heavy load. Please retry later."
msgstr "Systemet är möjligen under tung belastning. Försök igen senare."
#: plinth/middleware.py:147
#, python-brace-format
#, fuzzy, python-brace-format
#| msgid "Page not found - %(box_name)s"
msgid "Page not found: {url}"
msgstr "Sidan hittades inte: {url}"
msgstr "Sidan hittades inte - %(box_name)s"
#: plinth/middleware.py:150
#, fuzzy
#| msgid "Error running apt-get"
msgid "Error running operation."
msgstr "Fel vid körning av åtgärden."
msgstr "Fel vid körning av apt-get"
#: plinth/middleware.py:152
#, fuzzy
#| msgid "Error running apt-get"
msgid "Error loading page."
msgstr "Fel vid laddning av sida."
msgstr "Fel vid körning av apt-get"
#: plinth/modules/apache/__init__.py:33
msgid "Apache HTTP Server"
@ -202,15 +210,15 @@ msgstr "Lokalt nätverksdomän"
#: plinth/modules/avahi/manifest.py:14
msgid "Auto-discovery"
msgstr "Autoupptäck"
msgstr ""
#: plinth/modules/avahi/manifest.py:14 plinth/modules/backups/manifest.py:17
msgid "Local"
msgstr "Lokal"
msgstr ""
#: plinth/modules/avahi/manifest.py:14
msgid "mDNS"
msgstr "mDNS"
msgstr ""
#: plinth/modules/backups/__init__.py:24
msgid "Backups allows creating and managing backup archives."
@ -320,8 +328,6 @@ msgid ""
"In 24 hour format. Services may become temporarily unavailable while running "
"backup operation at this time of the day."
msgstr ""
"24-timmars format. Tjänster kan bli tillfälligt otillgängliga när du kör "
"säkerhetskopiering vid denna tidpunkt på dagen."
#: plinth/modules/backups/forms.py:85 plinth/modules/backups/forms.py:107
msgid "Included apps"
@ -367,9 +373,6 @@ msgid ""
"file previously downloaded from the result of a successful backup on a "
"{box_name}. It must have a .tar.gz extension."
msgstr ""
"Välj backup-filen för att ladda upp från den lokala datorn. Detta måste vara "
"en fil som tidigare laddats ner från resultatet av en framgångsrik "
"säkerhetskopia på en {box_name}. Det måste ha en .tar.gz förlängning."
#: plinth/modules/backups/forms.py:154
msgid "Repository path format incorrect."
@ -471,12 +474,16 @@ msgid "Restore"
msgstr "Återställa"
#: plinth/modules/backups/manifest.py:15
#, fuzzy
#| msgid "Encryption"
msgid "Encrypted"
msgstr "Krypterad"
msgstr "Kryptering"
#: plinth/modules/backups/manifest.py:16
#, fuzzy
#| msgid "Schedule"
msgid "Schedules"
msgstr "Scheman"
msgstr "Schema"
#: plinth/modules/backups/manifest.py:18
#, fuzzy
@ -485,8 +492,10 @@ msgid "Remote"
msgstr "Ta bort"
#: plinth/modules/backups/manifest.py:19
#, fuzzy
#| msgid "App updated"
msgid "App data"
msgstr "App data"
msgstr "App uppdaterad"
#: plinth/modules/backups/manifest.py:20
#: plinth/modules/homeassistant/templates/homeassistant.html:11
@ -530,20 +539,22 @@ msgid "Repository path is neither empty nor is an existing backups repository."
msgstr "Respositorysökvägen är varken tom eller en befintlig säkerhetskopia."
#: plinth/modules/backups/privileged.py:77
#, fuzzy
#| msgid "A share with this name already exists."
msgid "An archive with given name already exists in the repository."
msgstr "Ett arkiv med givna namn finns redan i kodförrådet."
msgstr "Det finns redan en share med det här namnet."
#: plinth/modules/backups/privileged.py:84
msgid "Archive with given name was not found in the repository."
msgstr "Arkiv med givna namn hittades inte i kodförrådet."
msgstr ""
#: plinth/modules/backups/privileged.py:90
msgid "Backup system is busy with another operation."
msgstr "Backupsystemet är upptaget med en annan process."
msgstr ""
#: plinth/modules/backups/privileged.py:95
msgid "Not enough space left on the disk or remote location."
msgstr "Inte tillräckligt med utrymme kvar på disken eller fjärrplatsen."
msgstr ""
#: plinth/modules/backups/repository.py:93
msgid "Existing repository is not encrypted."
@ -700,17 +711,27 @@ msgid "Restore data from"
msgstr "Återställa data från"
#: plinth/modules/backups/templates/backups_upload.html:17
#, python-format
#, fuzzy, python-format
#| msgid ""
#| "\n"
#| " Upload a backup file downloaded from another %(box_name)s to "
#| "restore its\n"
#| " contents. You can choose the apps you wish to restore after "
#| "uploading a\n"
#| " backup file.\n"
#| " "
msgid ""
"Upload a backup file downloaded from another %(box_name)s to restore its "
"contents. You can choose the apps you wish to restore after uploading a "
"backup file."
msgstr ""
"Ladda upp en backup-fil som hämtats från en annan %(box_name)s för att "
"\n"
" Ladda upp en backup-fil som hämtats från en annan %(box_name)s för att "
"återställa\n"
" Innehållet. Du kan välja vilka appar du vill återställa efter att du "
"laddat upp en\n"
" backup-fil."
" backup-fil.\n"
" "
#: plinth/modules/backups/templates/backups_upload.html:31
#, python-format
@ -786,8 +807,10 @@ msgid "Upload and restore a backup"
msgstr "Ladda upp och återställ en säkerhetskopia"
#: plinth/modules/backups/views.py:216
#, fuzzy
#| msgid "Logged out successfully."
msgid "Upload successful."
msgstr "Uppladdning genomförd."
msgstr "Du har loggat ut framgångsrikt."
#: plinth/modules/backups/views.py:254
msgid "No backup file found."
@ -812,8 +835,10 @@ msgid "Create backup repository"
msgstr "Skapa backup repository"
#: plinth/modules/backups/views.py:350
#, fuzzy
#| msgid "Added new remote SSH repository."
msgid "Added new repository."
msgstr "Lade till nytt kodförråd."
msgstr "Lade till ett nytt remote SSH-repository."
#: plinth/modules/backups/views.py:364
msgid "Create remote backup repository"
@ -972,12 +997,14 @@ msgstr ""
#: plinth/modules/samba/manifest.py:81 plinth/modules/sharing/manifest.py:19
#: plinth/modules/syncthing/manifest.py:58
#: plinth/modules/transmission/manifest.py:39
#, fuzzy
#| msgid "File & Snippet Sharing"
msgid "File sharing"
msgstr "Fildelning"
msgstr "Fil &amp; Snippet Sharing"
#: plinth/modules/bepasty/manifest.py:23
msgid "Pastebin"
msgstr "Pastebin"
msgstr ""
#: plinth/modules/bepasty/templates/bepasty.html:12
msgid "Manage Passwords"
@ -1107,8 +1134,10 @@ msgstr ""
"förfrågningar kommer att vidarebefordras"
#: plinth/modules/bind/manifest.py:16
#, fuzzy
#| msgid "Enable DNSSEC"
msgid "DNS"
msgstr "DNS"
msgstr "Aktivera DNSSEC"
#: plinth/modules/bind/manifest.py:17 plinth/modules/mumble/manifest.py:67
#: plinth/modules/radicale/manifest.py:91
@ -1118,7 +1147,7 @@ msgstr "Server"
#: plinth/modules/bind/manifest.py:18
msgid "Resolver"
msgstr "Resolver"
msgstr ""
#: plinth/modules/bind/templates/bind.html:11
msgid "Serving Domains"
@ -1227,15 +1256,19 @@ msgstr "Ett bibliotek med detta namn finns redan."
#: plinth/modules/calibre/manifest.py:21
msgid "Ebook"
msgstr "Ebok"
msgstr ""
#: plinth/modules/calibre/manifest.py:21
#, fuzzy
#| msgid "E-book Library"
msgid "Library"
msgstr "Bibliotek"
msgstr "E-bok Bibliotek"
#: plinth/modules/calibre/manifest.py:21
#, fuzzy
#| msgid "E-book Library"
msgid "Ebook reader"
msgstr "E-boksbibliotek"
msgstr "E-bok Bibliotek"
#: plinth/modules/calibre/templates/calibre-delete-library.html:11
#, python-format
@ -1343,12 +1376,16 @@ msgid "Cockpit"
msgstr "Cockpit"
#: plinth/modules/cockpit/manifest.py:23
#, fuzzy
#| msgid "Administration"
msgid "Advanced administration"
msgstr "Avancerad administration"
msgstr "Administrering"
#: plinth/modules/cockpit/manifest.py:24
#, fuzzy
#| msgid "Sharing"
msgid "Web terminal"
msgstr "Webbterminal"
msgstr "Sharing"
#: plinth/modules/cockpit/manifest.py:25 plinth/modules/storage/__init__.py:47
#: plinth/modules/storage/__init__.py:319
@ -1358,6 +1395,8 @@ msgid "Storage"
msgstr "Lagring"
#: plinth/modules/cockpit/manifest.py:26
#, fuzzy
#| msgid "Networks"
msgid "Networking"
msgstr "Nätverk"
@ -1368,7 +1407,7 @@ msgstr "Tjänster"
#: plinth/modules/cockpit/manifest.py:28 plinth/templates/app-logs.html:9
msgid "Logs"
msgstr "Loggar"
msgstr ""
#: plinth/modules/cockpit/manifest.py:29
#: plinth/modules/performance/__init__.py:16
@ -1377,12 +1416,16 @@ msgid "Performance"
msgstr "Prestanda"
#: plinth/modules/config/__init__.py:20
#, fuzzy
#| msgid ""
#| "Here you can set some general configuration options like hostname, domain "
#| "name, webserver home page etc."
msgid ""
"Here you can set some general configuration options like webserver home page "
"etc."
msgstr ""
"Här kan du ställa in några allmänna konfigurationsalternativ som webbserver, "
"hemsida med mera."
"Här kan du ställa in några allmänna konfigurationsalternativ som värdnamn, "
"domännamn, webserver, hemsida etc."
#: plinth/modules/config/__init__.py:42
msgid "General Configuration"
@ -1467,11 +1510,13 @@ msgstr "Hemsida"
#: plinth/modules/config/manifest.py:8
msgid "Logging"
msgstr "Loggning"
msgstr ""
#: plinth/modules/config/manifest.py:8
#, fuzzy
#| msgid "Advanced"
msgid "Advanced apps"
msgstr "Avancerade appar"
msgstr "Avancerat"
#: plinth/modules/config/views.py:41
#, python-brace-format
@ -1528,15 +1573,15 @@ msgstr "Ogiltig lista för URI:er för STUN/TURN-servrar"
#: plinth/modules/coturn/manifest.py:7 plinth/modules/janus/manifest.py:16
msgid "Video conference"
msgstr "Videokonferens"
msgstr ""
#: plinth/modules/coturn/manifest.py:7
msgid "STUN"
msgstr "STUN"
msgstr ""
#: plinth/modules/coturn/manifest.py:7
msgid "TURN"
msgstr "TURN"
msgstr ""
#: plinth/modules/coturn/templates/coturn.html:15
msgid "Use the following URLs to configure your communication server:"
@ -1580,10 +1625,14 @@ msgid "-- no time zone set --"
msgstr "-- ingen tidszon inställd --"
#: plinth/modules/datetime/manifest.py:15
#, fuzzy
#| msgid "Network Interface"
msgid "Network time"
msgstr "Nätverkstid"
msgstr "Nätverksgränssnitt"
#: plinth/modules/datetime/manifest.py:15
#, fuzzy
#| msgid "Time Zone"
msgid "Timezone"
msgstr "Tidszon"
@ -1629,18 +1678,24 @@ msgstr "Bittorrent-klient skriven i Python / PyGTK"
#: plinth/modules/deluge/manifest.py:21
#: plinth/modules/transmission/manifest.py:39
#, fuzzy
#| msgid "Deluge BitTorrent"
msgid "BitTorrent"
msgstr "BitTorrent"
msgstr "Deluge BitTorrent"
#: plinth/modules/deluge/manifest.py:21 plinth/modules/roundcube/manifest.py:23
#: plinth/modules/transmission/manifest.py:39
#, fuzzy
#| msgid "Launch web client"
msgid "Web client"
msgstr "Webklient"
msgstr "Starta webbklient"
#: plinth/modules/deluge/manifest.py:21 plinth/modules/syncthing/manifest.py:58
#: plinth/modules/transmission/manifest.py:39
#, fuzzy
#| msgid "I2P"
msgid "P2P"
msgstr "P2P"
msgstr "I2P"
#: plinth/modules/diagnostics/__init__.py:32
msgid ""
@ -1656,8 +1711,6 @@ msgid ""
"This app also shows the <a href=\"{logs_url}\">logs</a> for {box_name} "
"services."
msgstr ""
"Denna app visar också <a href=\"{logs_url}\">loggar</a> för {box_name} "
"tjänster."
#: plinth/modules/diagnostics/__init__.py:60
#: plinth/modules/diagnostics/__init__.py:255
@ -1666,7 +1719,7 @@ msgstr "Diagnostik"
#: plinth/modules/diagnostics/__init__.py:115
msgid "skipped"
msgstr "Hoppat över"
msgstr ""
#: plinth/modules/diagnostics/__init__.py:116
msgid "passed"
@ -1753,7 +1806,7 @@ msgstr "Om problem upptäcks, försök att reparera dem automatiskt."
#: plinth/modules/diagnostics/manifest.py:10
msgid "Detect problems"
msgstr "Finn problem"
msgstr ""
#: plinth/modules/diagnostics/manifest.py:10
msgid "Repair"
@ -1761,7 +1814,7 @@ msgstr "Reparera"
#: plinth/modules/diagnostics/manifest.py:10
msgid "Daily"
msgstr "Daglig"
msgstr ""
#: plinth/modules/diagnostics/templates/diagnostics.html:11
msgid "Diagnostics Run"
@ -1812,31 +1865,33 @@ msgstr "Resultat"
#: plinth/modules/diagnostics/templates/diagnostics_full.html:53
#, python-format
msgid "%(number)s passed"
msgstr "%(number)s passerade"
msgstr ""
#: plinth/modules/diagnostics/templates/diagnostics_full.html:57
#, python-format
msgid "%(number)s failed"
msgstr "%(number)s misslyckades"
msgstr ""
#: plinth/modules/diagnostics/templates/diagnostics_full.html:61
#, python-format
msgid "%(number)s warnings"
msgstr "%(number)s varningar"
msgstr ""
#: plinth/modules/diagnostics/templates/diagnostics_full.html:65
#, python-format
msgid "%(number)s errors"
msgstr "%(number)s fel"
msgstr ""
#: plinth/modules/diagnostics/templates/diagnostics_full.html:69
#, python-format
msgid "%(number)s skipped"
msgstr "%(number)s skippade"
msgstr ""
#: plinth/modules/diagnostics/templates/diagnostics_full.html:111
#, fuzzy
#| msgid "deluge-web is running"
msgid "Running..."
msgstr "Kör..."
msgstr "Deluges webbgränssnitt är aktiverat"
#: plinth/modules/diagnostics/templates/diagnostics_results.html:11
msgid "Test"

View File

@ -1,14 +1,12 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""FreedomBox app to manage backup archives."""
import collections.abc
import contextlib
import json
import logging
import os
import pathlib
import re
import subprocess
from pathlib import Path
from django.utils.text import get_valid_filename
from django.utils.translation import gettext_lazy as _
@ -16,10 +14,9 @@ from django.utils.translation import gettext_noop
from plinth import app as app_module
from plinth import cfg, glib, menu
from plinth.modules.names.components import DomainName
from plinth.package import Packages
from . import api, errors, manifest, privileged
from . import api, manifest, privileged
logger = logging.getLogger(__name__)
@ -135,77 +132,9 @@ def restore_from_upload(path, app_ids=None):
create_subvolume=False, backup_file=path)
def get_known_hosts_path() -> Path:
def get_known_hosts_path():
"""Return the path to the known hosts file."""
return Path(cfg.data_dir) / '.ssh' / 'known_hosts'
def get_ssh_client_auth_key_paths() -> tuple[Path, Path]:
"""Return the paths to the SSH client public key and private key."""
key_path = Path(cfg.data_dir) / '.ssh' / 'id_ed25519'
pubkey_path = key_path.with_suffix('.pub')
return pubkey_path, key_path
def generate_ssh_client_auth_key():
"""Generate SSH client authentication keypair, if needed."""
_, key_path = get_ssh_client_auth_key_paths()
key_path.parent.mkdir(parents=True, exist_ok=True)
if key_path.exists():
logger.info('SSH client key %s for FreedomBox service already exists',
key_path)
return
domain_name = DomainName.list_names()[0]
logger.info('Generating SSH client key %s for FreedomBox service',
key_path)
subprocess.run([
'ssh-keygen', '-t', 'ed25519', '-N', '', '-C',
f'freedombox@{domain_name}', '-f',
str(key_path)
], stdout=subprocess.DEVNULL, check=True)
def get_ssh_client_public_key() -> str:
"""Get SSH client public key for FreedomBox service."""
pubkey_path, _ = get_ssh_client_auth_key_paths()
with pubkey_path.open('r') as pubkey_file:
pubkey = pubkey_file.read()
return pubkey
def copy_ssh_client_public_key(pubkey_path: str, hostname: str, username: str,
password: str):
"""Copy the SSH client public key to the remote server.
Returns whether the copy was successful, and any error message.
"""
env = os.environ.copy()
env['SSHPASS'] = str(password)
with raise_ssh_error():
try:
subprocess.run([
'sshpass', '-e', 'ssh-copy-id', '-i', pubkey_path,
f'{username}@{hostname}'
], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True,
env=env)
logger.info("Copied SSH client public key to remote host's "
"authorized keys.")
except subprocess.CalledProcessError as exception:
logger.warning('Failed to copy SSH client public key: %s',
exception.stderr)
raise
@contextlib.contextmanager
def raise_ssh_error() -> collections.abc.Generator[None]:
"""Convert subprocess error to SshError."""
try:
yield
except subprocess.CalledProcessError as exception:
raise errors.SshError(exception.returncode, exception.cmd,
exception.output, exception.stderr)
return pathlib.Path(cfg.data_dir) / '.ssh' / 'known_hosts'
def is_ssh_hostkey_verified(hostname):

View File

@ -1,24 +1,18 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
import subprocess
from plinth.errors import PlinthError
class BorgError(PlinthError):
"""Generic borg errors."""
"""Generic borg errors"""
class BorgRepositoryDoesNotExistError(BorgError):
"""Borg access to a repository works but the repository does not exist."""
class SshError(subprocess.CalledProcessError):
"""Error when running an SSH command."""
"""Borg access to a repository works but the repository does not exist"""
class SshfsError(PlinthError):
"""Generic sshfs errors."""
"""Generic sshfs errors"""
class BorgRepositoryExists(BorgError):

View File

@ -184,7 +184,7 @@ class EncryptedBackupsMixin(forms.Form):
choices=[('repokey', _('Key in Repository')), ('none', _('None'))])
encryption_passphrase = forms.CharField(
label=_('Passphrase'),
help_text=_('Only needed when using encryption.'),
help_text=_('Passphrase; Only needed when using encryption.'),
widget=forms.PasswordInput(), required=False)
confirm_encryption_passphrase = forms.CharField(
label=_('Confirm Passphrase'), help_text=_('Repeat the passphrase.'),
@ -251,30 +251,13 @@ class AddRemoteRepositoryForm(EncryptedBackupsMixin, forms.Form):
help_text=_('Path of a new or existing repository. Example: '
'<i>user@host:~/path/to/repo/</i>'),
validators=[repository_validator])
ssh_auth_type = forms.ChoiceField(
label=_('SSH Authentication Type'),
help_text=_('Choose how to authenticate to the remote SSH server.'),
widget=forms.RadioSelect(),
choices=[('key_auth', _('Key-based Authentication')),
('password_auth', _('Password-based Authentication'))])
ssh_password = forms.CharField(
label=_('SSH server password'), widget=forms.PasswordInput(),
strip=True, help_text=_('Required for password-based authentication.'),
required=False)
label=_('SSH server password'), strip=True,
help_text=_('Password of the SSH Server.<br />'
'SSH key-based authentication is not yet possible.'),
widget=forms.PasswordInput(), required=False)
field_order = ['repository', 'ssh_auth_type', 'ssh_password'
] + encryption_fields
def clean(self):
"""Perform additional checks on form data."""
super().clean()
ssh_password = self.cleaned_data.get('ssh_password')
if self.cleaned_data.get(
'ssh_auth_type') == 'password_auth' and not ssh_password:
raise forms.ValidationError(
_('SSH password is needed for password-based authentication.'))
return self.cleaned_data
field_order = ['repository', 'ssh_password'] + encryption_fields
def clean_repository(self):
"""Validate repository form field."""

View File

@ -49,7 +49,7 @@ KNOWN_ERRORS = [
'raise_as': errors.BorgRepositoryDoesNotExistError,
},
{
'errors': ['[Pp]assphrase supplied in .* is incorrect'],
'errors': ['passphrase supplied in .* is incorrect'],
'message': _('Incorrect encryption passphrase'),
'raise_as': errors.BorgError,
},
@ -131,7 +131,8 @@ def _reraise_known_errors(err):
@reraise_known_errors
@privileged
def mount(mountpoint: str, remote_path: str, ssh_keyfile: str,
def mount(mountpoint: str, remote_path: str, ssh_keyfile: str | None = None,
password: secret_str | None = None,
user_known_hosts_file: str = '/dev/null'):
"""Mount a remote ssh path via sshfs."""
try:
@ -155,9 +156,16 @@ def mount(mountpoint: str, remote_path: str, ssh_keyfile: str,
'sshfs', remote_path, mountpoint, '-o',
f'UserKnownHostsFile={user_known_hosts_file}', '-o',
'StrictHostKeyChecking=yes', '-o', 'reconnect', '-o',
'ServerAliveInterval=15', '-o', 'ServerAliveCountMax=3', '-o',
'IdentityFile=' + ssh_keyfile
'ServerAliveInterval=15', '-o', 'ServerAliveCountMax=3'
]
if ssh_keyfile:
cmd += ['-o', 'IdentityFile=' + ssh_keyfile]
else:
if not password:
raise ValueError('mount requires either a password or ssh_keyfile')
cmd += ['-o', 'password_stdin']
input_ = password.encode()
action_utils.run(cmd, check=True, timeout=TIMEOUT, input=input_)

View File

@ -13,9 +13,7 @@ from django.utils.translation import gettext_lazy as _
from plinth import cfg
from plinth.utils import format_lazy
from . import (_backup_handler, api, copy_ssh_client_public_key, errors,
generate_ssh_client_auth_key, get_known_hosts_path,
get_ssh_client_auth_key_paths, privileged, raise_ssh_error,
from . import (_backup_handler, api, errors, get_known_hosts_path, privileged,
restore_archive_handler, split_path, store)
from .schedule import Schedule
@ -145,9 +143,6 @@ class BaseBorgRepository(abc.ABC):
privileged.delete_archive(archive_path,
self._get_encryption_passpharse())
def migrate_credentials(self) -> None:
"""Migrate any credentials."""
def initialize(self):
"""Initialize / create a borg repository."""
encryption = 'none'
@ -320,27 +315,11 @@ class SshBorgRepository(BaseBorgRepository):
self._umount_ignore_errors()
@property
def hostname(self) -> str:
def hostname(self):
"""Return hostname from the remote path."""
_, hostname, _ = split_path(self._path)
return hostname.split('%')[0] # XXX: Likely incorrect to split
@property
def username(self) -> str:
"""Return username from the remote path."""
username, _, _ = split_path(self._path)
return username
@property
def ssh_password(self) -> str | None:
"""Return SSH password if it is stored, otherwise None."""
return self.credentials.get('ssh_password')
@property
def ssh_keyfile(self) -> str | None:
"""Return path to SSH client key if stored, otherwise None."""
return self.credentials.get('ssh_keyfile')
@property
def _mountpoint(self):
"""Return the local mount point where repository is to be mounted."""
@ -351,22 +330,8 @@ class SshBorgRepository(BaseBorgRepository):
"""Return whether remote path is mounted locally."""
return privileged.is_mounted(self._mountpoint)
def migrate_credentials(self) -> None:
"""Add SSH keyfile credential and delete stored password."""
if not self.ssh_password:
return
pubkey_path, keyfile_path = get_ssh_client_auth_key_paths()
generate_ssh_client_auth_key()
copy_ssh_client_public_key(str(pubkey_path), self.hostname,
self.username, self.ssh_password)
self.credentials['ssh_keyfile'] = str(keyfile_path)
self.credentials.pop('ssh_password', None)
self.save()
def initialize(self):
"""Initialize the repository after mounting the target directory."""
self.migrate_credentials()
self._ensure_remote_directory()
self.mount()
super().initialize()
@ -376,11 +341,17 @@ class SshBorgRepository(BaseBorgRepository):
if self.is_mounted:
return
self.migrate_credentials()
known_hosts_path = get_known_hosts_path()
privileged.mount(self._mountpoint, self._path,
ssh_keyfile=self.credentials['ssh_keyfile'],
user_known_hosts_file=str(known_hosts_path))
kwargs = {'user_known_hosts_file': str(known_hosts_path)}
if 'ssh_password' in self.credentials and self.credentials[
'ssh_password']:
kwargs['password'] = self.credentials['ssh_password']
if 'ssh_keyfile' in self.credentials and self.credentials[
'ssh_keyfile']:
kwargs['ssh_keyfile'] = self.credentials['ssh_keyfile']
privileged.mount(self._mountpoint, self._path, **kwargs)
def umount(self):
"""Unmount the remote path that was mounted locally using sshfs."""
@ -420,16 +391,16 @@ class SshBorgRepository(BaseBorgRepository):
if dir_path[0] == '~':
dir_path = '.' + dir_path[1:]
password = self.credentials['ssh_password']
# Ensure remote directory exists, check contents
env = {'SSHPASS': password}
known_hosts_path = str(get_known_hosts_path())
with raise_ssh_error():
subprocess.run([
'ssh', '-i',
str(self.ssh_keyfile), '-o',
f'UserKnownHostsFile={known_hosts_path}', '-o',
'BatchMode=yes', f'{username}@{hostname}', 'mkdir', '-p',
dir_path
], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
subprocess.run([
'sshpass', '-e', 'ssh', '-o',
f'UserKnownHostsFile={known_hosts_path}', f'{username}@{hostname}',
'mkdir', '-p', dir_path
], check=True, env=env)
def get_repositories():

View File

@ -1,57 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
/**
* @licstart The following is the entire license notice for the JavaScript
* code in this page.
*
* This file is part of FreedomBox.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @licend The above is the entire license notice for the JavaScript code
* in this page.
*/
document.addEventListener('DOMContentLoaded', () => {
const keyAuth = document.getElementById('id_ssh_auth_type_0');
const passwordAuth = document.getElementById('id_ssh_auth_type_1');
const sshPasswordField = document.getElementById('id_ssh_password');
const encryptionType = document.getElementById('id_encryption');
const encryptionPassphraseField = document.getElementById('id_encryption_passphrase');
const encryptionConfirmPassphraseField = document.getElementById('id_confirm_encryption_passphrase');
function handleAuthTypeChange() {
if (passwordAuth.checked) {
sshPasswordField.parentElement.parentElement.style.display = 'block';
} else {
sshPasswordField.parentElement.parentElement.style.display = 'none';
}
}
function handleEncryptionTypeChange() {
let display = 'none';
if (encryptionType.value === "repokey") {
display = 'block';
}
encryptionPassphraseField.parentElement.parentElement.style.display = display;
encryptionConfirmPassphraseField.parentElement.parentElement.style.display = display;
}
keyAuth.addEventListener('change', handleAuthTypeChange);
passwordAuth.addEventListener('change', handleAuthTypeChange);
encryptionType.addEventListener('change', handleEncryptionTypeChange);
handleAuthTypeChange();
handleEncryptionTypeChange();
});

View File

@ -5,125 +5,15 @@
{% load bootstrap %}
{% load i18n %}
{% load static %}
{% block page_js %}
<script type="text/javascript" src="{% static 'backups/backups_add_remote_repository.js' %}"
defer></script>
{% endblock %}
{% block content %}
<h3>{{ title }}</h3>
<form class="form form-add-remote-repository" method="post">
{% if form.non_field_errors %}
<div class="alert alert-danger">
<a class="close" data-dismiss="alert">&times;</a>
{% for non_field_error in form.non_field_errors %}
{{ non_field_error }}
{% endfor %}
</div>
{% endif %}
<form class="form" method="post">
{% csrf_token %}
<div class="form-group{% if form.repository.errors %}
has-error{% endif %}">
<label class="control-label fs-4 fw-medium" for="id_repository">
{{ form.repository.label }}
</label>
<div>
<input type="text" name="repository" class="form-control"
required id="id_repository">
{% for error in form.repository.errors %}
<span class="help-block {{ form.error_css_class }}">{{ error }}</span>
{% endfor %}
<p class="help-block">
{{ form.repository.help_text|safe }}
</p>
</div>
</div>
<div class="form-group{% if form.ssh_auth_type.errors %}
has-error{% endif %}">
<label class="control-label fs-4 fw-medium">
{{ form.ssh_auth_type.label }}
</label>
<p class="help-block">
{{ form.ssh_auth_type.help_text|safe }}
</p>
{% for error in form.ssh_auth_type.errors %}
<span class="help-block {{ form.error_css_class }}">{{ error }}</span>
{% endfor %}
<div class="radio">
<label>
{{ form.ssh_auth_type.0.tag }}
{{ form.ssh_auth_type.0.choice_label }}
</label>
</div>
<div>
<p>
{% blocktrans trimmed %}
The following SSH client public key must be added to the
authorized keys list on the remote machine for {{ box_name }} to
be able to connect to the remote machine:
{% endblocktrans %}
</p>
<pre>{{ ssh_client_public_key }}</pre>
</div>
<div class="radio">
<label>
{{ form.ssh_auth_type.1.tag }}
{{ form.ssh_auth_type.1.choice_label }}
</label>
</div>
<p>
{% blocktrans trimmed %}
{{ box_name }} service will attempt to connect using the provided
password. If successful, then the public key will be automatically
added to the authorized keys list, so that future connections do
not need the password.
{% endblocktrans %}
</p>
{% with _=form.ssh_password|add_input_classes %}
{% include "bootstrapform/field.html" with field=form.ssh_password %}
{% endwith %}
</div>
<div class="form-group">
<div class="form-group{% if form.encryption.errors %}
has-error{% endif %}">
<label class="control-label fs-4 fw-medium" for="id_encryption">
{{ form.encryption.label }}
</label>
<div>
<select name="encryption" class="form-control" id="id_encryption">
<option value="repokey">{{ form.encryption.0.choice_label }}</option>
<option value="none">{{ form.encryption.1.choice_label }}</option>
</select>
{% for error in form.encryption.errors %}
<span class="help-block {{ form.error_css_class }}">{{ error }}</span>
{% endfor %}
<p class="help-block">
{{ form.encryption.help_text|safe }}
</p>
</div>
</div>
{% with _=form.encryption_passphrase|add_input_classes %}
{% include "bootstrapform/field.html" with field=form.encryption_passphrase %}
{% endwith %}
{% with _=form.confirm_encryption_passphrase|add_input_classes %}
{% include "bootstrapform/field.html" with field=form.confirm_encryption_passphrase %}
{% endwith %}
</div>
{{ form|bootstrap }}
<div class="alert alert-warning d-flex align-items-center" role="alert">
<div class="me-2">
@ -134,8 +24,8 @@
{% blocktrans trimmed %}
The credentials for this repository are stored on your {{ box_name }}.
<br />
To restore a backup on a new {{ box_name }} you need the SSH
credentials and, if chosen, the encryption passphrase.
To restore a backup on a new {{ box_name }} you need the SSH credentials
and, if chosen, the encryption passphrase.
{% endblocktrans %}
</div>
</div>

View File

@ -4,8 +4,8 @@
{% load i18n %}
<div class="table-responsive repository" data-repository-name="{{ repository.name }}">
<table class="table" class="archives-list">
<div class="table-responsive">
<table class="table" id="archives-list">
<thead>
<tr>
<th colspan="2">
@ -23,7 +23,7 @@
</span>
{% endif %}
<span class="repository-name">{{ repository.name }}</span>
{{ repository.name }}
</div>
<div class="text-end">

View File

@ -24,7 +24,7 @@
</p>
<p>
<form class="form form-remove-location" method="post">
<form class="form" method="post">
{% csrf_token %}
<input type="submit" class="btn btn-danger"
@ -33,3 +33,4 @@
</p>
{% endblock %}

View File

@ -10,7 +10,7 @@
<h3>{{ title }}</h3>
<form class="form form-verify-ssh-hostkey" method="post">
<form class="form" method="post">
{% csrf_token %}
{% if form.ssh_public_key|length == 0 %}

View File

@ -15,8 +15,6 @@ from plinth.tests import functional
pytestmark = [pytest.mark.system, pytest.mark.backups]
REMOTE_PATH = 'tester@localhost:~/backups'
@pytest.fixture(scope='module', autouse=True)
def fixture_background(session_browser):
@ -68,22 +66,6 @@ def test_set_schedule(session_browser):
monthly=30, run_at=15, without_app='firewall')
def test_remote_backup_location(session_browser):
"""Test remote backup location operations."""
_add_remote_backup_location(session_browser)
assert _has_remote_backup_location(session_browser)
_remove_remote_backup_location(session_browser)
assert not _has_remote_backup_location(session_browser)
# Add it again without providing SSH password.
_add_remote_backup_location(session_browser, False)
assert _has_remote_backup_location(session_browser)
_remove_remote_backup_location(session_browser)
assert not _has_remote_backup_location(session_browser)
def _assert_main_page_is_shown(session_browser):
assert (session_browser.url.endswith('/plinth/'))
@ -190,7 +172,6 @@ def _download_file_logged_in(browser, url, suffix=''):
def _download(browser, archive_name=None):
"""Download a backup archive to a temporary file on disk."""
functional.nav_to_module(browser, 'backups')
href = f'/plinth/sys/backups/root/download/{archive_name}/'
url = functional.base_url + href
@ -199,13 +180,11 @@ def _download(browser, archive_name=None):
def _open_main_page(browser):
"""Open the FreedomBox interface main page."""
with functional.wait_for_page_update(browser):
browser.links.find_by_href('/plinth/').first.click()
def _upload_and_restore(browser, app_name, downloaded_file_path):
"""Upload a backup archive from the disk and perform restore operation."""
functional.nav_to_module(browser, 'backups')
with functional.wait_for_page_update(browser):
browser.links.find_by_href('/plinth/sys/backups/upload/').first.click()
@ -218,52 +197,3 @@ def _upload_and_restore(browser, app_name, downloaded_file_path):
with functional.wait_for_page_update(browser,
expected_url='/plinth/sys/backups/'):
functional.submit(browser, form_class='form-restore')
def _has_remote_backup_location(browser) -> bool:
"""Return whether atleast one remote backup location is configured."""
functional.nav_to_module(browser, 'backups')
return browser.is_element_present_by_css(
f'.repository[data-repository-name="{REMOTE_PATH}"]')
def _add_remote_backup_location(browser, ssh_use_password=True):
"""Add a remote backup location."""
if _has_remote_backup_location(browser):
_remove_remote_backup_location(browser)
browser.links.find_by_href(
'/plinth/sys/backups/repositories/add-remote/').first.click()
browser.find_by_name('repository').fill(REMOTE_PATH)
password = functional.get_password(
functional.config['DEFAULT']['username'])
if ssh_use_password:
browser.find_by_id('id_ssh_auth_type_1').check()
browser.find_by_name('ssh_password').fill(password)
else:
browser.find_by_id('id_ssh_auth_type_0').check()
browser.choose('id_encryption', 'repokey')
browser.find_by_name('encryption_passphrase').fill(password)
browser.find_by_name('confirm_encryption_passphrase').fill(password)
functional.submit(browser, form_class='form-add-remote-repository')
assert browser.is_text_present('Added new remote SSH repository.')
if 'ssh-verify' in browser.url:
_verify_host_key(browser)
def _remove_remote_backup_location(browser):
"""Remove the remote backup location with known remote path."""
repository = browser.find_by_css(
f'.repository[data-repository-name="{REMOTE_PATH}"]').first
repository.find_by_css('.repository-remove').first.click()
functional.submit(browser, form_class='form-remove-location')
def _verify_host_key(browser):
"""Verify the remote location's SSH host key."""
browser.find_by_name('ssh_public_key').first.click()
functional.submit(browser, form_class='form-verify-ssh-hostkey')
assert browser.is_text_present('SSH host verified.')

View File

@ -6,6 +6,7 @@ Views for the backups app.
import contextlib
import logging
import os
import subprocess
from urllib.parse import unquote
from django.contrib import messages
@ -23,9 +24,7 @@ from plinth.errors import PlinthError
from plinth.modules import backups, storage
from plinth.views import AppView
from . import (SESSION_PATH_VARIABLE, api, errors, forms,
generate_ssh_client_auth_key, get_known_hosts_path,
get_ssh_client_auth_key_paths, get_ssh_client_public_key,
from . import (SESSION_PATH_VARIABLE, api, errors, forms, get_known_hosts_path,
is_ssh_hostkey_verified, privileged)
from .decorators import delete_tmp_backup_file
from .repository import (BorgRepository, SshBorgRepository, get_instance,
@ -39,19 +38,6 @@ def handle_common_errors(request: HttpRequest):
"""If any known Borg exceptions occur, show proper error messages."""
try:
yield
except errors.SshError as exception:
if exception.returncode in (6, 7):
message = _('SSH host public key could not be verified.')
elif (exception.returncode == 5
or 'Permission denied' in exception.stderr.decode()):
message = _('Authentication to remote server failed.')
else:
message = _(
'Error establishing connection to server: {} {} {}').format(
str(exception), exception.stdout.decode(),
exception.stderr.decode())
messages.error(request, message)
except errors.BorgError as exception:
messages.error(request, exception.args[0])
@ -358,11 +344,11 @@ class AddRepositoryView(FormView):
encryption_passphrase = None
credentials = {'encryption_passphrase': encryption_passphrase}
repository = BorgRepository(path, credentials)
with handle_common_errors(self.request):
_save_repository(self.request, repository)
messages.success(self.request, _('Added new repository.'))
return super().form_valid(form)
repository = BorgRepository(path, credentials)
if _save_repository(self.request, repository):
messages.success(self.request, _('Added new repository.'))
return super().form_valid(form)
return redirect(reverse_lazy('backups:add-repository'))
@ -372,19 +358,10 @@ class AddRemoteRepositoryView(FormView):
form_class = forms.AddRemoteRepositoryForm
template_name = 'backups_add_remote_repository.html'
def get(self, *args, **kwargs):
"""Handle GET requests.
Generate SSH client authentication key if necessary.
"""
generate_ssh_client_auth_key()
return super().get(*args, kwargs)
def get_context_data(self, **kwargs):
"""Return additional context for rendering the template."""
context = super().get_context_data(**kwargs)
context['title'] = _('Create remote backup repository')
context['ssh_client_public_key'] = get_ssh_client_public_key()
return context
def form_valid(self, form):
@ -397,13 +374,10 @@ class AddRemoteRepositoryView(FormView):
if form.cleaned_data.get('encryption') == 'none':
encryption_passphrase = None
credentials = {'encryption_passphrase': encryption_passphrase}
if form.cleaned_data.get('ssh_auth_type') == 'password_auth':
credentials['ssh_password'] = form.cleaned_data.get('ssh_password')
else:
_pubkey_path, key_path = get_ssh_client_auth_key_paths()
credentials['ssh_keyfile'] = str(key_path)
credentials = {
'ssh_password': form.cleaned_data.get('ssh_password'),
'encryption_passphrase': encryption_passphrase
}
with handle_common_errors(self.request):
repository = SshBorgRepository(path, credentials)
repository.verfied = False
@ -452,28 +426,27 @@ class VerifySshHostkeyView(FormView):
with known_hosts_path.open('a', encoding='utf-8') as known_hosts_file:
known_hosts_file.write(ssh_public_key + '\n')
def _save_repository_and_redirect(self):
"""Save the repository and redirect according to the result."""
with handle_common_errors(self.request):
_save_repository(self.request, self._get_repository())
return redirect(reverse_lazy('backups:index'))
return redirect(reverse_lazy('backups:add-remote-repository'))
def get(self, *args, **kwargs):
"""Skip this view if host is already verified."""
if not is_ssh_hostkey_verified(self._get_repository().hostname):
return super().get(*args, **kwargs)
messages.success(self.request, _('SSH host already verified.'))
return self._save_repository_and_redirect()
if _save_repository(self.request, self._get_repository()):
return redirect(reverse_lazy('backups:index'))
return redirect(reverse_lazy('backups:add-remote-repository'))
def form_valid(self, form):
"""Create and store the repository."""
ssh_public_key = form.cleaned_data['ssh_public_key']
self._add_ssh_hostkey(ssh_public_key)
messages.success(self.request, _('SSH host verified.'))
return self._save_repository_and_redirect()
with handle_common_errors(self.request):
self._add_ssh_hostkey(ssh_public_key)
messages.success(self.request, _('SSH host verified.'))
if _save_repository(self.request, self._get_repository()):
return redirect(reverse_lazy('backups:index'))
return redirect(reverse_lazy('backups:add-remote-repository'))
def _save_repository(request, repository):
@ -482,16 +455,29 @@ def _save_repository(request, repository):
repository.initialize()
repository.verified = True
repository.save()
except Exception:
# Remove the repository so that the user can have another go at
# creating it.
try:
repository.remove()
messages.error(request, _('Repository removed.'))
except KeyError:
pass
return True
except subprocess.CalledProcessError as exception:
if exception.returncode in (6, 7):
message = _('SSH host public key could not be verified.')
elif exception.returncode == 5:
message = _('Authentication to remote server failed.')
else:
message = _('Error establishing connection to server: {}').format(
str(exception))
except Exception as exception:
message = str(exception)
logger.exception('Error adding repository: %s', exception)
raise
messages.error(request, message)
# Remove the repository so that the user can have another go at
# creating it.
try:
repository.remove()
messages.error(request, _('Repository removed.'))
except KeyError:
pass
return False
class RemoveRepositoryView(TemplateView):
@ -536,8 +522,7 @@ def mount_repository(request, uuid):
repository = SshBorgRepository.load(uuid)
try:
with handle_common_errors(request):
repository.mount()
repository.mount()
except Exception as err:
msg = "%s: %s" % (_('Mounting failed'), str(err))
messages.error(request, msg)

View File

@ -102,7 +102,6 @@ class ShowClientView(SuccessMessageMixin, TemplateView):
context['endpoints'] = [
domain + ':' + str(server_info['listen_port'])
for domain in domains
if not domain.endswith('.local')
]
return context

View File

@ -260,6 +260,7 @@ def _shutdown_server() -> None:
This must be run in a thread separate from the server.serve_forever()
otherwise it will deadlock waiting for the shutdown to complete.
"""
global _server
logger.info('SIGTERM received, shutting down the server.')
if _server:
_server.shutdown()