Commit Graph

377 Commits

Author SHA1 Message Date
Dilshod Rasulov
3225214e43 Add files via upload 2026-08-18 23:59:21 +05:00
Dilshod Rasulov
e4c921022b Add files via upload 2026-08-18 23:59:05 +05:00
markmorado
73493d7ef2 Add active tasks indicator, propagate LXC create errors, fix Fedora logo
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Failing after 1m24s
Deploy to Production / Trigger production update (push) Failing after 10s
- Surface a topbar indicator with a live count of active tasks
- Propagate exceptions from Proxmox LXC container creation instead of
  swallowing them, and validate minimum password length
- Replace the Fedora catalog icon with the official logo artwork
2026-08-18 23:34:32 +05:00
markmorado
9d57e388cd feat: add 5 new cloud images to built-in catalog
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Failing after 1m28s
Deploy to Production / Trigger production update (push) Failing after 1m37s
Add AlmaLinux 10, CentOS Stream 9, Debian 13 (Trixie), Fedora 41, and
Ubuntu 26.04 LTS (Resolute) with pre-installed qemu-guest-agent. Images
patched via virt-customize and hosted on GitHub release 2026-08-18.
2026-08-18 23:11:13 +05:00
markmorado
5f0a38e03d fix: improve templates page responsiveness for long names and small screens
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
Prevent edit/delete action buttons from overlapping with long template
names by using shrink-0 on icon and button containers with flex-1
min-w-0 on the text area. Make header and filter selects responsive
with flex-wrap and full-width on mobile breakpoints.
2026-08-18 20:28:08 +05:00
8cbba05dac fix: validate instance name as DNS label and improve wizard UX
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Failing after 1m23s
Deploy to Production / Trigger production update (push) Failing after 10s
Add DNS name validation (RFC 952/1123) for VM, LXC and ISO instance
names on both backend and frontend. Proxmox rejects names with
underscores or special characters but the error was only surfaced as
a raw 400 from the API.

Also require Cloud-Init user and password fields for VM deploys
(highlighted when empty, Next button blocked), and fix numeric input
fields that showed "0" on clear and concatenated values like "04096".
2026-08-17 16:24:55 +05:00
0220f338db docs: add CONTRIBUTING and SECURITY links to Russian README
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 11s
v1.16.1
2026-08-17 02:00:57 +05:00
985f04c7ad docs: add CONTRIBUTING and SECURITY links to README
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 9s
2026-08-17 01:59:39 +05:00
8b3cde79cf docs: replace template SECURITY.md with project-specific policy
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
2026-08-17 01:56:57 +05:00
72d933228c docs: add contributing guide for new contributors
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 9s
2026-08-17 01:52:18 +05:00
5999183c8e fix: activate docker group in current session via sg re-exec
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
After install_docker adds the user to the docker group, the change only
takes effect in a new login session.  The script continued running in
the old session, docker info failed, and deploy exited with "Cannot
connect to Docker daemon" even though the daemon was running fine.

Now check_requirements detects this situation (sudo docker works but
docker doesn't, and the user IS in the docker group per /etc/group)
and re-execs the entire script under `sg docker` to activate the
supplementary GID without a logout/login cycle.  A _DEPLOY_REEXEC
guard prevents infinite loops.
2026-08-17 01:42:05 +05:00
286576715b docs: release notes and version bump for v1.16.1
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 9s
2026-08-17 01:26:05 +05:00
820da8ca01 fix: set 755 permissions on pve CLI so non-root users can read the script
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
sudo cp preserves the source umask which can leave the file without
read permission for group/others.  A bash script needs both read and
execute bits — without read, the shell cannot interpret it.
2026-08-17 01:20:43 +05:00
a275b8bed5 fix: prevent SSL rollback by using container state instead of status text
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
verify_nginx_running grepped for "running|healthy" in the human-readable
STATUS column of docker compose ps, but the column shows "Up Xs (health:
starting)" for the first 30s before the healthcheck passes — neither
word matches.  With max_wait=20s the function always timed out and the
deploy script rolled back to the HTTP-only config even though the SSL
certificate was successfully obtained.

