Skip to content

Sell IP counts as a template/custom-pricing dimension - #339

Merged
v0l merged 4 commits into
masterfrom
feat/template-ip-resources-105
Jul 30, 2026
Merged

Sell IP counts as a template/custom-pricing dimension#339
v0l merged 4 commits into
masterfrom
feat/template-ip-resources-105

Conversation

@v0l

@v0l v0l commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Closes #105.

Offers now state how many addresses they include, instead of every offer implying exactly one IPv4 plus a best-effort IPv6.

Schema (lnvps_db/migrations/20260730130000_template_ip_counts.sql) — vm_template.ip4_count/ip6_count, vm_custom_template.ip4_count/ip6_count, vm_custom_pricing.min_ip4/max_ip4/min_ip6/max_ip6, all defaulting to 1 so nothing starts offering a choice until an operator widens a plan. Existing custom VMs are backfilled from the addresses they actually hold, and each plan's range is widened to cover its own existing VMs, so no live renewal amount moves and no grandfathered VM fails spec validation on upgrade.

Billing — custom VM cost now multiplies the ordered counts by ip4_cost/ip6_cost (lnvps_api_common/src/pricing.rs:757-806) instead of counting current assignments with a .max(1) floor. That was unquotable before a VM existed and would have moved the price whenever provisioning held fewer addresses than were sold. Counts are range-checked at order/upgrade time (:855-872), and an upgrade carries the sold counts over untouched (:1330-1345, lnvps_api/src/provisioner/vm.rs:938-952 — the second builder, which is what the upgrade actually persists).

Capacitycan_accommodate requires ip4_count free addresses region-wide rather than >= 1 (lnvps_api_common/src/capacity.rs:438-465), and custom plan params get max_ip4 clamped to what the region can supply, with plans whose min_ip4 cannot be met dropped from the listing (:200-240).

Provisioningpick_ips_for_region allocates N addresses across the region's ranges, excluding what it has already picked in the same pass (nothing is persisted until the whole assignment succeeds), and assign_ips loops instead of taking one of each (lnvps_api_common/src/network.rs:69-140, lnvps_api/src/provisioner/vm.rs:1056-1105).

Two deliberate calls, both worth arguing with:

  • IPv4 is strict, IPv6 stays best-effort. An order that asks for IPv4 it cannot get fails, as today. Making IPv6 strict would stop provisioning in any region without an IPv6 range, which I cannot verify from here; a SLAAC range also only yields one address per VM (one MAC). So ip6_count is billed but not guaranteed — same as the old .max(1) behaviour, now explicit in the docs and the API comment.
  • Standard templates get a count, not a price. ip4_count on VmTemplate drives capacity and provisioning; a standard plan's price is still its flat cost plan, so an operator selling extra addresses prices them into the plan.

Tests: unit for the picker (distinct addresses, refusal when short, IPv6-absent region), capacity gating, price scaling and range validation, request defaults; assign_ips covered directly against the mock host (lnvps_api/src/provisioner/vm.rs:2405-2530). e2e orders a 2x IPv4 custom VM, asserts the counts land on the VM and the billed amount includes both addresses, and that an out-of-range count is refused (lnvps_e2e/src/lifecycle.rs:1648-1705). cargo test --workspace green at ceae93e (excluding lnvps_e2e, which needs the live stack); full e2e 165 passed in 15.9s; clippy clean on every line touched.

Docs: API_CHANGELOG.md and ADMIN_API_ENDPOINTS.md. Additive — no existing field changed shape.

Follow-ups, not in scope here: CustomVmSpec (the admin custom-create spec added in #337) needs the same two fields once that lands, which folds into #338; per-order/per-account caps beyond region capacity are still unsettled in the issue; web#92 and admin#14 can now build against min_ip4/max_ip4 and ip4_count.

Originating channel: general (721a8a6c-c36c-5fa1-ae07-822ddc8567c5).


// The router-generated vMAC is per-VM, not per-address, so it is only
// generated for the first assignment; later ones reuse the VM's MAC.
self.assign_mac(&mut assignment).await?;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an assumption, in some cases each IP needs its own vMAC, meaning we would have to attach multiple nics to be able to use additional ips, please dig into this more before making assumptions

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, and it is worse than the vMAC question — the guest side cannot carry a second address at all today, so I am not shipping the provisioning half as it stands.

What I found:

  • lnvps_api/src/host/proxmox.rs:979-1049 builds exactly one NIC (net0) and joins every assignment into one ipconfig0. With ip4_count = 2 make_config emits ipconfig0=ip=185.18.221.65/24,gw=…,ip6=auto,ip=185.18.221.66/24,gw=… — three ip= keys in one property string. Proxmox ipconfig[n] takes at most one ip= and one ip6=, one index per net device, so the extra address is dropped (and the reconcile compare at :1768 would then churn against it forever). Verified by running make_config with two v4 assignments, not by reading alone.
  • lnvps_api/src/provisioner/vm.rs:1026-1047: assign_mac generates the vMAC for the first v4 only and every later address is added to that same vMAC via add_arp_entry (router/ovh.rs:113POST virtualMac/{mac}/virtualAddress). That is the assumption you are pointing at.

I cannot confirm OVH's own rule from here — whether one vMAC may legitimately hold N addresses or whether each address needs its own vMAC — I have no OVH credentials and the docs/API console are not reachable from this machine. But it does not change the verdict: even if OVH accepted it, the guest never gets address 2.

Prod evidence that this path has never run: of 412 live IP assignments there is not one VM with more than one live IPv4, and not one vMAC holding more than one address (GET /api/admin/v1/vm_ip_assignments, all pages, grouped by vm_id and by the mac=ip arp_ref prefix).

Multi-address provisioning therefore needs a NIC per address — net1/ipconfig1…, one vMAC per NIC, per-NIC ARP entries and the matching capacity/reconcile handling — which is a separate piece of work from selling the dimension.

Proposal, happy to be overruled: keep this PR to schema, pricing, capacity accounting and the API/admin surface, and refuse an order above 1 address until the multi-NIC work lands, so nothing can be sold that would not be configured. The multi-NIC provisioning becomes its own issue where the OVH vMAC rule gets settled first with your account access. Say the word if you would rather I grow this branch to do the multi-NIC work instead.

@v0l
v0l force-pushed the feat/template-ip-resources-105 branch from 6d69637 to f77ab1a Compare July 30, 2026 14:26
@v0l
v0l merged commit f527c74 into master Jul 30, 2026
6 checks passed
@v0l
v0l deleted the feat/template-ip-resources-105 branch July 30, 2026 16:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Template IP resources

1 participant