Blog ·
A script that knows it is talking to a container
Seven optional fields where absent means not established, a command table whose test derives its own cases, and an operating system that was in the wrong list.
By Provibr
A Proxmox host can run two very different things: a full virtual machine with its own kernel and firmware, and a container that shares the host's kernel. From a distance they are both "a server" and the panel treats them as one list. Up close they differ in ways that matter to anyone writing automation, and until this round a script could not tell them apart.
That is a bad position. A script that asks a container to resize a virtual disk, attach an ISO or boot from the network is not asking for something difficult; it is asking for something that does not exist. The failure arrives four layers later, as a command that was accepted, queued, dispatched and then refused by an agent.
Seven fields, and absent means not established
Scripts and webhooks now receive a group of facts about what is underneath a server: its kind, the template it came from, whether it is unprivileged, the storage its root sits on, its mount points, what it can actually do, and what it cannot.
Every one of those fields may be absent, and absent means not established. That is the same rule the underlying column follows, where the kind was never silently backfilled, and it exists because the alternative is worse than useless. A snapshot that fills in "virtual machine" when nobody ever measured is a guess handed to a script that will make a decision on it.
So an unknown kind does not become a default. The field disappears from the render context entirely, and reading it is then a hard error at the moment the flow is saved, rather than an empty string that quietly makes a condition false. This is the same reasoning as removing a variable group a trigger cannot supply: loud at save time beats silent at run time.
It is also the first nested group in the variable catalogue. A field name containing a dot is normally a trap in this codebase, because the translation catalogue treats a dot as a level; here the catalogue key deliberately flattens it while the context path walks one level deeper, and a test pins the assumption that there is exactly one dot.
Host paths do not travel
A bind mount in a container's configuration looks like a key, a host path and a mount point. The host path is a directory on the hypervisor, and it must never leave it.
The rule in our own documentation is blunt: customers never get a host shell, a host file system path, or a hypervisor root credential through Provibr. A variable snapshot is exactly such a route. It lands in a script, in a webhook body at a third party, and in an audit row.
So the snapshot never carries the source. What is left is what a customer may know about their own container: which key it is, where it is mounted inside the container, and what kind of source it comes from. One fixed text form, because a script reads this by splitting on a separator rather than by parsing prose: mp1:bind:/srv says there is a bind mount at /srv, and says nothing whatsoever about where it comes from on the host.
That applies to volume mounts too. A storage identifier is not a host path, but it is still the hypervisor's own naming, and no scripting question needs it. The storage name appears once, on its own, as a single word the customer already saw when they ordered.
The price of this design is stated rather than hidden: the mount facts come from the configuration the agent reads during a backup, so a container that has never been backed up produces no mount points at all. The field is then absent, which is correct, because an empty list would claim the container has no mount points.
A table whose test derives its own cases
There was already a written table saying, per virtual machine command, whether a container can do it and why not. What was missing was a table covering every command a Proxmox integration can carry, including backups and the four read commands that describe the integration rather than one guest.
Without that distinction, a sieve cannot tell "no opinion recorded" apart from "this is not about a guest at all", and the only safe behaviour for the second is to let it through. That is precisely the silent branch this project keeps getting burned by.
So the new table is derived from the written one rather than written next to it, and the test does not enumerate cases. It computes the reachable set from the registries, by taking every command kind the Proxmox provider does not refuse that also serves a guest resource, plus the read commands named on the provider descriptor itself, and then demands a statement for each. Twenty seven guest commands and four integration reads, and the test was proven red before it was proven green.
A test per case cannot find a missing case. That is a lesson this codebase has now learned three times, and this is the third shape of the same guard.
One command is absent on purpose, and the comment says where to look for it. Executing a command inside a container would today mean running it as root on the hypervisor, outside the container, with the host's privileges. That is not a scripting feature with a small footprint; it is a host shell with a friendly name. Whoever builds it gets a round of their own, with an executor inside the guest, a permission of its own, and an audit trail of its own.
A refusal before dispatch leaves no row
The gateway already refused impossible commands. That refusal happens to a command that already exists: the row is in the database, the operation was created, and the customer is watching a task fail.
The requirement was validation before dispatch, and the difference is exactly that. Asking a container for its virtual hardware should produce a sentence, not an audit row.
The two sieves cannot drift, because they read the same table. This is not a second truth; it is the same truth, one layer earlier. The gateway is still needed for the repeated or forged request that never passes through the panel function at all.
The refusal reason renders as a single message with a branch per reason, and the guard around it demands two things: a branch for every reason, and that the sentences differ from each other. A missing branch in a language selector falls through to the default and says something that is true and means nothing, which is a failure mode this project has met before.
The operating system that was in the wrong list
The finding that only a real database produced had nothing to do with any of the above.
Operating system entries used to keep their install source in one column and now keep it in two, with the old column read as a fallback for rows no new writer has ever touched. Container templates were added later and are written to their own column, and, on a fresh row, also projected into the old one.
The fallback did not know about that. It read a container reference in the legacy column as a virtual machine template, so an operating system that had only a container template appeared in the virtual machine list of the order flow, without a compute kind, and an order placed against it would have failed later on a clone source with no id to clone from.
The fix is one branch: a container reference in the legacy column is not a virtual machine source. Nothing is lost, because the container side reads its own column and nowhere else, and rows old enough to need the fallback cannot carry a container reference at all: that column did not exist yet.
A second finding came from a screenshot rather than from any assertion. The list of things a container cannot do included a payment command that belongs to a different provider and does not exist on Proxmox for either kind. True, and completely out of place on a container's detail page. It was removed from that list.
What we would write down
- Absent is an answer. If you do not know the kind, do not supply a default. A guess passed to automation is worse than a gap the script can see.
- Permission safety is a property of the shape, not of the caller. Design the snapshot so the host path is not in it, and no future reader can leak it.
- One fixed text form beats prose. Something is going to split this on a separator.
- Derive the cases from the registry. A test per case is structurally incapable of finding the case nobody wrote.
- Refuse before you record. A refusal that produces an audit row has already told the customer the wrong story.
- Look at the screenshots. The out of place entry was true, passed every assertion, and was obviously wrong to a human eye.
For clarity about what this is: containers are orderable on Proxmox and the compute facts are live. Several things are unproven. None of it was exercised against a real hypervisor in this round, the mount facts depend on a backup having happened at least once, one sentence in the order flow still says "the VM" on a container, and the licence counter still reports a container as a virtual machine.
From the platform work on Provibr. Why an unprivileged container is a measurement rather than a setting is in unprivileged is a measurement, not a setting, and which mount points a container backup does and does not contain is in what a backup does not contain.