PageSourceSearch

https://docs.pantavisor.io/assets/js/be4b650c.df41e233.js

js pantavisor.io collected 2026-10-04 00:17:49 UTC 43,042 bytes, 1 lines download raw bytes

1"use strict";(self.webpackChunkpantavisor_docs=self.webpackChunkpantavisor_docs||[]).push([["12983"],{45676(e,n,t){t.r(n),t.d(n,{metadata:()=>s,default:()=>p,frontMatter:()=>a,contentTitle:()=>l,toc:()=>o,assets:()=>d});var s=JSON.parse('{"id":"meta-pantavisor/overview/testing/manual/testplans/testplan-ipam","title":"IPAM Networking Test Plan","description":"Tests for IPAM (IP Address Management) pool-based container networking via the appengine environment.","source":"@site/reference/meta-pantavisor/overview/testing/manual/testplans/testplan-ipam.md","sourceDirName":"meta-pantavisor/overview/testing/manual/testplans","slug":"/meta-pantavisor/overview/testing/manual/testplans/testplan-ipam","permalink":"/meta-pantavisor/overview/testing/manual/testplans/testplan-ipam","draft":false,"unlisted":false,"tags":[],"version":"current","frontMatter":{},"sidebar":"metaPantavisorOverviewSidebar","previous":{"title":"Download-Progress Reporting Test Plan","permalink":"/meta-pantavisor/overview/testing/manual/testplans/testplan-download-progress"},"next":{"title":"Object Download Resume Test Plan","permalink":"/meta-pantavisor/overview/testing/manual/testplans/testplan-object-download-resume"}}'),r=t(74848),i=t(28453);let a={},l="IPAM Networking Test Plan",d={},o=[{value:"Prerequisites",id:"prerequisites",level:2},{value:"Build Appengine Image and Test Containers",id:"build-appengine-image-and-test-containers",level:3},{value:"Common Setup",id:"common-setup",level:3},{value:"Common Teardown",id:"common-teardown",level:3},{value:"Test 1: Basic IPAM Pool Allocation",id:"test-1-basic-ipam-pool-allocation",level:2},{value:"Setup",id:"setup",level:3},{value:"Execute",id:"execute",level:3},{value:"Verify",id:"verify",level:3},{value:"Expected Results",id:"expected-results",level:3},{value:"Test 2: Static IP Assignment",id:"test-2-static-ip-assignment",level:2},{value:"Setup",id:"setup-1",level:3},{value:"Execute",id:"execute-1",level:3},{value:"Verify",id:"verify-1",level:3},{value:"Expected Results",id:"expected-results-1",level:3},{value:"Test 3: Two pools, NAT vs no-NAT",id:"test-3-two-pools-nat-vs-no-nat",level:2},{value:"Setup",id:"setup-2",level:3},{value:"Execute",id:"execute-2",level:3},{value:"Verify",id:"verify-2",level:3},{value:"Expected Results",id:"expected-results-2",level:3},{value:"Test 4: NAT backend selection (nftables preferred)",id:"test-4-nat-backend-selection-nftables-preferred",level:2},{value:"Verify backend selection",id:"verify-backend-selection",level:3},{value:"Verify iptables fallback (optional)",id:"verify-iptables-fallback-optional",level:3},{value:"Test 6: IPAM lease stability across stop/start",id:"test-6-ipam-lease-stability-across-stopstart",level:2},{value:"Setup",id:"setup-3",level:3},{value:"Execute + verify",id:"execute--verify",level:3},{value:"Expected Results",id:"expected-results-3",level:3},{value:"Auto-recovery note",id:"auto-recovery-note",level:3},{value:"Test 7: Revision rejected on unknown pool reference",id:"test-7-revision-rejected-on-unknown-pool-reference",level:2},{value:"Setup",id:"setup-4",level:3},{value:"Verify",id:"verify-3",level:3},{value:"Expected Results",id:"expected-results-4",level:3},{value:"Notes",id:"notes",level:3},{value:"Test 8: Pool-using container with baked <code>lxc.net.*</code> is refused",id:"test-8-pool-using-container-with-baked-lxcnet-is-refused",level:2},{value:"Verification approach",id:"verification-approach",level:3},{value:"Option A \u2014 code review (recommended default)",id:"option-a--code-review-recommended-default",level:4},{value:"Option B \u2014 manual reproduction",id:"option-b--manual-reproduction",level:4},{value:"Expected log",id:"expected-log",level:3},{value:"Regression guard",id:"regression-guard",level:3},{value:"Test 9: Static-IP reservation from backend-native containers",id:"test-9-static-ip-reservation-from-backend-native-containers",level:2},{value:"Setup",id:"setup-5",level:3},{value:"Verify",id:"verify-4",level:3},{value:"Expected Results",id:"expected-results-5",level:3},{value:"What failure looks like",id:"what-failure-looks-like",level:3},{value:"Notes",id:"notes-1",level:3},{value:"Troubleshooting",id:"troubleshooting",level:2}];function c(e){let n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",h4:"h4",header:"header",hr:"hr",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,i.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.header,{children:(0,r.jsx)(n.h1,{id:"ipam-networking-test-plan",children:"IPAM Networking Test Plan"})}),"\n",(0,r.jsx)(n.p,{children:"Tests for IPAM (IP Address Management) pool
1-based container networking via the appengine environment."}),"\n",(0,r.jsx)(n.hr,{}),"\n",(0,r.jsx)(n.h2,{id:"prerequisites",children:"Prerequisites"}),"\n",(0,r.jsx)(n.h3,{id:"build-appengine-image-and-test-containers",children:"Build Appengine Image and Test Containers"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"./kas-container build kas/build-configs/release/docker-x86_64-scarthgap.yaml:kas/with-workspace.yaml \\\n    --target pv-example-device-ipam \\\n    --target pv-example-device-ipam-2pools \\\n    --target pv-example-device-ipam-lxcbr \\\n    --target pv-example-net-server \\\n    --target pv-example-net-lab-server \\\n    --target pv-example-net-client \\\n    --target pv-example-net-pvcnet \\\n    --target pv-example-ipam-valid \\\n    --target pv-example-ipam-invalid \\\n    --target pv-example-ipam-collision \\\n    --target pv-example-ipam-nopool \\\n    --target pv-example-ipam-static \\\n    --target pantavisor-appengine\n\ndocker load < build/tmp-scarthgap/deploy/images/docker-x86_64/pantavisor-appengine-docker.tar\n"})}),"\n",(0,r.jsx)(n.h3,{id:"common-setup",children:"Common Setup"}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"pv-example-device-ipam"})," pvrexport provides the ",(0,r.jsx)(n.code,{children:"device.json"})," with IPAM pool\ndefinitions. It must be included in ",(0,r.jsx)(n.code,{children:"pvtx.d/"})," alongside container pvrexports so that\n",(0,r.jsx)(n.code,{children:"pvtx add"})," merges the device.json into the trail during appengine startup."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"docker rm -f pva-test 2>/dev/null\ndocker volume rm storage-test 2>/dev/null\nmkdir -p pvtx.d\n"})}),"\n",(0,r.jsx)(n.h3,{id:"common-teardown",children:"Common Teardown"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"docker rm -f pva-test\ndocker volume rm storage-test\n"})}),"\n",(0,r.jsx)(n.hr,{}),"\n",(0,r.jsx)(n.h2,{id:"test-1-basic-ipam-pool-allocation",children:"Test 1: Basic IPAM Pool Allocation"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Purpose"}),": Verify containers referencing an IPAM pool get automatic IP allocation from the configured subnet."]}),"\n",(0,r.jsx)(n.h3,{id:"setup",children:"Setup"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"rm -f pvtx.d/*.pvrexport.tgz\ncp build/tmp-scarthgap/deploy/images/docker-x86_64/pv-example-device-ipam.pvrexport.tgz pvtx.d/\ncp build/tmp-scarthgap/deploy/images/docker-x86_64/pv-example-net-server.pvrexport.tgz pvtx.d/\ncp build/tmp-scarthgap/deploy/images/docker-x86_64/pv-example-net-client.pvrexport.tgz pvtx.d/\n"})}),"\n",(0,r.jsx)(n.h3,{id:"execute",children:"Execute"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"docker rm -f pva-test 2>/dev/null; docker volume rm storage-test 2>/dev/null\ndocker run --name pva-test -d --privileged \\\n    -v $(pwd)/pvtx.d:/usr/lib/pantavisor/pvtx.d \\\n    -v storage-test:/var/pantavisor/storage \\\n    --entrypoint /bin/sh pantavisor-appengine:latest -c \"sleep infinity\"\n\ndocker exec pva-test sh -c 'pv-appengine &'\nsleep 15\n"})}),"\n",(0,r.jsx)(n.h3,{id:"verify",children:"Verify"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"# Check both containers are running with IPs from 10.0.5.0/24\ndocker exec pva-test lxc-ls -f\n# Expected: net-server and net-client RUNNING with 10.0.5.x IPs\n\n# Check device.json was loaded with pool config\ndocker exec pva-test cat /var/pantavisor/storage/trails/0/device.json\n# Expected: network.pools.internal with subnet 10.0.5.0/24\n\n# Check IPAM log messages\ndocker exec pva-test grep -i \"ipam\\|pool\\|allocated\" \\\n    /var/pantavisor/storage/logs/0/pantavisor/pantavisor.log\n# Expected: pool 'internal' added, IPs allocated to each container\n\n# Check bridge was created\ndocker exec pva-test ip addr show pvbr0\n# Expected: pvbr0 bridge with 10.0.5.1/24\n"})}),"\n",(0,r.jsx)(n.h3,{id:"expected-results",children:"Expected Results"}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Check"}),(0,r.jsx)(n.th,{children:"Expected"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Container status"}),(0,r.jsx)(n.td,{children:"Both RUNNING"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"net-client IP"}),(0,r.jsx)(n.td,{children:"10.0.5.x (auto-allocated from pool)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"net-server IP"}),(0,r.jsx)(n.td,{children:"10.0.5.x (auto-allocated from pool)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Bridge pvbr0"}),(0,r.jsx)(n.td,{children:"Created with gateway 10.0.5.1/24"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"IPAM log"}),(0,r.jsx)(n.td,{children:'"added pool \'internal\'", "allocated 10.0.5.x"'})]})]})]}),"\n",(0,r.jsx)(n.hr,{}),"\n",(0,r.jsx)(n.h2,{id:"test-2-static-ip-assignment",children:"Test 2: Static IP Assignment"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Purpose"}),": Verify a container can request a specific static IP from the pool."]}
1),"\n",(0,r.jsx)(n.h3,{id:"setup-1",children:"Setup"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"rm -f pvtx.d/*.pvrexport.tgz\ncp build/tmp-scarthgap/deploy/images/docker-x86_64/pv-example-device-ipam.pvrexport.tgz pvtx.d/\ncp build/tmp-scarthgap/deploy/images/docker-x86_64/pv-example-ipam-valid.pvrexport.tgz pvtx.d/\n"})}),"\n",(0,r.jsx)(n.h3,{id:"execute-1",children:"Execute"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"docker rm -f pva-test 2>/dev/null; docker volume rm storage-test 2>/dev/null\ndocker run --name pva-test -d --privileged \\\n    -v $(pwd)/pvtx.d:/usr/lib/pantavisor/pvtx.d \\\n    -v storage-test:/var/pantavisor/storage \\\n    --entrypoint /bin/sh pantavisor-appengine:latest -c \"sleep infinity\"\n\ndocker exec pva-test sh -c 'pv-appengine &'\nsleep 15\n"})}),"\n",(0,r.jsx)(n.h3,{id:"verify-1",children:"Verify"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:'docker exec pva-test lxc-ls -f\n# Expected: pv-example-ipam-valid RUNNING with 10.0.5.50\n\ndocker exec pva-test grep "allocated" \\\n    /var/pantavisor/storage/logs/0/pantavisor/pantavisor.log\n# Expected: allocated 10.0.5.50 to pv-example-ipam-valid\n'})}),"\n",(0,r.jsx)(n.h3,{id:"expected-results-1",children:"Expected Results"}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Check"}),(0,r.jsx)(n.th,{children:"Expected"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Container status"}),(0,r.jsx)(n.td,{children:"RUNNING"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Assigned IP"}),(0,r.jsx)(n.td,{children:"10.0.5.50 (static, as requested in args.json)"})]})]})]}),"\n",(0,r.jsx)(n.hr,{}),"\n",(0,r.jsx)(n.h2,{id:"test-3-two-pools-nat-vs-no-nat",children:"Test 3: Two pools, NAT vs no-NAT"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Purpose"}),": Verify each pool's ",(0,r.jsx)(n.code,{children:"nat"})," flag independently controls outbound MASQUERADE. One pool with ",(0,r.jsx)(n.code,{children:"nat: true"})," can reach external hosts; one with ",(0,r.jsx)(n.code,{children:"nat: false"})," cannot (source IPs aren't translated and are not routable beyond the bridge)."]}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"device-ipam-2pools"})," export defines:"]}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Pool"}),(0,r.jsx)(n.th,{children:"Bridge"}),(0,r.jsx)(n.th,{children:"Subnet"}),(0,r.jsx)(n.th,{children:"NAT"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"internal"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"pvbr0"})}),(0,r.jsx)(n.td,{children:"10.0.5.0/24"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"true"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"lab"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"pvbr1"})}),(0,r.jsx)(n.td,{children:"10.0.6.0/24"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"false"})})]})]})]}),"\n",(0,r.jsx)(n.h3,{id:"setup-2",children:"Setup"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"rm -f pvtx.d/*.pvrexport.tgz\ncp build/tmp-scarthgap/deploy/images/docker-x86_64/pv-example-device-ipam-2pools.pvrexport.tgz pvtx.d/\ncp build/tmp-scarthgap/deploy/images/docker-x86_64/pv-example-net-server.pvrexport.tgz pvtx.d/\ncp build/tmp-scarthgap/deploy/images/docker-x86_64/pv-example-net-lab-server.pvrexport.tgz pvtx.d/\n"})}),"\n",(0,r.jsx)(n.h3,{id:"execute-2",children:"Execute"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"docker rm -f pva-test 2>/dev/null; docker volume rm storage-test 2>/dev/null\ndocker run --name pva-test -d --privileged \\\n    -v $(pwd)/pvtx.d:/usr/lib/pantavisor/pvtx.d \\\n    -v storage-test:/var/pantavisor/storage \\\n    --entrypoint /bin/sh pantavisor-appengine:latest -c \"sleep infinity\"\n\ndocker exec pva-test sh -c 'pv-appengine &'\nsleep 20\n"})}),"\n",(0,r.jsx)(n.h3,{id:"verify-2",children:"Verify"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"# Both containers running with IPs from their respective pools\ndocker exec pva-test lxc-ls -f\n\n# Only the internal pool got a MASQUERADE rule\ndocker exec pva-test nft list ruleset\n\n# Outbound from internal (nat=true) \u2014 should succeed\ndocker exec pva-test pventer -c pv-example-net-server ping 8.8.8.8 -c 2\n\n# Outbound from lab (nat=false) \u2014 should 100% loss (no MASQUERADE)\ndocker exec pva-test pventer -c pv-example-net-lab-server ping 8.8.8.8 -c 2\n\n# Same-pool intra-subnet \u2014 should succeed on lab despite no NAT\ndocker exec pva-test pventer -c pv-example-net-lab-server ping 10.0.6.1 -c 2\n"})}),"\n",(0,r.jsx)(n.h3,{id:"expected-results-2",children:"Expected Results"}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Check"}),(0,r.jsx)(n.th,{children:"Expected"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"nft list ruleset"})}),(0,r.jsx)(n.td,{children:"Single MASQUERADE entry for 10.0.5.0/24 on pvbr0 (no rule for pvbr1)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"net-server (internal) \u2192 8.8.8.8"}),(0,r.jsx)(n.td,{children:"0% packet loss"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"net-lab-server (lab) \u2192 8.8.8.8"}),(0,r.jsx)(n.td,{children:"100% packet loss"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"net-lab-server \u2192 10.0.6.1 (gateway)"}),(0,r.jsx)(n.td,{children:"0% packet loss (bridge-local, no NAT needed)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"IPAM log"}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"added pool 'internal': ..., nat=yes"})," and ",(0,r.jsx)(n.code,{children:"added pool 'lab': ..., nat=no"}),"; ",(0,r.jsx)(n.code,{children:"setup NAT (nftables) for pool internal"})," appears but no such line for ",(0,r.jsx)(n.code,{children:"lab"})]})]})]})]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Note"}),": With the current IPAM implementation there is no cross-pool isolation \u2014 ",(0,r.jsx)(n.code,{children:"internal \u2192 lab"})," (e.g. ",(0,r.jsx)(n.code,{children:"ping 10.0.6.2"})," from net-server) will succeed because the kernel's FORWARD chain defaults to ACCEPT. Cross-pool isolation is a separate feature (tracked for a follow-up)."]}),"\n",(0,r.jsx)(n.hr,{}),"\n",(0,r.jsx)(n.h2,{id:"test-4-nat-backend-selection-nftables-preferred",children:"Test 4: NAT backend selection (nftables preferred)"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Purpose"}),": Verify pantavisor's ",(0,r.jsx)(n.code,{children:"setup_nat"})," probes ",(0,r.jsx)(n.code,{children:"command -v nft"})," and ",(0,r.jsx)(n.code,{children:"command -v iptables"})," and prefers nftables when both are available, falling back to iptables only if nft is missing or fails."]}),"\n",(0,r.jsxs)(n.p,{children:["The appengine image ships with ",(0,r.jsx)(n.code,{children:"nftables"})," installed (",(0,r.jsx)(n.code,{children:"iptables"})," is ",(0,r.jsx)(n.strong,{children:"not"})," included \u2014 nftables is sufficient on every distro kernel from 2014 onwards)."]}),"\n",(0,r.jsx)(n.h3,{id:"verify-backend-selection",children:"Verify backend selection"}),"\n",(0,r.jsxs)(n.p,{children:["Re-use the Test 1 or Test 3 setup; after ",(0,r.jsx)(n.code,{children:"pv-appengine"})," is running, inspect the IPAM log:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:'docker exec pva-test grep "setup NAT" \\\n    /var/pantavisor/storage/logs/0/pantavisor/pantavisor.log\n# Expected: "setup NAT (nftables) for pool <name>"\n# No "(iptables)" lines, and no warnings about missing nft/iptables.\n'})}),"\n",(0,r.jsxs)(n.p,{children:["The nft ruleset should show a ",(0,r.jsx)(n.code,{children:"table ip nat"})," with a ",(0,r.jsx)(n.code,{children:"postrouting"})," chain of ",(0,r.jsx)(n.code,{children:"srcnat"})," priority:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"docker exec pva-test nft list ruleset\n"})}),"\n",(0,r.jsx)(n.h3,{id:"verify-iptables-fallback-optional",children:"Verify iptables fallback (optional)"}),"\n",(0,r.jsxs)(n.p,{children:["To exercise the fallback path you would need a variant appengine image that ships ",(0,r.jsx)(n.code,{children:"iptables"})," but not ",(0,r.jsx)(n.code,{children:"nftables"}),", and confirm the log then says ",(0,r.jsx)(n.code,{children:"setup NAT (iptables) for pool <name>"}),". This isn't covered by the default appengine image."]}),"\n",(0,r.jsx)(n.hr,{}),"\n",(0,r.jsx)(n.h2,{id:"test-6-ipam-lease-stability-across-stopstart",children:"Test 6: IPAM lease stability across stop/start"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Purpose"}),": Verify that stopping and starting a container via the container-control API (",(0,r.jsx)(n.code,{children:"pvcontrol containers stop"})," / ",(0,r.jsx)(n.code,{children:"start"}),") keeps the container's IPAM-assigned IP stable. The same expectation applies to the auto-recovery restart path \u2014 a container that crashes and is restarted by ",(0,r.jsx)(n.code,{children:"pv_state_check_auto_recovery"})," also comes back with the same IP."]}),"\n",(0,r.jsxs)(n.p,{children:["The lease is keyed by ",(0,r.jsx)(n.code,{children:"(pool_name, container_name)"})," and ",(0,r.jsx)(n.code,{children:"pv_ipam_allocate"})," reuses any existing lease before allocating a new IP, so the IP persists across any lifecycle transition that doesn't destroy the platform (container-control stop/start, auto-recovery retries). Platform teardown (",(0,r.jsx)(n.code,{children:"pv_platform_free"}),", on state transition / reboot / rollback) does release the lease; that is intentional."]}),"\n",(0,r.jsx)(n.h3,{id:"setup-3",children:"Setup"}),"\n",(0,r.jsxs)(n.p,{children:["Reuse the Test 1 single-pool setup (",(0,r.jsx)(n.code,{children:"pv-example-device-ipam"})," + ",(0,r.jsx)(n.code,{children:"pv-example-net-server"}),")."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"rm -f pvtx.d/*.pvrexport.tgz\ncp build/tmp-scarthgap/deploy/images/docker-x86_64/pv-example-device-ipam.pvrexport.tgz pvtx.d/\ncp build/tmp-scarthgap/deploy/images/docker-x86_64/pv-example-net-server.pvrexport.tgz pvtx.d/\n\ndocker rm -f pva-test 2>/dev/null; docker volume rm storage-test 2>/dev/null\ndocker run --name pva-test -d --privileged \\\n    -v $(pwd)/pvtx.d:/usr/lib/pantavisor/pvtx.d \\\n    -v storage-test:/var/pantavisor/storage \\\n    --entrypoint /bin/sh pantavisor-appengine:latest -c \"sleep infinity\"\ndocker exec pva-test sh -c 'pv-appengine &'\nsleep 20\n"})}),"\n",(0,r.jsx)(n.h3,{id:"execute--verify",children:"Execute + verify"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:'# 1) Baseline \u2014 record the running container\'s IP\ndocker exec pva-test lxc-ls -f\n# Expected: pv-example-net-server RUNNING with 10.0.5.2\n\n# 2) Stop via the container-control API\ndocker exec pva-test pvcontrol containers stop pv-example-net-server\nsleep 10\ndocker exec pva-test lxc-ls -f\n# Expected: pv-example-net-server STOPPED (IPv4 column is blank)\n\n# 3) Start again via the API\ndocker exec pva-test pvcontrol containers start pv-example-net-server\nsleep 10\ndocker exec pva-test lxc-ls -f\n# Expected: pv-example-net-server RUNNING with 10.0.5.2 (same as baseline)\n\n# 4) The IPAM log confirms the lease was reused, not re-allocated\ndocker exec pva-test grep "reusing existing lease\\|allocated 10.0.5" \\\n    /var/pantavisor/storage/logs/0/pantavisor/pantavisor.log\n# Expected to see exactly one "allocated 10.0.5.2/24 to pv-example-net-server"\n# line from the initial start, followed by a "reusing existing lease for\n# pv-example-net-server: 10.0.5.2/24" line from the post-stop restart.\n'})}),"\n",(0,r.jsx)(n.h3,{id:"expected-results-3",children:"Expected Results"}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Check"}),(0,r.jsx)(n.th,{children:"Expected"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"IP before stop"}),(0,r.jsx)(n.td,{children:"10.0.5.2"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"State after stop"}),(0,r.jsx)(n.td,{children:"STOPPED"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"IP after start"}),(0,r.jsx)(n.td,{children:"10.0.5.2 (unchanged)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"IPAM log"}),(0,r.jsxs)(n.td,{children:["one ",(0,r.jsx)(n.code,{children:"allocated"})," line + one ",(0,r.jsx)(n.code,{children:"reusing existing lease"})," line"]})]})]})]}),"\n",(0,r.jsx)(n.h3,{id:"auto-recovery-note",children:"Auto-recovery note"}),"\n",(0,r.jsxs)(n.p,{children:["The same ",(0,r.jsx)(n.code,{children:"pv_ipam_allocate"})," reuse-by-name logic is taken when the auto-recovery path restarts a crashed container (both the delayed-retry branch via ",(0,r.jsx)(n.code,{children:"timer_retry"})," and the immediate-retry branch \u2014 see the ",(0,r.jsx)(n.code,{children:"fix(ipam): keep IPAM lease stable across auto-recovery restarts"})," commit that removed the pre-restart ",(0,r.jsx)(n.code,{children:"pv_ipam_release()"}
1)," on the immediate branch). A future expansion of this test could use a purpose-built crashing recipe to exercise that path end-to-end; for now the invariant is covered by code review plus this stop/start check, which drives the same allocate-with-reuse code path."]}),"\n",(0,r.jsx)(n.hr,{}),"\n",(0,r.jsx)(n.h2,{id:"test-7-revision-rejected-on-unknown-pool-reference",children:"Test 7: Revision rejected on unknown pool reference"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Purpose"}),": Verify pantavisor refuses the revision when any container declares ",(0,r.jsx)(n.code,{children:"PV_NETWORK_POOL"})," referencing a pool that is not defined in ",(0,r.jsx)(n.code,{children:"device.json"}),". In an in-progress update this propagates through ",(0,r.jsx)(n.code,{children:"pv_state_run \u2192 _pv_run"})," into ",(0,r.jsx)(n.code,{children:"PV_STATE_ROLLBACK"}),"; in steady state into ",(0,r.jsx)(n.code,{children:"PV_STATE_REBOOT"}),". The check is performed at ",(0,r.jsx)(n.code,{children:"pv_platform_start"})," time and short-circuits before any namespace / LXC work happens."]}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"pv-example-ipam-nopool"})," recipe ships with ",(0,r.jsx)(n.code,{children:'PV_NETWORK_POOL: "does-not-exist"'})," and reuses the minimal ",(0,r.jsx)(n.code,{children:"inherit image"})," template \u2014 it is a ~2.7 MB pvrexport with just busybox and a defensive idle-loop entrypoint (the entrypoint should never actually run)."]}),"\n",(0,r.jsx)(n.h3,{id:"setup-4",children:"Setup"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"rm -f pvtx.d/*.pvrexport.tgz\ncp build/tmp-scarthgap/deploy/images/docker-x86_64/pv-example-device-ipam.pvrexport.tgz pvtx.d/\ncp build/tmp-scarthgap/deploy/images/docker-x86_64/pv-example-net-server.pvrexport.tgz pvtx.d/\ncp build/tmp-scarthgap/deploy/images/docker-x86_64/pv-example-ipam-nopool.pvrexport.tgz pvtx.d/\n\ndocker rm -f pva-test 2>/dev/null; docker volume rm storage-test 2>/dev/null\ndocker run --name pva-test -d --privileged \\\n    -v $(pwd)/pvtx.d:/usr/lib/pantavisor/pvtx.d \\\n    -v storage-test:/var/pantavisor/storage \\\n    --entrypoint /bin/sh pantavisor-appengine:latest -c \"sleep infinity\"\ndocker exec pva-test sh -c 'pv-appengine &'\nsleep 25\n"})}),"\n",(0,r.jsx)(n.h3,{id:"verify-3",children:"Verify"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"# No containers running \u2014 the revision was torn down\ndocker exec pva-test lxc-ls -f\n# Expected: empty output\n\n# The pantavisor log confirms the refuse + teardown sequence\ndocker exec pva-test grep -E \\\n    \"unknown pool|failed IPAM network validation|did not work as expected\" \\\n    /var/pantavisor/storage/logs/0/pantavisor/pantavisor.log\n# Expected sequence (each pv_state_run tick retries and logs again):\n#   ERROR [platforms] ... references unknown pool 'does-not-exist', refusing to start\n#   ERROR [platforms] ... failed IPAM network validation, triggering rollback if in try-boot\n#   ERROR [controller] ... a platform did not work as expected. Tearing down...\n"})}),"\n",(0,r.jsx)(n.h3,{id:"expected-results-4",children:"Expected Results"}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Check"}),(0,r.jsx)(n.th,{children:"Expected"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"lxc-ls -f"})}),(0,r.jsxs)(n.td,{children:["Empty \u2014 no containers running (including the well-formed ",(0,r.jsx)(n.code,{children:"pv-example-net-server"})," in the same revision)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Pantavisor log"}),(0,r.jsx)(n.td,{children:"The three ERROR lines above appear in order"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"pv_state_run"})," return"]}),(0,r.jsxs)(n.td,{children:["Non-zero \u2014 propagates into ",(0,r.jsx)(n.code,{children:"_pv_run"})," which goes to ",(0,r.jsx)(n.code,{children:"PV_STATE_ROLLBACK"})," (in TESTING) or ",(0,r.jsx)(n.code,{children:"PV_STATE_REBOOT"})," (steady state)"]})]})]})]}),"\n",(0,r.jsx)(n.h3,{id:"notes",children:"Notes"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["The ",(0,r.jsx)(n.code,{children:"pv_platform_start"})," bubble-up is the single error-handling path \u2014 no separate validate step. The same mechanism cat
1ches volume-mount and driver-load failures."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"pv_state_run"}),"'s platform loop does not break on first error, so other platforms in the same revision may briefly attempt their starts before the overall return value triggers teardown. They still end up torn down; the only cost is a few extra log lines per tick."]}),"\n"]}),"\n",(0,r.jsx)(n.hr,{}),"\n",(0,r.jsxs)(n.h2,{id:"test-8-pool-using-container-with-baked-lxcnet-is-refused",children:["Test 8: Pool-using container with baked ",(0,r.jsx)(n.code,{children:"lxc.net.*"})," is refused"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Purpose"}),": Verify that a container which declares an IPAM pool AND bakes ",(0,r.jsx)(n.code,{children:"lxc.net.*"})," entries into its ",(0,r.jsx)(n.code,{children:"lxc.container.conf"})," is refused at start time via the backend plugin's ",(0,r.jsx)(n.code,{children:"validate_config"})," hook. The error bubbles up the same way as an unknown-pool reference \u2014 ",(0,r.jsx)(n.code,{children:"pv_state_run \u2192 _pv_run \u2192 PV_STATE_ROLLBACK"})," in a TESTING update."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Policy"})," (see ",(0,r.jsx)(n.a,{href:"https://github.com/pantavisor/pantavisor/blob/master/docs/overview/ipam.md#pre-start-validation",children:"Pantavisor IPAM overview"}),"): if a container opts into an IPAM pool, it must let pantavisor own its network namespace. Pantavisor injects its own ",(0,r.jsx)(n.code,{children:"lxc.net.0.*"})," from the allocated IP/MAC/bridge at start time; silently overwriting a user-baked ",(0,r.jsx)(n.code,{children:"lxc.net.0"})," would leak orphan attributes (e.g. stale ",(0,r.jsx)(n.code,{children:"lxc.net.0.macvlan.mode"})," after type is rewritten to veth)."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"lxc.namespace.keep = net"})," is ",(0,r.jsx)(n.strong,{children:"not"})," treated as a conflict \u2014 pvr's default template includes it, and pantavisor strips ",(0,r.jsx)(n.code,{children:"net"})," from the keep list at runtime."]}),"\n",(0,r.jsx)(n.h3,{id:"verification-approach",children:"Verification approach"}),"\n",(0,r.jsxs)(n.p,{children:["Constructing a signed pvrexport with ",(0,r.jsx)(n.code,{children:"lxc.net.*"})," baked in requires a recipe-level post-processing step that is not shipped today (the default ",(0,r.jsx)(n.code,{children:"pvr app add"})," does not produce ",(0,r.jsx)(n.code,{children:"lxc.net.*"})," entries). Two ways to exercise this test:"]}),"\n",(0,r.jsx)(n.h4,{id:"option-a--code-review-recommended-default",children:"Option A \u2014 code review (recommended default)"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Confirm ",(0,r.jsx)(n.code,{children:"plugins/pv_lxc.c:pv_validate_container_
1config"})," scans for a prefix ",(0,r.jsx)(n.code,{children:"lxc.net."})," after trimming leading whitespace and skipping comments."]}),"\n",(0,r.jsxs)(n.li,{children:["Confirm ",(0,r.jsx)(n.code,{children:"platforms.c:pv_platform_start"})," calls ",(0,r.jsx)(n.code,{children:"ctrl->validate_config(p, path)"})," before the IPAM allocation block and returns ",(0,r.jsx)(n.code,{children:"-1"})," on non-zero."]}),"\n",(0,r.jsxs)(n.li,{children:["Confirm the dlsym in ",(0,r.jsx)(n.code,{children:"load_pv_plugin"})," wires ",(0,r.jsx)(n.code,{children:"pv_validate_container_config"})," into ",(0,r.jsx)(n.code,{children:"cont_ctrl[].validate_config"}),"."]}),"\n"]}),"\n",(0,r.jsx)(n.h4,{id:"option-b--manual-reproduction",children:"Option B \u2014 manual reproduction"}),"\n",(0,r.jsxs)(n.p,{children:["In a non-signed development build, stop pantavisor, append ",(0,r.jsx)(n.code,{children:"lxc.net.0.type = veth"})," to a pool-using container's ",(0,r.jsx)(n.code,{children:"lxc.container.conf"})," in the trail, and restart pantavisor. (On a signed production build this fails signature verification first, which is a different error path \u2014 useful as a regression guard but not for exercising the ",(0,r.jsx)(n.code,{children:"validate_config"})," hook.)"]}),"\n",(0,r.jsx)(n.h3,{id:"expected-log",children:"Expected log"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"ERROR [pv_lxc]: pv_validate_container_
1config: platform '<name>' declares an IPAM pool but its lxc.container.conf already contains lxc.net.* entries \u2014 pantavisor will not overwrite them. Remove the baked lxc.net.* config, or drop the PV_NETWORK_POOL reference.\nERROR [platforms]: pv_platform_start: platform '<name>' refused by backend pre-start validation\nERROR [state]: pv_state_start_platform: platform <name> could not be started\nERROR [controller]: _pv_wait: a platform did not work as expected. Tearing down...\n"})}),"\n",(0,r.jsxs)(n.p,{children:["And ",(0,r.jsx)(n.code,{children:"docker exec pva-test lxc-ls -f"})," shows no containers running."]}),"\n",(0,r.jsx)(n.h3,{id:"regression-guard",children:"Regression guard"}),"\n",(0,r.jsx)(n.p,{children:"Re-running Tests 1-7 must still pass \u2014 the validation only fires when:"}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:["The container declares ",(0,r.jsx)(n.code,{children:"PV_NETWORK_POOL"})," (\u2192 ",(0,r.jsx)(n.code,{children:"p->network->mode == NET_MODE_POOL"}),"), and"]}),"\n",(0,r.jsxs)(n.li,{children:["The baked ",(0,r.jsx)(n.code,{children:"lxc.container.conf"})," contains a line starting with ",(0,r.jsx)(n.code,{children:"lxc.net."}),"."]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Default pvr-generated containers have no ",(0,r.jsx)(n.code,{children:"lxc.net.*"})," lines, so the check is a no-op for them."]}),"\n",(0,r.jsx)(n.hr,{}),"\n",(0,r.jsx)(n.h2,{id:"test-9-static-ip-reservation-from-backend-native-containers",children:"Test 9: Static-IP reservation from backend-native containers"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Purpose"}),": Verify pantavisor's reservation walk picks up a hard-coded ",(0,r.jsx)(n.code,{children:"lxc.net.0.ipv4.address"})," in a legacy (non-pool) container and keeps the IPAM allocator from handing the same address out to a pool-using container on the same subnet."]}),"\n",(0,r.jsx)(n.p,{children:"The test pairs three artifacts:"}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Artifact"}),(0,r.jsx)(n.th,{children:"Role"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"pv-example-device-ipam-lxcbr"})}),(0,r.jsxs)(n.td,{children:["device.json: single pool ",(0,r.jsx)(n.code,{children:"pvcnet"}),", bridge ",(0,r.jsx)(n.code,{children:"lxcbr0"}),", subnet ",(0,r.jsx)(n.code,{children:"10.0.3.0/24"}),", gateway ",(0,r.jsx)(n.code,{children:"10.0.3.1"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"pv-example-ipam-static"})}),(0,r.jsxs)(n.td,{children:["Legacy container. Does ",(0,r.jsx)(n.strong,{children:"not"})," set ",(0,r.jsx)(n.code,{children:"PV_NETWORK_POOL"}),". Uses ",(0,r.jsx)(n.code,{children:"PV_LXC_NETWORK_*"})," pvr template vars to bake ",(0,r.jsx)(n.code,{children:"lxc.net.0.type = veth"}),", ",(0,r.jsx)(n.code,{children:"lxc.net.0.link = lxcbr0"}),", ",(0,r.jsx)(n.code,{children:"lxc.net.0.ipv4.address = 10.0.3.2/24"})," directly into its lxc.container.conf."]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"pv-example-net-pvcnet"})}),(0,r.jsxs)(n.td,{children:["Pool-using container attached to the ",(0,r.jsx)(n.code,{children:"pvcnet"})," pool via ",(0,r.jsx)(n.code,{children:"PV_NETWORK_POOL"}),". Expects IPAM to hand it the next free address after the reserved ",(0,r.jsx)(n.code,{children:"10.0.3.2"}),", which is ",(0,r.jsx)(n.code,{children:"10.0.3.3"}),"."]})]})]})]}),"\n",(0,r.jsx)(n.h3,{id:"setup-5",children:"Setup"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"rm -f pvtx.d/*.pvrexport.tgz\ncp build/tmp-scarthgap/deploy/images/docker-x86_64/pv-example-device-ipam-lxcbr.pvrexport.tgz pvtx.d/\ncp build/tmp-scarthgap/deploy/images/docker-x86_64/pv-example-ipam-static.pvrexport.tgz pvtx.d/\ncp build/tmp-scarthgap/deploy/images/docker-x86_64/pv-example-net-pvcnet.pvrexport.tgz pvtx.d/\n\ndocker rm -f pva-test 2>/dev/null; docker volume rm storage-test 2>/dev/null\ndocker run --name pva-test -d --privileged \\\n    -v $(pwd)/pvtx.d:/usr/lib/pantavisor/pvtx.d \\\n    -v storage-test:/var/pantavisor/storage \\\n    --entrypoint /bin/sh pantavisor-appengine:latest -c \"sleep infinity\"\ndocker exec pva-test sh -c 'pv-appengine &'\nsleep 25\n"})}),"\n",(0,r.jsx)(n.h3,{id:"verify-4",children:"Verify"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"# Container state \u2014 both containers RUNNING with distinct IPs\ndocker exec pva-test lxc-ls -f\n# Expected:\n#   pv-example-ipam-static  RUNNING 10.
10.3.2   (from baked lxc.net.0.ipv4.address)\n#   pv-example-net-pvcnet   RUNNING 10.0.3.3   (allocator skipped .2 because it was reserved)\n\n# IPAM log \u2014 confirm the reservation and the allocation\ndocker exec pva-test grep -E \"reserved static|allocated|created bridge|setup NAT\" \\\n    /var/pantavisor/storage/logs/0/pantavisor/pantavisor.log\n# Expected sequence:\n#   INFO [ipam] setup_bridge: created bridge lxcbr0 with IP 10.0.3.1/24\n#   INFO [ipam] setup_nat: setup NAT (nftables) for pool pvcnet\n#   INFO [ipam] pv_ipam_reserve_static: reserved static IP 10.0.3.2 (from pv-example-ipam-static) in pool 'pvcnet'\n#   INFO [ipam] pv_ipam_allocate: allocated 10.0.3.3/24 to pv-example-net-pvcnet from pool pvcnet\n"})}),"\n",(0,r.jsx)(n.h3,{id:"expected-results-5",children:"Expected Results"}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Check"}),(0,r.jsx)(n.th,{children:"Expected"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"pv-example-ipam-static"})," IPv4"]}),(0,r.jsx)(n.td,{children:"10.0.3.2 (baked directly into lxc.container.conf, not allocated from the pool)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"pv-example-net-pvcnet"})," IPv4"]}),(0,r.jsx)(n.td,{children:"10.0.3.3 (next-available, with .2 reserved and .1 being the gateway)"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Log: reservation line"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"reserved static IP 10.0.3.2 (from pv-example-ipam-static) in pool 'pvcnet'"})})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Log: allocation line"}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"allocated 10.0.3.3/24 to pv-example-net-pvcnet from pool pvcnet"})})]})]})]}),"\n",(0,r.jsx)(n.h3,{id:"what-failure-looks-like",children:"What failure looks like"}),"\n",(0,r.jsxs)(n.p,{children:["If the reservation walk is broken or not being called, IPAM would happily allocate ",(0,r.jsx)(n.code,{children:"10.0.3.2"})," to ",(0,r.jsx)(n.code,{children:"pv-example-net-pvcnet"})," (since that's the first free IP after the gateway), then both containers end up fighting for the same L3 address. ",(0,r.jsx)(n.code,{children:"lxc-ls -f"})," would show the pool-using container on ",(0,r.jsx)(n.code,{children:"10.0.3.2"})," and ARP for that address on the bridge would flap between the two."]}),"\n",(0,r.jsx)(n.h3,{id:"notes-1",children:"Notes"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["The baked address (",(0,r.jsx)(n.code,{children:"10.0.3.2/24"}),") is generated via ",(0,r.jsx)(n.code,{children:"PV_LXC_NETWORK_*"})," pvr template variables in ",(0,r.jsx)(n.code,{children:"pv-example-ipam-static.args.json"}),", not by hand-editing ",(0,r.jsx)(n.code,{children:"lxc.container.conf"}),". See the ",(0,r.jsx)(n.a,{href:"../../../../../pvr/PVR_TEMPLATES.md",children:(0,r.jsx)(n.code,{children:"PVR_TEMPLATES.md"})})," reference for the supported vars."]}),"\n",(0,r.jsxs)(n.li,{children:["pv-example-ipam-static does ",(0,r.jsx)(n.strong,{children:"not"})," set ",(0,r.jsx)(n.code,{children:"PV_NETWORK_POOL"})," \u2014 the validation hook introduced in Test 8 would reject a container that declares both. The two approaches are mutually exclusive by design."]}),"\n",(0,r.jsx)(n.li,{children:"Same-subnet coexistence between lxc-native and pool-using containers is the whole point of this design. Different-subnet coexistence (legacy container on a different bridge pantavisor doesn't know about) falls through the reservation walk with a DEBUG log and no effect on pool allocation."}),"\n"]}),"\n",(0,r.jsx)(n.hr,{}),"\n",(0,r.jsx)(n.h2,{id:"troubleshooting",children:"Troubleshooting"}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Symptom"}),(0,r.jsx)(n.th,{children:"Cause"}),(0,r.jsx)(n.th,{children:"Fix"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"No IPs assigned"}),(0,r.jsx)(n.td,{children:"Missing device.json"}),(0,r.jsx)(n.td,{children:"Ensure pv-example-device-ipam.pvrexport.tgz is in pvtx.d"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:'"pool not found"'}),(0,r.jsx)(n.td,{children:"device.json not parsed"}),(0,r.jsx)(n.td,{children:"Check pantavisor.log for device.json parsing"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:'"failed to setup NAT" in log'}),(0,r.jsx)(n.td,{children:"iptables/nftables missing in appengine image"}),(0,r.jsxs)(n.td,{children:["Rebuild appengine image (iptables and nftables are pulled in by default via ",(0,r.jsx)(n.code,{children:"CORE_IMAGE_EXTRA_INSTALL"}),")"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:"Bridge not created"}),(0,r.jsx)(n.td,{children:"IPAM init failed"}),(0,r.jsx)(n.td,{children:'Check for "IPAM subsystem initialized" in log'})]})]})]})]})}function p(e={}){let{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(c,{...e})}):c(e)}},28453(e,n,t){t.d(n,{R:()=>a,x:()=>l});var s=t(96540);let r={},i=s.createContext(r);function a(e){let n=s.useContext(i);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),s.createElement(i.Provider,{value:n},e.children)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.