Web-Oberflaeche als Dienst, Anmeldung mit den Proxmox-Benutzern
install.sh --with-webexplorer --port <nummer> richtet pvesnap-web.service ein. Fehlt der Port, bricht das Skript mit einer Erklaerung ab; --port ohne --with-webexplorer, ein Port ausserhalb 1-65535, etwas anderes als eine Zahl und Port 8006 (Proxmox selbst) werden ebenfalls abgefangen - und zwar vor der root-Pruefung, damit ein Tippfehler sofort auffaellt. --help beschreibt jetzt alle Optionen mit Beispielen. Port, Adresse und Anmeldeart stehen in /etc/pvesnap/web.conf und lassen sich dort aendern, ohne die Unit anzufassen. Angemeldet wird mit den Benutzern von Proxmox VE: dieselbe Maske aus Benutzer, Passwort und Realm-Auswahl, geprueft ueber POST /access/ticket auf der lokalen API - also derselbe Weg wie im Proxmox-Webinterface. Alle Realms funktionieren damit automatisch; Zwei-Faktor-Anmeldungen werden abgewiesen statt halb durchgewinkt. Ein gueltiges Passwort allein reicht nicht: zusaetzlich braucht der Benutzer auf dem Gast das Recht VM.Snapshot (geprueft auf /, /vms, /vms/<id> und dem Pool). Sonst koennte jeder Proxmox-Benutzer saemtliche Dateien aller Gaeste lesen. In der Gastliste erscheinen nur erlaubte Gaeste, root@pam sieht wie in Proxmox alles. Anpassbar ueber --require-privilege und --allow-user. Ausserdem: SIGTERM haengt einen offenen Snapshot wieder aus (ohne das bliebe er bei "systemctl stop" eingebunden), und beim Start werden Reste von Prozessen abgeraeumt, die es nicht mehr gibt - parallel laufende Sitzungen bleiben dabei unangetastet. Auf pvetest01 geprueft: Dienststart ueber systemd, Anmeldemaske mit den Realms des Hosts, Abweisung ohne und mit falschen Zugangsdaten (401, protokolliert mit Absender-IP), Zugriff ohne Recht (403 auf Snapshot- Liste und Oeffnen), vollstaendiger Ablauf als Berechtigter bis zum Datei-Download, Abmelden macht die Sitzung ungueltig, und SIGTERM hinterlaesst weder rbd-Maps noch Mountpunkte. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
91bfdea76c
commit
963463a1a1
+229
-56
@@ -18,6 +18,7 @@ import io
|
||||
import logging
|
||||
import os
|
||||
import secrets
|
||||
import signal
|
||||
import socket
|
||||
import stat
|
||||
import sys
|
||||
@@ -32,12 +33,47 @@ from .. import __version__
|
||||
from ..proxmox import Proxmox, ProxmoxError, pvesh_available
|
||||
from ..snapfs import (Session, SnapfsError, cleanup_leftovers, human_bytes,
|
||||
list_snapshots)
|
||||
from .assets import PAGE, SCRIPT, STYLE
|
||||
from .assets import LOGIN_FORM, PAGE, SCRIPT, STYLE
|
||||
from .auth import DEFAULT_PRIVILEGE, AuthError, authenticate, list_realms, may_access, permissions
|
||||
|
||||
log = logging.getLogger("pvesnap.web")
|
||||
|
||||
COOKIE = "pvesnap_token"
|
||||
COOKIE = "pvesnap_token" # Zugangsschluessel (Modus "token")
|
||||
SESSION_COOKIE = "pvesnap_session" # Anmeldung (Modus "pve")
|
||||
CHUNK = 256 * 1024
|
||||
SESSION_MAX_AGE = 8 * 3600
|
||||
|
||||
|
||||
class Sessions:
|
||||
"""Angemeldete Benutzer. Nur im Arbeitsspeicher - ein Neustart meldet alle ab."""
|
||||
|
||||
def __init__(self, max_age=SESSION_MAX_AGE):
|
||||
self.max_age = max_age
|
||||
self._entries = {}
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def create(self, userid):
|
||||
token = secrets.token_urlsafe(24)
|
||||
with self._lock:
|
||||
self._entries[token] = {"userid": userid, "until": time.time() + self.max_age,
|
||||
"permissions": None}
|
||||
return token
|
||||
|
||||
def get(self, token):
|
||||
if not token:
|
||||
return None
|
||||
with self._lock:
|
||||
entry = self._entries.get(token)
|
||||
if entry is None:
|
||||
return None
|
||||
if entry["until"] < time.time():
|
||||
del self._entries[token]
|
||||
return None
|
||||
return entry
|
||||
|
||||
def drop(self, token):
|
||||
with self._lock:
|
||||
self._entries.pop(token, None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -47,20 +83,40 @@ CHUNK = 256 * 1024
|
||||
class AppState:
|
||||
"""Was gerade geoeffnet ist. Es ist immer hoechstens ein Snapshot offen."""
|
||||
|
||||
def __init__(self, token, proxmox=None, fixed_path=""):
|
||||
def __init__(self, token="", proxmox=None, fixed_path="", auth_mode="token",
|
||||
privilege=DEFAULT_PRIVILEGE, allowed_users=()):
|
||||
self.token = token
|
||||
self.proxmox = proxmox
|
||||
self.fixed_path = fixed_path
|
||||
self.auth_mode = auth_mode # pve | token | none
|
||||
self.privilege = privilege
|
||||
self.allowed_users = tuple(allowed_users or ())
|
||||
self.sessions = Sessions()
|
||||
self.session = None
|
||||
self.guest = None
|
||||
self.snapname = ""
|
||||
self.roots = [] # [(Beschriftung, Pfad)]
|
||||
self.lock = threading.Lock()
|
||||
self.notes = []
|
||||
self.opened_by = ""
|
||||
|
||||
if fixed_path:
|
||||
self.roots = [(fixed_path, os.path.realpath(fixed_path))]
|
||||
|
||||
def visible_guests(self, userid):
|
||||
"""Nur die Gaeste, in die dieser Benutzer hineinsehen darf."""
|
||||
guests = self.proxmox.inventory(refresh=True)
|
||||
if self.auth_mode != "pve":
|
||||
return guests
|
||||
tree = permissions(userid)
|
||||
return [g for g in guests
|
||||
if may_access(userid, g, self.privilege, tree, self.allowed_users)]
|
||||
|
||||
def may_open(self, userid, guest):
|
||||
if self.auth_mode != "pve":
|
||||
return True
|
||||
return may_access(userid, guest, self.privilege, None, self.allowed_users)
|
||||
|
||||
@property
|
||||
def open(self):
|
||||
return bool(self.roots)
|
||||
@@ -197,9 +253,11 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.send_header("Location", target)
|
||||
self.send_header("Content-Length", "0")
|
||||
if cookie:
|
||||
name, value = cookie
|
||||
age = 0 if value == "" else SESSION_MAX_AGE
|
||||
self.send_header("Set-Cookie",
|
||||
"%s=%s; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400"
|
||||
% (COOKIE, cookie))
|
||||
"%s=%s; Path=/; HttpOnly; SameSite=Strict; Max-Age=%d"
|
||||
% (name, value, age))
|
||||
self.end_headers()
|
||||
|
||||
def _error(self, message, status=400):
|
||||
@@ -211,17 +269,69 @@ class Handler(BaseHTTPRequestHandler):
|
||||
|
||||
# -- Zugang -----------------------------------------------------------
|
||||
|
||||
def _cookie(self, name):
|
||||
for part in (self.headers.get("Cookie") or "").split(";"):
|
||||
key, _, value = part.strip().partition("=")
|
||||
if key == name:
|
||||
return value
|
||||
return ""
|
||||
|
||||
def _authorized(self, params):
|
||||
supplied = ""
|
||||
if "token" in params:
|
||||
supplied = params["token"][0]
|
||||
else:
|
||||
cookies = self.headers.get("Cookie") or ""
|
||||
for part in cookies.split(";"):
|
||||
name, _, value = part.strip().partition("=")
|
||||
if name == COOKIE:
|
||||
supplied = value
|
||||
return secrets.compare_digest(supplied, self.state.token), supplied
|
||||
"""Modus 'token': Schluessel aus Adresse oder Cookie."""
|
||||
supplied = params["token"][0] if "token" in params else self._cookie(COOKIE)
|
||||
return secrets.compare_digest(supplied, self.state.token or ""), supplied
|
||||
|
||||
def _user(self):
|
||||
"""Angemeldeter Benutzer im Modus 'pve', sonst None."""
|
||||
entry = self.state.sessions.get(self._cookie(SESSION_COOKIE))
|
||||
return entry["userid"] if entry else None
|
||||
|
||||
def _check_access(self, params, route):
|
||||
"""(erlaubt, Benutzer) - kuemmert sich um alle drei Anmeldearten."""
|
||||
mode = self.state.auth_mode
|
||||
if mode == "none":
|
||||
return True, ""
|
||||
if mode == "token":
|
||||
ok, supplied = self._authorized(params)
|
||||
if ok and "token" in params and route not in ("/download", "/zip"):
|
||||
clean = {k: v[0] for k, v in params.items() if k != "token"}
|
||||
self._redirect(link(route, **clean), cookie=(COOKIE, supplied))
|
||||
return None, "" # None = Antwort ist schon raus
|
||||
return ok, ""
|
||||
user = self._user()
|
||||
return (user is not None), (user or "")
|
||||
|
||||
def _page_login(self, message_text="", username="", realm="pam", status=200):
|
||||
realms = list_realms()
|
||||
options = "".join(
|
||||
"<option value='%s'%s>%s</option>"
|
||||
% (esc(value), " selected" if value == realm else "", esc(label))
|
||||
for value, label in realms)
|
||||
note = ("<div class='note warn'>%s</div>" % esc(message_text)) if message_text else ""
|
||||
body = LOGIN_FORM % {"note": note, "username": esc(username), "realms": options}
|
||||
self._send(page("pvesnap - Anmeldung", self.state, body), status=status)
|
||||
|
||||
def _do_login(self, form):
|
||||
username = (form.get("username") or [""])[0]
|
||||
password = (form.get("password") or [""])[0]
|
||||
realm = (form.get("realm") or ["pam"])[0]
|
||||
try:
|
||||
userid = authenticate(username, realm, password)
|
||||
except AuthError as exc:
|
||||
log.warning("Anmeldung abgelehnt (%s@%s von %s): %s",
|
||||
username, realm, self.address_string(), exc)
|
||||
self._page_login(str(exc), username, realm, status=401)
|
||||
return
|
||||
log.info("Anmeldung: %s von %s", userid, self.address_string())
|
||||
token = self.state.sessions.create(userid)
|
||||
self._redirect("/", cookie=(SESSION_COOKIE, token))
|
||||
|
||||
def _actions(self, user, extra=""):
|
||||
if self.state.auth_mode != "pve" or not user:
|
||||
return extra
|
||||
return ("%s<span class='pill'>%s</span>"
|
||||
"<form method='post' action='/logout' style='margin:0'>"
|
||||
"<button type='submit'>Abmelden</button></form>" % (extra, esc(user)))
|
||||
|
||||
# -- Anfragen ---------------------------------------------------------
|
||||
|
||||
@@ -233,28 +343,31 @@ class Handler(BaseHTTPRequestHandler):
|
||||
params = urllib.parse.parse_qs(parsed.query)
|
||||
route = parsed.path
|
||||
|
||||
ok, supplied = self._authorized(params)
|
||||
if not ok:
|
||||
self._send(page("Zugang", self.state,
|
||||
'<div class="note warn">Zugangsschluessel fehlt oder ist '
|
||||
'falsch.<br>Bitte die Adresse benutzen, die beim Start von '
|
||||
'<code>pvesnap-web</code> ausgegeben wurde.</div>'),
|
||||
status=403)
|
||||
if route == "/login" and self.state.auth_mode == "pve":
|
||||
self._page_login()
|
||||
return
|
||||
|
||||
# Schluessel aus der Adresse in ein Cookie umziehen
|
||||
if "token" in params and route not in ("/download", "/zip"):
|
||||
clean = {k: v[0] for k, v in params.items() if k != "token"}
|
||||
self._redirect(link(route, **clean), cookie=supplied)
|
||||
ok, user = self._check_access(params, route)
|
||||
if ok is None:
|
||||
return # Weiterleitung ist schon raus
|
||||
if not ok:
|
||||
if self.state.auth_mode == "pve":
|
||||
self._page_login("Bitte anmelden.", status=401)
|
||||
else:
|
||||
self._send(page("Zugang", self.state,
|
||||
'<div class="note warn">Zugangsschluessel fehlt oder '
|
||||
'ist falsch.<br>Bitte die Adresse benutzen, die beim '
|
||||
'Start von <code>pvesnap-web</code> ausgegeben '
|
||||
'wurde.</div>'), status=403)
|
||||
return
|
||||
|
||||
try:
|
||||
if route == "/":
|
||||
self._page_home()
|
||||
self._page_home(user)
|
||||
elif route == "/snapshots":
|
||||
self._page_snapshots(params)
|
||||
self._page_snapshots(params, user)
|
||||
elif route == "/files":
|
||||
self._page_files(params)
|
||||
self._page_files(params, user)
|
||||
elif route == "/download":
|
||||
self._download(params)
|
||||
else:
|
||||
@@ -266,21 +379,32 @@ class Handler(BaseHTTPRequestHandler):
|
||||
|
||||
def do_POST(self):
|
||||
parsed = urllib.parse.urlparse(self.path)
|
||||
ok, _ = self._authorized(urllib.parse.parse_qs(parsed.query))
|
||||
if not ok:
|
||||
self._send("Zugang verweigert", status=403, content_type="text/plain")
|
||||
return
|
||||
params = urllib.parse.parse_qs(parsed.query)
|
||||
|
||||
length = int(self.headers.get("Content-Length") or 0)
|
||||
raw = self.rfile.read(length).decode("utf-8", "replace") if length else ""
|
||||
form = urllib.parse.parse_qs(raw)
|
||||
|
||||
if parsed.path == "/login" and self.state.auth_mode == "pve":
|
||||
self._do_login(form)
|
||||
return
|
||||
|
||||
ok, user = self._check_access(params, parsed.path)
|
||||
if ok is None:
|
||||
return
|
||||
if not ok:
|
||||
self._send("Zugang verweigert", status=403, content_type="text/plain")
|
||||
return
|
||||
|
||||
try:
|
||||
if parsed.path == "/open":
|
||||
self._open_snapshot(form)
|
||||
self._open_snapshot(form, user)
|
||||
elif parsed.path == "/close":
|
||||
self.state.close()
|
||||
self._redirect("/")
|
||||
elif parsed.path == "/logout":
|
||||
self.state.sessions.drop(self._cookie(SESSION_COOKIE))
|
||||
self._redirect("/login", cookie=(SESSION_COOKIE, ""))
|
||||
elif parsed.path == "/zip":
|
||||
self._zip(form)
|
||||
else:
|
||||
@@ -292,7 +416,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
|
||||
# -- Seiten -----------------------------------------------------------
|
||||
|
||||
def _page_home(self):
|
||||
def _page_home(self, user=""):
|
||||
state = self.state
|
||||
if state.open:
|
||||
self._redirect(link("/files", fs=0, p=""))
|
||||
@@ -301,7 +425,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self._error("Kein Verzeichnis geoeffnet.")
|
||||
return
|
||||
|
||||
guests = state.proxmox.inventory(refresh=True)
|
||||
guests = state.visible_guests(user)
|
||||
rows = []
|
||||
for guest in guests:
|
||||
rows.append(
|
||||
@@ -314,21 +438,30 @@ class Handler(BaseHTTPRequestHandler):
|
||||
guest.vmid, "LXC" if guest.type == "lxc" else "VM",
|
||||
esc(guest.status), esc(guest.node)))
|
||||
|
||||
hint = ""
|
||||
if state.auth_mode == "pve" and not rows:
|
||||
hint = ("<div class='note warn'>Fuer keinen Gast liegt das noetige Recht "
|
||||
"<b>%s</b> vor. Ein Administrator kann es in Proxmox unter "
|
||||
"Datacenter → Permissions vergeben.</div>" % esc(state.privilege))
|
||||
body = (
|
||||
"<h2>Gast auswaehlen</h2>"
|
||||
hint + "<h2>Gast auswaehlen</h2>"
|
||||
"<div class='card'><table><thead><tr><th>Name</th><th>VMID</th>"
|
||||
"<th>Typ</th><th>Status</th><th>Node</th></tr></thead><tbody>%s</tbody>"
|
||||
"</table></div>" % ("".join(rows) or
|
||||
"<tr><td colspan='5' class='empty'>Keine Gaeste gefunden.</td></tr>"))
|
||||
self._send(page("pvesnap - Gaeste", state, body))
|
||||
self._send(page("pvesnap - Gaeste", state, body, actions=self._actions(user)))
|
||||
|
||||
def _page_snapshots(self, params):
|
||||
def _page_snapshots(self, params, user=""):
|
||||
state = self.state
|
||||
vmid = int((params.get("vmid") or ["0"])[0])
|
||||
guest = next((g for g in state.proxmox.inventory() if g.vmid == vmid), None)
|
||||
if guest is None:
|
||||
self._error("Gast %d nicht gefunden." % vmid, status=404)
|
||||
return
|
||||
if not state.may_open(user, guest):
|
||||
self._error("Fuer %s fehlt das Recht %s auf diesem Gast."
|
||||
% (user, state.privilege), status=403)
|
||||
return
|
||||
|
||||
rows = []
|
||||
for snap in list_snapshots(state.proxmox, guest):
|
||||
@@ -357,9 +490,10 @@ class Handler(BaseHTTPRequestHandler):
|
||||
% (esc(guest.label),
|
||||
"".join(rows) or "<tr><td colspan='4' class='empty'>"
|
||||
"Dieser Gast hat keine Snapshots.</td></tr>"))
|
||||
self._send(page("Snapshots %s" % guest.label, state, body))
|
||||
self._send(page("Snapshots %s" % guest.label, state, body,
|
||||
actions=self._actions(user)))
|
||||
|
||||
def _open_snapshot(self, form):
|
||||
def _open_snapshot(self, form, user=""):
|
||||
state = self.state
|
||||
vmid = int((form.get("vmid") or ["0"])[0])
|
||||
snapname = (form.get("snap") or [""])[0]
|
||||
@@ -367,11 +501,16 @@ class Handler(BaseHTTPRequestHandler):
|
||||
if guest is None or not snapname:
|
||||
self._error("Gast oder Snapshot fehlt.")
|
||||
return
|
||||
log.info("Oeffne Snapshot %s von %s", snapname, guest.label)
|
||||
if not state.may_open(user, guest):
|
||||
self._error("Fuer %s fehlt das Recht %s auf diesem Gast."
|
||||
% (user, state.privilege), status=403)
|
||||
return
|
||||
log.info("%s oeffnet Snapshot %s von %s", user or "-", snapname, guest.label)
|
||||
state.opened_by = user
|
||||
state.open_snapshot(guest, snapname)
|
||||
self._redirect(link("/files", fs=0, p=""))
|
||||
|
||||
def _page_files(self, params):
|
||||
def _page_files(self, params, user=""):
|
||||
state = self.state
|
||||
if not state.open:
|
||||
self._redirect("/")
|
||||
@@ -442,11 +581,11 @@ class Handler(BaseHTTPRequestHandler):
|
||||
"<tbody>%s</tbody></table></div>"
|
||||
% ("".join(rows) or "<tr><td colspan='4' class='empty'>Leeres Verzeichnis</td></tr>"))
|
||||
|
||||
actions = ("<form method='post' action='/close' style='margin:0'>"
|
||||
"<button type='submit'>Snapshot schliessen</button></form>"
|
||||
if not state.fixed_path else "")
|
||||
self._send(page("pvesnap - %s" % (relative or "/"), state,
|
||||
"".join(body), actions=actions))
|
||||
close_button = ("<form method='post' action='/close' style='margin:0'>"
|
||||
"<button type='submit'>Snapshot schliessen</button></form>"
|
||||
if not state.fixed_path else "")
|
||||
self._send(page("pvesnap - %s" % (relative or "/"), state, "".join(body),
|
||||
actions=self._actions(user, close_button)))
|
||||
|
||||
@staticmethod
|
||||
def _describe(item):
|
||||
@@ -688,9 +827,19 @@ def build_parser():
|
||||
help="Port (Vorgabe: %(default)s)")
|
||||
parser.add_argument("-p", "--path",
|
||||
help="beliebiges Verzeichnis freigeben, ohne Proxmox zu fragen")
|
||||
parser.add_argument("-a", "--auth", choices=("pve", "token", "none"),
|
||||
help="Anmeldung: 'pve' = Benutzer von Proxmox VE (Vorgabe auf "
|
||||
"einem PVE-Host), 'token' = Zugangsschluessel in der "
|
||||
"Adresse, 'none' = ohne jede Anmeldung")
|
||||
parser.add_argument("--require-privilege", default=DEFAULT_PRIVILEGE, metavar="RECHT",
|
||||
help="Recht, das ein Benutzer auf einem Gast haben muss, um "
|
||||
"dessen Dateien zu sehen (Vorgabe: %(default)s)")
|
||||
parser.add_argument("--allow-user", action="append", default=[], metavar="USER@REALM",
|
||||
help="dieser Benutzer darf alles sehen (mehrfach moeglich)")
|
||||
parser.add_argument("-t", "--token", help="Zugangsschluessel selbst vorgeben")
|
||||
parser.add_argument("--no-token", action="store_true",
|
||||
help="ohne Zugangsschluessel (nur in vertrauenswuerdigen Netzen!)")
|
||||
help="Kurzform fuer --auth none (nur in vertrauenswuerdigen "
|
||||
"Netzen!)")
|
||||
parser.add_argument("--cleanup", action="store_true",
|
||||
help="haengengebliebene Einbindungen aufraeumen und beenden")
|
||||
parser.add_argument("-v", "--verbose", action="store_true")
|
||||
@@ -725,8 +874,18 @@ def main(argv=None):
|
||||
print("Kein Verzeichnis: %s" % args.path, file=sys.stderr)
|
||||
return 2
|
||||
|
||||
token = "" if args.no_token else (args.token or secrets.token_urlsafe(18))
|
||||
state = AppState(token=token, proxmox=proxmox, fixed_path=args.path or "")
|
||||
# Auf einem Proxmox-Host wird ohne weitere Angabe die Anmeldung mit den
|
||||
# Proxmox-Benutzern verwendet; ohne Proxmox bleibt nur der Zugangsschluessel.
|
||||
mode = args.auth or ("none" if args.no_token else ("token" if args.path else "pve"))
|
||||
if mode == "pve" and not proxmox:
|
||||
print("--auth pve braucht einen Proxmox-Host (mit --path nicht moeglich).",
|
||||
file=sys.stderr)
|
||||
return 2
|
||||
|
||||
token = (args.token or secrets.token_urlsafe(18)) if mode == "token" else ""
|
||||
state = AppState(token=token, proxmox=proxmox, fixed_path=args.path or "",
|
||||
auth_mode=mode, privilege=args.require_privilege,
|
||||
allowed_users=args.allow_user)
|
||||
|
||||
if args.vmid and args.snapshot and proxmox:
|
||||
guest = next((g for g in proxmox.inventory() if g.vmid == args.vmid), None)
|
||||
@@ -740,23 +899,37 @@ def main(argv=None):
|
||||
print("Snapshot nicht zu oeffnen: %s" % exc, file=sys.stderr)
|
||||
return 5
|
||||
|
||||
if args.no_token:
|
||||
Handler._authorized = lambda self, params: (True, "")
|
||||
# Reste eines abgestuerzten Vorgaengers wegraeumen - laufende Sitzungen
|
||||
# anderer Prozesse bleiben unangetastet.
|
||||
cleanup_leftovers(only_dead=True)
|
||||
|
||||
Handler.state = state
|
||||
server = ThreadingHTTPServer((args.bind, args.port), Handler)
|
||||
server.daemon_threads = True
|
||||
|
||||
# Unter systemd kommt SIGTERM; ohne das bliebe der Snapshot eingebunden.
|
||||
def _terminate(_signum, _frame):
|
||||
raise KeyboardInterrupt
|
||||
signal.signal(signal.SIGTERM, _terminate)
|
||||
|
||||
suffix = ("?token=%s" % token) if token else ""
|
||||
lines = ["", "pvesnap-web laeuft. Im Browser oeffnen:", ""]
|
||||
for address in _addresses(args.port):
|
||||
lines.append(" http://%s:%d/%s" % (address, args.port, suffix))
|
||||
if args.bind in ("127.0.0.1", "localhost", "::1"):
|
||||
lines.append(" http://%s:%d/%s" % (args.bind, args.port, suffix))
|
||||
lines += ["", " Beenden mit Strg-C."]
|
||||
if not token:
|
||||
lines.append(" ACHTUNG: ohne Zugangsschluessel gestartet.")
|
||||
lines.append("")
|
||||
if mode == "pve":
|
||||
lines.append(" Anmeldung mit den Benutzern von Proxmox VE.")
|
||||
lines.append(" Sichtbar ist ein Gast nur mit dem Recht '%s' darauf%s."
|
||||
% (state.privilege,
|
||||
(" (immer erlaubt: %s)" % ", ".join(state.allowed_users))
|
||||
if state.allowed_users else ""))
|
||||
elif mode == "token":
|
||||
lines.append(" Anmeldung ueber den Zugangsschluessel in der Adresse.")
|
||||
else:
|
||||
lines.append(" ACHTUNG: ohne jede Anmeldung gestartet.")
|
||||
lines += ["", " Beenden mit Strg-C.", ""]
|
||||
# flush: sonst haengt die Adresse mit dem Schluessel im Puffer, sobald die
|
||||
# Ausgabe nicht direkt im Terminal landet (Pipe, systemd, tee).
|
||||
print("\n".join(lines), flush=True)
|
||||
|
||||
Reference in New Issue
Block a user