Switch to --format '{{.State}}' which returns the raw "running" state
immediately, and bump max_wait to 60s for slow first starts.
2026-08-17 01:13:43 +05:00
fbfbd76960 fix: install pve CLI without root via sudo fallback to ~/.local/bin
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 9s
Previously install_pve_cli silently skipped installation when deploy.sh
was run without root, leaving the 'pve' command unavailable.  Now it
tries sudo first (password is usually cached from docker operations)
and falls back to ~/.local/bin with automatic PATH configuration.
2026-08-17 00:34:58 +05:00
8fa1208cde feat(frontend): switch to Inter Variable, bump type scale, fix TS errors
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
- Replace DM Sans with Inter Variable (better screen rendering at small sizes)
- Bump text-xs 12→13px, text-sm 14→15px globally via theme tokens
- Fix hardcoded text-[13px] → text-sm in AppLayout, CommandPalette, DashboardPage
- Fix button sm variant text-[0.8rem] → text-xs for consistency
- Fix TS: ScriptExecuteDialog unused isRunning, setResult → setExecutionId(r.id)
- Fix TS: InstanceActionDialogs onValueChange null guards (string | null → string)
- Dockerfile: npm ci → npm install (cross-platform lockfile, Linux optional deps)
2026-08-17 00:14:06 +05:00
ec076959db docs: release notes and version bump for v1.16.0
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
v1.16.0
2026-08-14 20:06:12 +05:00
22162408ac fix: correct password visibility icon logic across all dialogs
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
2026-08-14 19:52:59 +05:00
319de5c96e feat: swap SSH keys when admin changes instance owner
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Failing after 1m21s
Deploy to Production / Trigger production update (push) Failing after 10s
Remove the previous owner's SSH keys and apply the new owner's keys
(profile + library) when ownership is reassigned. If the new owner has
no keys, the old keys are simply removed.

