A framework for extending Proxmox VE 9.x — adding REST API endpoints and web-UI
components to a live host — packaged so that it survives apt full-upgrade with
nothing to reapply.
proxmod modifies zero Proxmox-owned files. No sed into index.html.tpl, no
awk into PVE/API2/*.pm, no backup-and-restore dance. After installing proxmod
and any number of extensions, dpkg -V pve-manager libpve-common-perl libpve-http-server-perl still reports a clean tree.
Status: in development,
v0.2.1. Not affiliated with or endorsed by Proxmox Server Solutions GmbH. proxmod attaches at unofficial seams; seedocs/pve-facts.mdfor exactly which, and what happens when one moves.
Two mechanisms, both fail-safe:
Backend. A systemd ExecStart drop-in wraps pvedaemon and pveproxy so
they start with -MProxmod. That single injection point loads every registered
extension, each inside its own eval. Perl's taint mode (-T, which both
daemons use) ignores PERL5LIB, so a command-line -M is the only way in.
ExecReload is rewritten to a full restart, because PVE's graceful reload
re-execs the original argv and would drop the flag.
Frontend. Proxmod::Frontend glob-wraps PVE::Service::pveproxy::get_index
at INIT time to inject exactly one <script> tag — pointing at
/proxmod/loader.js, which is generated per request from the live registry.
Extensions add UI by dropping a .js file and a .conf file; they never touch
index.html.tpl, and a frontend-only extension needs no daemon restart at all.
Update survival. Everything proxmod owns lives where Proxmox never writes, so
an upgrade cannot clobber it. A dpkg trigger (the same mechanism pve-manager
itself uses) runs an idempotent convergence script, and pve-manager's own
postinst reload path restarts the daemons through our wrapper for us. A
boot-time oneshot is the safety net.
A missing extension is acceptable. A dead
pvedaemonorpveproxyis not.
Every layer degrades toward "PVE exactly as shipped". A bad shebang, a failed
seam probe, a group-writable module directory, or /etc/proxmod/disabled all
cause the wrapper to exec the daemon unmodified. Because failure is silent by
design, proxmod-verify is not optional — wire it into monitoring.
An extension package needs no maintainer scripts. It Depends: proxmod and
ships three files:
/usr/share/perl5/Acme/Widget.pm # register_method calls
/usr/share/proxmod/extensions.d/50-widget.conf # the manifest
/usr/share/proxmod/www/widget.js # Ext.define overrides
Writing into those paths fires proxmod's dpkg trigger, and everything converges.
See examples/proxmod-example-hello/ for a
complete, buildable worked example.
On a test host first — this loads into pvedaemon and pveproxy and
restarts them.
apt install ./proxmod_*_all.deb
proxmod-verify # exits 0 when healthy
dpkg -V pve-manager libpve-common-perl libpve-http-server-perl # silentThat last line is the claim, and it stays silent after upgrades and after purge.
Off without uninstalling:
proxmodctl disable # touch /etc/proxmod/disabled, restart both daemons stockDetails in docs/install.md.
Start at docs/README.md for reading orders by audience —
extension author, administrator, someone learning Proxmox internals, or someone
changing proxmod itself.
Learning how Proxmox works — useful without proxmod:
docs/pve-internals.md |
processes, request lifecycle, REST tree, auth, pmxcfs, how the interface is served, and a seam inventory marking what is official |
docs/backend-extensions.md |
writing a Perl REST endpoint |
docs/frontend-extensions.md |
writing an ExtJS interface |
docs/packaging.md |
shipping it as a .deb |
docs/patching.md |
the escape hatch, and a post-mortem of doing it the other way |
Building an extension:
getting-started.md ·
extension-manifest.md ·
perl-api.md ·
js-api.md
Running a host with it installed:
install.md ·
verification.md ·
troubleshooting.md ·
cli.md ·
security.md ·
compatibility.md
Why it is built this way:
specifications.md (normative) ·
architecture.md ·
decisions.md (ADRs) ·
conventions.md ·
testing.md ·
glossary.md
docs/pve-facts.md underpins all of it: every claim
about Proxmox internals in this repository cites a [PVE-F-nnn] entry naming
the file and lines it was read from. Nothing is asserted from memory.
make test # unit tests; no Proxmox host needed
make lint # perl -T -c + shellcheck
make deb # build the package into ../
make e2e # full QEMU integration run
make facts ISO=proxmox-ve.iso # re-derive PVE seam evidence, offlinemake test needs no Proxmox and is the loop you write code in. make e2e
boots a real PVE 9.x in QEMU and proves the claims no stub can — that a live
pvedaemon is running our module, that a pve-manager upgrade leaves it
running, and that apt purge gives the host back untouched.
docs/testing.md covers both, including how to build the
VM image the first time.
scripts/extract-pve-source.sh reads PVE source straight out of an installer ISO
— no root, no loop mount, no running Proxmox — so every claim in the docs can be
re-checked against a specific pve-manager version by anyone. Re-run
make facts after each PVE point release and diff docs/facts/ to see which
assumptions moved.
CONTRIBUTING.md for how a change gets here, and
AGENTS.md for a cold-start map of the repository — which document
owns which subject, and the hazards worth knowing before the first edit.
Found something exploitable? SECURITY.md, not the issue
tracker.
AGPL-3.0-or-later, matching Proxmox VE. Full text in LICENSE.