mirror of
https://github.com/Joulenap/joulenap.git
synced 2026-08-11 13:21:43 +02:00
fix: container logging, preview label, and two documentation gaps
Four small ones found alongside the Gate 2 defects. Nothing configured the root logger, so every `log.info` in the package went nowhere: `docker logs` showed uvicorn's handful of lines and nothing else, including across a 0.9 -> 1.0 config migration — the riskiest thing the app ever does, and it left no trace of having run. Both entry points now set logging up first, with `JOULENAP_LOG_LEVEL` to turn it up. The route editor's preview chip printed the internal kind-prefixed key (`pve:pve`) instead of the device id, ever since the draft started carrying keys so a PVE and a backup server could share a name. SECURITY.md said backup-server API traffic is pinned to a stored fingerprint without saying where that fingerprint comes from. Adding a server through a Proxmox host takes it from that host's storage configuration; adding one directly reads it off the box over a connection nothing has authenticated, which is trust on first use. Pinning protects everything after setup, not setup itself, and the document now says so. The changelog now warns upgraders that history is tracked per route, so converted routes read "never run" and every guest reads "never backed up" until the first 1.0 run — with the old runs still listed underneath, which makes it look like data was lost when nothing was.
This commit is contained in:
@@ -14,6 +14,12 @@ Joulenap is no longer built around one Proxmox host backing up to one backup ser
|
||||
configurations are converted automatically on the first start; see Changed below for the one
|
||||
conversion that is lossy, and for the two breaking changes outside the interface.
|
||||
|
||||
**After upgrading, expect one alarming-looking display that is not a problem.** Backup history is
|
||||
tracked per route, and the conversion gives your old schedule new route ids, so *Last backup per
|
||||
guest* reads "never" for every guest and the converted routes read "never run" — with all the old
|
||||
runs still listed underneath. Nothing has been lost: the caches fill in again per guest as runs
|
||||
happen, and the first run of each route restores its badge.
|
||||
|
||||
### Added
|
||||
|
||||
- **Routes.** A route is one scheduled flow of backup data between devices: sources, a target, its
|
||||
@@ -101,6 +107,27 @@ conversion that is lossy, and for the two breaking changes outside the interface
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Stopping a run while a backup server was still coming up left it running.** The magic packet
|
||||
had already gone out, but the lease that decides when a box goes back to sleep was only taken
|
||||
once the box answered — so a run stopped during the wake had nothing to release, recorded no
|
||||
power-off step, and left the machine on until somebody noticed, whatever the stop dialog's
|
||||
power-off toggle said. On a route with more than one server it was worse: once stopped, the
|
||||
reachability check for the next box returned "unreachable" without touching the network, and
|
||||
Joulenap woke a machine nobody had asked for. A run stopped during the wake also no longer
|
||||
starts its cycle, which used to file it as failed — and notify about it — if the first call to
|
||||
a just-booted server errored.
|
||||
- **Provisioning a device replaced an API token of the same name without asking.** A token's
|
||||
secret only exists at creation, so one that already had the name was deleted and recreated —
|
||||
silently invalidating it for everything else using it, typically the backup server's storage
|
||||
entry on a Proxmox host, and therefore every backup through it. Replacing a token is now
|
||||
something you confirm, and the confirmation says what breaks. A create rejected for any other
|
||||
reason no longer takes a live token down with it.
|
||||
- **A backup server registered after its Proxmox host could never receive backups.** Which PVE
|
||||
storage points at which server is discovered, not typed, and only the Add-PVE wizard ever
|
||||
discovered it — so a server added afterwards stayed unlinked, with no way to fix it in the
|
||||
interface. Worse, both the wizard's closing warning and the device editor told you to re-run
|
||||
the Add-PVE wizard, which provisions a token before failing on the duplicate host. Settings →
|
||||
Devices → edit the PVE → **Re-read from Proxmox** now rebuilds the map.
|
||||
- **A sync route worked once and then never again.** Proxmox Backup Server refuses to delete a
|
||||
remote that a sync job still references, so the second run of any sync route failed. The job is
|
||||
now removed before the remote is touched. Two related failures went with it: the run and delete
|
||||
|
||||
@@ -71,6 +71,13 @@ sync run and **deletes it again when the run ends**, so it is not left sitting t
|
||||
device with **no fingerprint is not pinned and its certificate is not validated at all**. Joulenap
|
||||
refuses to send root credentials over such a connection, but ordinary API traffic (carrying the
|
||||
device's API token) still goes over it. Keep the fingerprint set.
|
||||
- **Where that fingerprint comes from decides what pinning is worth.** Adding a backup server
|
||||
through a Proxmox host takes it from that host's storage configuration — a channel you have
|
||||
already authenticated. Adding one directly reads it off the box over a connection nothing has
|
||||
authenticated yet, which is trust on first use: an attacker already in the middle at that moment
|
||||
would have their certificate pinned instead, and every later connection would faithfully verify
|
||||
against it. Pinning protects everything after setup, not setup itself. On an untrusted network,
|
||||
compare the fingerprint the wizard shows against `proxmox-backup-manager cert info` on the box.
|
||||
- **A backup server's SSH host key is verified against `data/known_hosts`.** The wizard's automatic
|
||||
key-install path shows it to you for confirmation first, and refuses to send a root password
|
||||
before you have confirmed it. Two paths do not populate `known_hosts`: installing the public key
|
||||
|
||||
@@ -35,12 +35,34 @@ from .notify.messages import build_interrupted_message
|
||||
|
||||
log = logging.getLogger("joulenap.main")
|
||||
|
||||
#: Container log verbosity. `JOULENAP_LOG_LEVEL=DEBUG` for a noisy run.
|
||||
_LOG_LEVEL_ENV = "JOULENAP_LOG_LEVEL"
|
||||
|
||||
|
||||
def setup_logging() -> None:
|
||||
"""Give the app's own loggers somewhere to go.
|
||||
|
||||
Without this nothing configures the root logger, so every ``log.info`` in the package is
|
||||
dropped and ``docker logs`` shows uvicorn's handful of lines and nothing else — including
|
||||
across a 0.9 -> 1.0 config migration, which is the single riskiest thing this app ever
|
||||
does and left no trace of having happened.
|
||||
|
||||
``basicConfig`` is a no-op once handlers exist, so calling this from both entry points is
|
||||
safe, and uvicorn's own loggers (which configure themselves) are untouched.
|
||||
"""
|
||||
logging.basicConfig(
|
||||
level=os.environ.get(_LOG_LEVEL_ENV, "INFO").upper(),
|
||||
format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
|
||||
)
|
||||
|
||||
|
||||
#: How long shutdown waits for a startup thread. Both only make one notification round-trip, so
|
||||
#: this is generous; it exists so a black-holing channel can't hang the process on exit. Kept
|
||||
#: comfortably under Docker's 10s SIGTERM→SIGKILL grace, so a hung channel costs a slow stop
|
||||
#: rather than a killed one.
|
||||
_STARTUP_THREAD_JOIN_TIMEOUT = 5.0
|
||||
|
||||
|
||||
def _frontend_dir() -> Path:
|
||||
"""Directory of the built SPA (Vite output) served as static files.
|
||||
|
||||
@@ -150,6 +172,7 @@ def _send_startup_alerts(
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
setup_logging()
|
||||
# Load (or first-run create) config before building the app: the session
|
||||
# middleware needs the signing key, and routers read config via app.state.
|
||||
store = ConfigStore.load_or_create()
|
||||
@@ -213,6 +236,8 @@ def run() -> None:
|
||||
"""
|
||||
import uvicorn
|
||||
|
||||
# Before the config load below, which is where a 0.9 -> 1.0 migration runs and logs.
|
||||
setup_logging()
|
||||
port = ConfigStore.load_or_create().config.app.port
|
||||
uvicorn.run("app.main:create_app", factory=True, host="0.0.0.0", port=port, reload=False)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Modal } from '../../components/Modal'
|
||||
import { Toggle } from '../../components/Toggle'
|
||||
import { useRegisterDirty, useUnsavedGuard } from '../../shell/UnsavedGuard'
|
||||
import { guestTypeLabel, type PveGuests } from '../../utils/guestPanel'
|
||||
import { pbsNodeId, pveNodeId, routeKindBadge } from '../../utils/routes'
|
||||
import { deviceId, pbsNodeId, pveNodeId, routeKindBadge } from '../../utils/routes'
|
||||
import {
|
||||
ROUTE_COLORS,
|
||||
type FieldError,
|
||||
@@ -349,7 +349,9 @@ export function RouteModal({ route, routes, pves, pbss, groups, onClose, onSaved
|
||||
{draft.sourceIds.map((id, i) => (
|
||||
<Fragment key={id}>
|
||||
{i > 0 && <span className="arrow">+</span>}
|
||||
<span className="chip-s">{id}</span>
|
||||
{/* The draft holds kind-prefixed keys so a PVE and a PBS can share
|
||||
an id; the preview shows the device, not the key. */}
|
||||
<span className="chip-s">{deviceId(id)}</span>
|
||||
</Fragment>
|
||||
))}
|
||||
<svg width="46" height="10" aria-hidden="true">
|
||||
|
||||
Reference in New Issue
Block a user