QEMU VMs: updates cloud-init sshkeys via Proxmox API.
LXC containers: writes /root/.ssh/authorized_keys via exec.
The swap is best-effort — failures are logged but do not block the
ownership change.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-08-14 19:07:59 +05:00
f4878eac50 scripts: add patch-cloud-images.sh for building GitHub mirror with qemu-guest-agent
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 11s
2026-08-14 17:02:25 +05:00
4d00fdf959 catalog: point amd64 images to GitHub mirror with qemu-guest-agent and sha256
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Failing after 1m21s
Deploy to Production / Trigger production update (push) Failing after 10s
2026-08-14 16:46:28 +05:00
f29226a735 fix: filter out loop devices and system partitions from VM storage overview
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Failing after 1m20s
Deploy to Production / Trigger production update (push) Failing after 10s
Only show user-relevant filesystems by skipping snap/loop mounts
and /boot/* partitions in QEMU guest agent fsinfo response.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-08-14 16:40:01 +05:00
8ff757f8c1 fix: re-download cached import images when checksum is provided
Some checks failed
Deploy to Production / Trigger production update (push) Has been cancelled
Backend Tests / pytest (Python 3.12) (push) Has been cancelled
volume_exists() only checked file presence, not content. A stale image
from a previous download (different URL/version) would be silently reused,
causing templates and VMs to be created from unpatched images even when
the mirror's checksum pointed to a newer patched build.

Add delete_volume() to ProxmoxClient storage mixin, and update both
_do_image_download_sync and _do_image_template_sync to delete the cached
volume and re-download whenever a checksum is provided, letting Proxmox
verify the new download server-side.
2026-08-14 16:19:41 +05:00
3540291c0e i18n: translate hardcoded Russian strings in graphs, wizard, storages, scripts
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 12s
- GraphsTab: wrap "Все", "Сбросить", "Нет данных", IOPS and disk-pct chart
  titles with t(); add graphs.iops_read/write/disk_usage_pct keys
- CreateInstanceWizard: translate (безопаснее) spans via common.safer;
  translate IP placeholder via wizard.ip_cidr_placeholder
- StoragesTab: translate Ceph monhost placeholder via storages.monhost_placeholder
- ScriptEditDialog: translate name placeholder via scripts.name_placeholder
- Add scripts.execution_title and scripts.no_output keys (EN + RU)
- Add common.safer key (EN + RU)
2026-08-14 14:06:10 +05:00
5af2ffed01 i18n: add missing translation keys for appstore, nodeadm, common, and scripts
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 9s
- appstore.golden.pick_server_hint (EN + RU)
- nodeadm: certs, syslog, tasks, system, tasks_hint, task_start, task_type,
  task_id, task_user, task_running, syslog_hint, tz_saved, dns_saved,
  time, node_time, timezone
- common.changePassword, common.runCommand, common.queued, common.no_data
- Remove duplicate common.no_data key from en.json
2026-08-13 17:26:43 +05:00
d6ea83c8aa i18n(dashboard,instances): translate dashboard, action dialogs and resource cards
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
2026-08-13 17:19:26 +05:00
43db0f79ca i18n(images): translate Images page, dialogs and mirrors manager
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
2026-08-13 16:59:01 +05:00
477faef44a i18n: add scripts and certs sections (ru + en)
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
scripts: full section — catalog, repos, executions tabs, edit/execute
         dialogs, community scripts warning, all labels and toasts
certs: full section — upload form, certificate details, expiry badges
2026-08-13 16:43:04 +05:00
5a1fb72660 i18n: add bulk_migrate and access realm keys (ru + en)
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
instances: bulk_migrate_title/desc, target_node, select_node,
           online_migration, status_migrating, nothing_to_migrate,
           migrate_same_server
access: user_attr, username_claim translated to Russian
2026-08-13 16:24:52 +05:00
008e29b70f i18n: add missing nodes power and common action keys (ru + en)
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 9s
nodes: reboot, shutdown, shell, confirm_reboot, confirm_shutdown,
       reboot_started, shutdown_started
common: comment, add, clone, iso, notes, reinstall, done, copied, message
2026-08-13 16:21:30 +05:00
80d7a98256 i18n: fix critical untranslated strings across the UI
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
- DestroyTab: wrap all hardcoded English strings with t()
- instances: add status_reinstalling, status_changing_password keys
- common: add shutdown key (ru + en)
- fw: translate groups, ipsets, aliases, security_group and related actions
- nodeadm: translate upgrade button label
- en.json: fix Russian placeholder_notes in English locale
2026-08-13 16:17:07 +05:00
d88647de77 i18n(roles): add missing permission label translations to Russian
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 18s
node_power, app_view/install/manage, script_view/execute/manage
2026-08-13 15:57:46 +05:00
82a3ae72be i18n(roles): translate remote_migrate permission labels to Russian 2026-08-13 15:56:00 +05:00
a7fda196b5 docs: release notes and version bump for v1.15.0
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
2026-08-13 15:41:27 +05:00
8337539e9e i18n(users): translate the terminated-sessions toast
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Failing after 2m12s
Deploy to Production / Trigger production update (push) Failing after 11s
Both toasts printed the endpoint's own message field verbatim, so a Russian UI
answered "Terminated 0 sessions". That string is assembled in Python and can
never be translated where it is built, so the endpoints now also return the raw
count and the phrase is composed on the client.

message is kept: it goes to the audit log and may have other consumers, and
adding a field breaks no one.

Russian needs three forms, and zero does not land in the obvious one — 0 is the
"many" category ("сессий", not "сессии") — hence four keys against English's
two.

Fixes both call sites: the per-user button in the table row and Terminate all
sessions on the Sessions tab, which shared the same untranslated string.
2026-08-12 15:00:07 +05:00
9e4120bd8d feat(instances): disable Create Instance when the quota is used up
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
The wizard already refuses to open once a limit has no headroom left, but the
button leading to it still looked available, so the only way to find out was to
click it. Reuse exhaustedMetrics() from the wizard's quota module, so the button
and the wizard cannot disagree about what counts as exhausted, and skip the
check for admins for the same reason the wizard does — their own quota says
nothing about an instance they create for someone else.

The button is rendered as a Link, and disabled does nothing to an <a>: it would
still navigate. So the exhausted case renders a plain button with no Link, and a
tooltip naming the limits that are full. The trigger wraps it in a span because
a disabled button receives no pointer events of its own.

The wizard keeps its own block, which is what still covers navigating straight
to /instances/create.
2026-08-12 14:44:42 +05:00
91fa1b9971 fix(nginx): raise limit_conn so HTTP/2 stream count stops throttling the app
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
Rebuilding the frontend surfaced the limiter sitting behind the request-rate
one fixed in 935c804: with limit_req no longer rejecting the bundle, the
cold load started failing on `limiting connections by zone "addr"` instead,
and the page went blank again.

nginx counts each concurrent HTTP/2 stream against limit_conn, and a browser
multiplexes the entire page over a single connection, so `limit_conn addr 10`
capped the whole page at ten in-flight requests. That is far below the 89
files the build emits, and it would equally have throttled the parallel API
queries the dashboard issues after login — the login screen just happened to
be where it showed.

Raise the server-level cap to 100 and give the static-asset location 200.
Verified against the live deployment: 89/89 assets return 200, the login
page renders, and nginx has logged no limiting events since the reload.
2026-08-11 21:24:17 +05:00
925b5c8664 chore: mark the shell entry points executable
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
deploy.sh, bootstrap.sh, update_host.sh, setup_ssh.sh and pve all carry a
#!/bin/bash shebang and are invoked directly, but git recorded every one of
them as 100644. A fresh clone therefore could not run ./deploy.sh, and any
host where the bit had been fixed by hand showed a permanently dirty
working tree — the mode was the only difference.

Record the bit so a clone is runnable and `git status` stays clean.
2026-08-11 21:14:16 +05:00
c981a14cf9 chore(nginx): stop tracking the generated site config
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 11s
nginx/conf.d/pvemanager.conf is not source: setup_nginx_config() writes it
on every deploy by copying one of the .template files and substituting the
domain. Tracking it meant the repository carried one deployment's hostname,
and every deploy showed the file as modified.

Untrack it and ignore it. The templates it is generated from stay tracked,
so nothing is lost — a missing copy is restored by re-running deploy.sh, or
by hand:

    cp nginx/conf.d/pvemanager.conf.template nginx/conf.d/pvemanager.conf
    sed -i "s/DOMAIN_NAME/<domain>/g" nginx/conf.d/pvemanager.conf

Note for existing checkouts: this commit deletes the path, so a deployed
host that pulls it will lose its live config unless the file is regenerated
afterwards.
2026-08-11 21:10:09 +05:00
0662dda6ae fix(compose): actually drop the published ports in production
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 11s
compose.prod.yml has always intended to take the app and frontend off the
host interface and let nginx front them — "remove port exposure (nginx will
handle it)". It did not work: Compose concatenates list-valued fields when
merging -f files, so `ports: []` contributed nothing and 8000:8000 and
3001:80 survived from compose.yml.

The effect was that an SSL deployment still answered on both ports over
plain HTTP, bypassing TLS, HSTS, the security headers and the rate limits —
confirmed against the live host from off-box, where the API returned its
health payload unencrypted on :8000 and the panel loaded on :3001.

Use the !reset tag so the inherited value is dropped rather than extended.
Verified in place: the merged config now publishes only 80 and 443, both
former ports refuse connections from outside, and the panel still serves
over HTTPS.
2026-08-11 21:05:42 +05:00
935c80427f fix(nginx): stop rate-limiting the frontend bundle into a blank page
Loading the panel over the domain produced a white page while the same
panel served directly from the frontend container rendered fine. The built
SPA requests 86 files on a cold load — one entry module plus 85 preloaded
chunks — and the server-level

    limit_req zone=general burst=20 nodelay;   # general = 10r/s

applies to those static assets too. Measured against the live site, 60 of
the 86 came back 503 and nginx logged "limiting requests ... by zone
general" for each. The entry module was among the casualties, so React
never booted and the document stayed empty.

Give static assets their own zone and restate the limit in a location that
matches them. The limit has to be restated rather than omitted: nginx
inherits limit_req from the enclosing level only when a location declares
none, and there is no `limit_req off`.

Both templates get the block, since the non-SSL config serves the same
bundle. Verified on the live deployment: 86/86 assets now return 200 and
the login page renders with no console errors.
2026-08-11 21:05:30 +05:00
74b7a9fe73 fix(nginx): raise the container file descriptor limit to match worker_connections
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
nginx.conf configures 2048 worker_connections, but Docker's default
RLIMIT_NOFILE of 1024 capped what the workers could actually open — and
since a proxied connection needs a descriptor both downstream and upstream,
the real ceiling was about a quarter of the configured one. nginx warned
about exactly this on every config test.

Raise nofile to 8192 for the nginx container. Verified in place: the limit
goes from 1024 to 8192 and `nginx -t` is now free of warnings.
2026-08-11 20:47:49 +05:00
5f20b87824 fix(nginx): drop two directives that only produced reload warnings
`listen 443 ssl http2` has been deprecated since nginx 1.25.1 in favour of
a separate `http2 on;` directive, and Let's Encrypt no longer publishes an
OCSP responder URL in its certificates, so `ssl_stapling` had nothing to
staple. Both warned on every reload — noise that now matters more, since
nginx reloads itself every 6h to pick up renewed certificates.

Reloads are clean afterwards and HTTP/2 is still negotiated.
2026-08-11 20:38:03 +05:00
c6a32255df fix(ssl): stop reporting a working certificate as a failed deployment
The SSL deployment aborted even when Let's Encrypt had issued a perfectly
valid certificate. obtain_ssl_certificate() confirmed success with

    [ -d "nginx/certbot/conf/live/${domain}" ]

but certbot runs as root inside its container and creates live/, archive/
and accounts/ with mode 0700. Whenever deploy.sh runs as a non-root user
that test fails with EACCES no matter what is on disk, so every SSL deploy
fell back to the HTTP-only config and printed "Failed to obtain SSL
certificate" right after certbot reported the certificate was fine.

Check the certificate from inside a container instead, and treat the files
on disk as the source of truth rather than certbot's exit code, so a
certificate that is present and not yet due for renewal counts as success.
Pass --keep-until-expiring to make that case explicit.

Also drop the recursive chmod on nginx/certbot: it could not touch the
root-owned files it was aimed at (producing the wall of "Operation not
permitted" noise) and would have exposed the private keys if it had.

Verify nginx actually came up after switching to the SSL config and roll
back to HTTP if it did not — a crash-looping nginx takes the whole panel
offline, which is worse than serving without HTTPS.

Reload nginx every 6h so renewed certificates are picked up: nginx reads
the certificate once at startup, so the renewal loop would otherwise keep
serving the old one until the container happened to restart.

Ignore nginx/certbot/ — it holds Let's Encrypt private keys and was
untracked but not ignored.
2026-08-11 20:28:58 +05:00
bddbf15652 feat(rbac): split setting:view into panel settings and a permission-free profile
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Failing after 1m22s
Deploy to Production / Trigger production update (push) Failing after 11s
setting:view used to gate the whole /settings page, which mixed panel-wide
configuration with a user's own account. Revoking it to keep a standard user
out of the panel settings also took away their profile, password, 2FA and SSH
keys, so the permission could not be used for what it names.

The account half now lives at /profile: its endpoints are self-scoped and only
require a session, so the page carries no permission at all and is reached from
the user menu (and Ctrl+K) rather than a sidebar group. /settings keeps the
panel-wide tabs behind setting:view, with saving gated on setting:update,
security on setting:manage and the SMTP/Telegram channels on is_admin, matching
what each endpoint checks.

The shipped "user" role no longer gets setting:view. Migration 42 drops it from
roles holding it without setting:update/manage, since those grants only existed
to reach the profile page; it runs once (marker in panel_settings) so a later
deliberate grant is not reverted on the next start.
2026-08-11 20:07:34 +05:00
991daddd63 fix(rbac): guard routes by permission and hide admin-only tabs
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
Hiding a sidebar entry was the only frontend gate: /users, /logs and
/settings opened for anyone who typed the URL, and their pages rendered
every tab regardless of the caller (the backend then answered the tab's
requests with 403s).

Route elements are now wrapped in RequirePermission, gated on the same
permission the page API requires, so a hidden menu item can no longer be
reached by URL. Denied users land on the first page they can open, or on
an explanatory screen when nothing is reachable, so the redirect cannot
loop. Permission checking moves into lib/permissions.ts and canSeeNavItem
delegates to it, keeping menu and guard in sync.

Settings hides the panel tab without setting:update, the security tab
without setting:manage, the SMTP/Telegram cards from non-admins, and the
update controls behind setting:manage; profile, SSH keys and personal
notification preferences stay available to everyone. Users hides the
roles tab without role:view and the security tab without setting:manage.
2026-08-11 18:44:20 +05:00
c40611bedb feat(instances): tell users which quota is in the way before the wizard
Some checks failed
Deploy to Production / Trigger production update (push) Failing after 10s
Quotas were only enforced by check_quota() on the backend, which answers 429
from the deploy endpoint — so a user walked all five wizard steps and learned
about the limit on the very last click. QuotaHint coloured the offending number
red but blocked nothing.

Split the two situations the UI never distinguished:

  * No headroom at all (used >= limit): no configuration would ever fit, so the
    wizard does not open. It is replaced by a message naming exactly which
    limits are used up, with their numbers.
  * Headroom left but the chosen configuration does not fit (used + requested >
    limit): the wizard opens, and from the config step onwards it names the
    metric and disables Next/Deploy until the resources are reduced.

The overrun check deliberately stays quiet until the config step. Gating every
step meant a default size that did not fit the remaining quota stranded the user
on the server picker, with no way to reach the fields and shrink it.

Neither block applies to admins: their own quota says nothing about an instance
they are creating on behalf of someone else, and blocking on it would lock an
admin with a quota out of the wizard entirely. The figures are still shown.

Also label the quota card as a projection. It renders used + the instance being
created, so a user with 3 of 4 instances saw "4 / 4" and reasonably read it as
already exhausted — which is what prompted this.

The arithmetic lives in features/instances/quota.ts as pure functions rather
than inline in JSX, so the two thresholds can be reasoned about on their own.
2026-08-11 18:21:51 +05:00
c4fda1b0bb feat(migrations): backfill the owner of instances created before the fix
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Failing after 1m20s
Deploy to Production / Trigger production update (push) Failing after 10s
Instances deployed while save_vm_instance() dropped the owner are still sitting
in the database with owner_id = NULL, so their users cannot see them. Recover
the creator from the two places it was recorded:

  1. deploy_tasks — user_id is a real foreign key stored when the deploy was
     queued. server_id is absent for LXC deploys, so a NULL there is treated as
     matching any server instead of discarding the row.
  2. audit_logs — vm_create / ct_create / lxc_create entries, which only carry a
     username and are matched by name.

Ownership decides who may see, stop and delete an instance, so a wrong guess is
worse than no owner at all: a row is updated only when every applicable piece of
evidence names the same existing user. VMIDs get reused, so conflicting evidence
is expected and those instances deliberately stay ownerless for an operator to
assign with assign_instances_owner.py. Existing owners, soft-deleted rows,
failed create events and logs from deleted users are all left alone, which also
makes the migration idempotent.

Note this recovers who created an instance, not who it was meant for: a VM an
admin created on behalf of a user is attributed to the admin, because the
intended owner was never persisted before the fix.

The attribution rule is a pure function (pick_backfill_owner) so it can be
tested directly. The migration's own statements are portable, so the tests run
it against SQLite with the two information_schema helpers stubbed out.
2026-08-11 17:28:59 +05:00
1152410860 fix(instances): assign the owner on create and inject the owner's SSH keys
Some checks failed
Backend Tests / pytest (Python 3.12) (push) Failing after 2m30s
Deploy to Production / Trigger production update (push) Failing after 10s
Newly created VMs/LXCs ended up with owner_id = NULL, and a VM created by an
admin on behalf of a user received the admin's SSH key instead of the user's.

Ownership: save_vm_instance() only set owner_id when inserting a new row. The
row practically always existed by then — sync_vm_cache runs every 10s and
registers VMs it sees on Proxmox with owner_id = NULL while the clone is still
running, whereas the deploy saves the owner minutes later at 95% progress. So
every deploy landed in the update branch and silently dropped the owner. Set
owner_id there too, but only when it is passed explicitly: callers such as
save-before-delete do not know the owner and must not blank it out.

SSH keys: the profile key was taken from whoever pressed "Create" rather than
from the instance owner, and the LXC path injected no profile key at all. Add
build_deploy_ssh_keys(), which merges pasted keys, keys picked from the library
(same authorization rules as before) and the owner's profile key, deduplicated.
Both the template and LXC deploy endpoints now use it, so the key material is
resolved in one place instead of inside the deploy worker.

ISO VMs are left alone on purpose: they are blank VMs without cloud-init, so
there is nowhere to inject keys.

Covered by backend/tests/test_instance_ownership.py, including the sync race.
2026-08-11 17:19:19 +05:00
markmorado
e2d776bb6c fix(bootstrap): register the host at the correct /proxmox/api/servers path
Some checks failed
Deploy to Production / Trigger production update (push) Has been cancelled
The proxmox router is mounted under the /proxmox prefix in main.py, so the
real create-server endpoint is /proxmox/api/servers, not /api/servers as
the registration step assumed. The bare /api root only has /api/auth/*.

Verified end to end against a real Proxmox host: LXC provisioning, Docker
install inside it, deploy.sh --standalone build+run, admin login, local
root@pam token creation, and registration all completed successfully,
with the panel reporting the new node online.
2026-07-25 01:01:38 +05:00