<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Nested-Esxi on The Nested Lab</title>
    <link>https://thenestedlab.com/tags/nested-esxi/</link>
    <description>Recent content in Nested-Esxi on The Nested Lab</description>
    <generator>Hugo</generator>
    <language>en-gb</language>
    <lastBuildDate>Wed, 16 Sep 2026 08:20:00 +0100</lastBuildDate>
    <atom:link href="https://thenestedlab.com/tags/nested-esxi/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Nested ESXi inside an NSX VPC: the trunk-subnet design</title>
      <link>https://thenestedlab.com/posts/nested-esxi-nsx-vpc/</link>
      <pubDate>Wed, 16 Sep 2026 08:20:00 +0100</pubDate>
      <guid>https://thenestedlab.com/posts/nested-esxi-nsx-vpc/</guid>
      <description>Plain VPC subnets silently blackhole a nested ESXi host. Here&amp;rsquo;s why — and the trunk subnet + binding map design that makes nested labs work as an ordinary NSX VPC tenant, verified end to end.</description>
      <content:encoded><![CDATA[<p>The host booted clean. Management IP configured, services up, DCUI happy.
And every single packet it sent — ARP included — died silently.</p>
<p>That&rsquo;s how my first attempt at running nested ESXi inside an NSX VPC ended,
and the failure mode is nasty precisely because nothing <em>looks</em> wrong. If
you&rsquo;re trying to build nested vSphere labs on VCF 9 with VPC networking,
this post is the map of the minefield — and the design that gets you across
it, verified live.</p>
<h2 id="the-setup">The setup</h2>
<p>VCF 9.1, vSphere Supervisor with NSX VPC networking. The goal: deploy nested
ESXi hosts as ordinary VM Service VMs inside a tenant&rsquo;s VPC — no physical
fabric changes, no provider tickets, no special treatment. The kind of thing
you want for training pods, cert-study labs, or reproducing customer issues.</p>
<p>Nested ESXi needs what physical ESXi needs: a management network, vMotion,
vSAN — traditionally VLANs trunked to every host. But a VPC is an overlay
world. There are no VLANs to trunk. So what happens if you just attach the
nested host&rsquo;s vNIC to a normal VPC subnet?</p>
<h2 id="failure-1-the-silent-blackhole">Failure #1: the silent blackhole</h2>
<p>Here&rsquo;s the trap. A standard VPC subnet port gets <strong>address bindings</strong>: NSX
pins the exact IP + MAC it allocated to that vNIC, and SpoofGuard drops
everything else.</p>
<p>ESXi&rsquo;s vmk0 doesn&rsquo;t use the vNIC&rsquo;s MAC. It synthesises its own:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">vmk0
</span></span><span class="line"><span class="cl">   MAC Address: 00:50:ac:1e:00:8c     &lt;- NOT the vNIC MAC (04:50:56:...)
</span></span></code></pre></div><p>So every frame the management interface sends carries a MAC the port doesn&rsquo;t
own. NSX drops it all — ARP, ping, everything — while the host itself boots
green and reports healthy. There is no error anywhere. You just can&rsquo;t reach
it, ever.</p>
<p><img alt="Standard VPC subnet port: SpoofGuard pins one IP+MAC; vmk0&rsquo;s synthesised MAC loses, silently" loading="lazy" src="/images/post1-blackhole.svg"></p>
<p>(There&rsquo;s a second trap stacked on top: VPC subnets run with DHCP deactivated,
so the appliance also sits at &ldquo;waiting for DHCP&rdquo; unless you inject static
addressing via OVF <code>guestinfo.*</code> properties. More on that below.)</p>
<h2 id="the-design-that-works-a-trunk-subnet--binding-maps">The design that works: a trunk subnet + binding maps</h2>
<p>The fix isn&rsquo;t a hack — it&rsquo;s a first-class NSX VPC construct that&rsquo;s barely
documented in the wild: <strong><code>SubnetConnectionBindingMap</code></strong>.</p>
<p>The idea:</p>
<ol>
<li>Create one ordinary VPC subnet to act as a <strong>trunk</strong> (<code>sn-trunk</code>). The
nested host&rsquo;s vNICs attach <em>only</em> here.</li>
<li>Create a normal VPC subnet per traditional network — <code>sn-mgmt</code>,
<code>sn-vmotion</code>, <code>sn-vsan</code>.</li>
<li>Bind each of those to the trunk with a <strong>binding map carrying a VLAN tag</strong>.
The nested host&rsquo;s vSwitch tags frames exactly as it would on metal; the
binding map strips the tag and delivers the frame into the right subnet.</li>
</ol>
<p>Pure L2 demultiplexing. One vNIC carries N VLANs, the VPC never routes on a
tag, and the physical fabric never sees any of it (the 802.1Q header rides
inside the Geneve overlay).</p>
<p>All of it is tenant-creatable through the supervisor as Kubernetes objects:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># sn-trunk and sn-mgmt are ordinary Private Subnets; the interesting object:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">crd.nsx.vmware.com/v1alpha1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">SubnetConnectionBindingMap</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w"> </span>{<span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">bm-mgmt}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">subnetName</span><span class="p">:</span><span class="w"> </span><span class="l">sn-mgmt         </span><span class="w"> </span><span class="c"># the map is a child of the VLAN subnet...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">targetSubnetName</span><span class="p">:</span><span class="w"> </span><span class="l">sn-trunk  </span><span class="w"> </span><span class="c"># ...and points AT the trunk</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">vlanTrafficTag</span><span class="p">:</span><span class="w"> </span><span class="m">1610</span><span class="w">
</span></span></span></code></pre></div><p>That direction is easy to invert, so it&rsquo;s worth saying twice: <strong>the binding
map belongs to the VLAN subnet and points at the trunk</strong>, not the other way
round.</p>
<p><img alt="NSX: sn-trunk realized once per VPC, binding maps hanging off the VLAN subnets" loading="lazy" src="/images/ui/u11b-nsx-sntrunk-per-vpc.jpg"></p>
<p>On the nested host, nothing exotic — plain VST, like physical:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">Name                Virtual Switch  Active Clients  VLAN ID
</span></span><span class="line"><span class="cl">------------------  --------------  --------------  -------
</span></span><span class="line"><span class="cl">Management Network  vSwitch0                     1     1610
</span></span><span class="line"><span class="cl">vMotion             vSwitch0                     1     1611
</span></span><span class="line"><span class="cl">vSAN                vSwitch0                     1     1612
</span></span></code></pre></div><p><img alt="Host Client: port groups on VLANs 1610 / 1611 / 1612" loading="lazy" src="/images/ui/u12a-hostclient-portgroups-vlans.jpg">
<em>The same three VLANs as the nested host sees them.</em></p>
<p>And because there&rsquo;s no DHCP in a VPC subnet, the nested-ESXi appliance gets
its identity through OVF properties in the VM Service spec:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">bootstrap</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">vAppConfig</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- {<span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="nt">guestinfo.ipaddress, value</span><span class="p">:</span><span class="w"> </span>{<span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;172.30.0.40&#34;</span>}}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- {<span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="nt">guestinfo.netmask,   value</span><span class="p">:</span><span class="w"> </span>{<span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;255.255.255.224&#34;</span>}}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- {<span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="nt">guestinfo.gateway,   value</span><span class="p">:</span><span class="w"> </span>{<span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;172.30.0.33&#34;</span>}}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- {<span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="nt">guestinfo.vlan,     value</span><span class="p">:</span><span class="w"> </span>{<span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;1610&#34;</span>}}<span class="w">
</span></span></span></code></pre></div><h2 id="does-it-actually-work-the-receipts">Does it actually work? The receipts</h2>
<p>Two nested hosts, vNICs on <code>sn-trunk</code>, three VLANs. From host one:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">[root@esx01:~] vmkping -c2 172.30.0.41            # mgmt, VLAN 1610
</span></span><span class="line"><span class="cl">3 packets transmitted, 3 packets received, 0% packet loss
</span></span><span class="line"><span class="cl">[root@esx01:~] vmkping -I vmk1 -c3 172.30.0.71    # vMotion, VLAN 1611
</span></span><span class="line"><span class="cl">3 packets transmitted, 3 packets received, 0% packet loss
</span></span><span class="line"><span class="cl">[root@esx01:~] vmkping -I vmk2 -c3 172.30.0.101   # vSAN, VLAN 1612
</span></span><span class="line"><span class="cl">3 packets transmitted, 3 packets received, 0% packet loss
</span></span></code></pre></div><p><img alt="Live capture: vmnic0 down, vMotion and vSAN VLANs still passing at 0% loss" loading="lazy" src="/images/demo-c6-nic-failover.jpg">
<em>The transcript that matters: fail the first NIC, and every VLAN keeps flowing on the second — captured live.</em></p>
<p>Two more results worth knowing before you design around this:</p>
<p><strong>Untagged frames are dropped.</strong> I put a probe vmk on the untagged
portgroup using the address NSX itself had allocated to the trunk port:
100% loss, empty ARP table, while tagged traffic flowed happily beside it.
Every network your nested host uses needs a VLAN and a binding map — there
is no untagged fallback.</p>
<p><strong>Failover behaves like real hardware.</strong> With two vNICs on the trunk teamed
active/active, <code>esxcli network nic down -n vmnic0</code> moved every VLAN onto
vmnic1 with zero loss — and the SSH session I was watching from never
dropped. The vmk MAC migrating between trunk ports mid-flow is exactly the
scenario that MAC-pinned standard ports would blackhole; the trunk carries
it fine.</p>
<h2 id="why-this-matters-outside-the-lab">Why this matters outside the lab</h2>
<p>Running whole vSphere environments <em>inside</em> a VPC turns the platform into
something most customers never had: a way to stand up complete, isolated
copies of infrastructure on demand, without a physical fabric change and
without waiting for anyone. That&rsquo;s what makes it commercially interesting:</p>
<ul>
<li><strong>Training and certification labs</strong> where every learner gets a real
vSphere environment, not a shared one.</li>
<li><strong>Reproducing a customer problem</strong> on a like-for-like copy instead of on
the customer&rsquo;s estate.</li>
<li><strong>Rehearsing upgrades and migrations</strong> end to end before the change
window, then throwing the copy away.</li>
<li><strong>Vendor and feature evaluations</strong> with real behaviour, at zero risk to
production.</li>
</ul>
<p>This is the design Comms-care uses to give every consultant a dedicated
environment, and the same pattern scales to a classroom or a proof-of-concept
factory.</p>
<h2 id="rules-learned">Rules learned</h2>
<ul>
<li>A nested ESXi vNIC on a <strong>standard</strong> VPC subnet is dead on arrival:
vmk0&rsquo;s synthesised MAC loses to SpoofGuard, silently.</li>
<li>Attach nested-host vNICs <strong>only to a trunk subnet</strong>; one binding map per
VLAN; the map lives under the VLAN subnet and points at the trunk.</li>
<li><strong>No DHCP in VPC subnets</strong> — bootstrap addressing via <code>guestinfo.*</code>
(appliances) or cloud-init (Linux). Static IP plans are a feature in a
lab anyway.</li>
<li>ESXi&rsquo;s default TCP/IP stack has <strong>one</strong> gateway — set per-vmk override
gateways (<code>esxcli ... ipv4 set -g</code>) so vMotion/vSAN carry their own
subnet&rsquo;s gateway.</li>
<li>Recreating a VM <strong>reallocates</strong> its NSX addresses. Pin what you depend on.</li>
<li>MTU: everything here ran at 1500. Raise the trunk and the nested vDS
before you do vSAN at any real scale.</li>
</ul>
<p>Next in this series: what happens when you want <em>ten</em> of these labs — with
byte-identical IP plans, firewalled from each other by construction. That&rsquo;s
where NSX VPCs go from &ldquo;workaround&rdquo; to genuinely better than physical.</p>
<hr>
<p><em>Lab environment; opinions my own. Everything above was captured from a live
VCF 9.1 environment — output trimmed for length, never edited for outcome.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Three datacenters, one IP plan: identical isolated pods with NSX VPCs</title>
      <link>https://thenestedlab.com/posts/three-datacenters-one-ip-plan/</link>
      <pubDate>Wed, 16 Sep 2026 08:00:00 +0100</pubDate>
      <guid>https://thenestedlab.com/posts/three-datacenters-one-ip-plan/</guid>
      <description>Three nested-ESXi pods, byte-identical addressing — same subnets, same VLANs, same host IPs, even the same MACs — with zero reachability between them. How overlapping VPC CIDRs and deterministic subnet realization turn cookie-cutter environments into a first-class feature.</description>
      <content:encoded><![CDATA[<p>Here are three hosts, all answering to <code>vmk0 = 172.30.0.40</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">ssh root@192.168.144.30  -&gt;  [root@esx01-a:~]  vmk0  172.30.0.40
</span></span><span class="line"><span class="cl">ssh root@192.168.144.32  -&gt;  [root@esx01-b:~]  vmk0  172.30.0.40
</span></span><span class="line"><span class="cl">ssh root@192.168.144.34  -&gt;  [root@esx01-c:~]  vmk0  172.30.0.40
</span></span></code></pre></div><p>Same IP. Same VLAN. Same gateway. Same <em>MAC address</em>, as it turns out. And
none of them can reach any of the others. This is the post where NSX VPCs
stop being a workaround for nested labs and become genuinely better than
the physical alternative.</p>
<p><img alt="Three pods, identical IP plans, no route between them" loading="lazy" src="/images/product-01-hook.jpg"></p>
<h2 id="why-identical-addressing-matters">Why identical addressing matters</h2>
<p>If you&rsquo;ve ever built training pods, cert-study labs, or per-team
reproduction environments, you know the pain: every copy needs a unique
address plan, so every runbook, every screenshot, every &ldquo;type this exact
command&rdquo; has to be parameterised per pod. Students in seat 7 see different
numbers from the slides. Reproductions drift from the original.</p>
<p>The fix is obvious and normally impossible: <strong>give every pod the same
addresses</strong>. On a physical fabric that means VRFs, per-pod NAT, and a
network team that stops answering your emails. In an NSX VPC it&rsquo;s the
default behaviour.</p>
<h2 id="the-mechanism-overlapping-privateips">The mechanism: overlapping privateIPs</h2>
<p>Each pod gets its own VPC, and every VPC declares the same private range:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">vpc.nsx.vmware.com/v1alpha1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">VPC</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w"> </span>{<span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">nested-vpc-a}     </span><span class="w"> </span><span class="c"># then -b, then -c</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">privateIPs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;172.30.0.0/16&#34;</span><span class="p">]</span><span class="w">      </span><span class="c"># identical in all three</span><span class="w">
</span></span></span></code></pre></div><p>A <code>Private</code> subnet is never advertised beyond its VPC, so NSX has no
objection to three VPCs carving up the same /16. The pods aren&rsquo;t
&ldquo;firewalled from each other&rdquo; — there is simply no route between them.
Isolation by construction, not by policy.</p>
<p><img alt="NSX: four VPCs, four sn-mgmt subnets, same CIDR" loading="lazy" src="/images/ui/u11a-nsx-snmgmt-four-vpcs.jpg">
<em>NSX&rsquo;s subnet view filtered to <code>sn-mgmt</code>: four rows, four VPCs, one CIDR.</em></p>
<h2 id="the-trick-deterministic-realization">The trick: deterministic realization</h2>
<p>Identical <em>ranges</em> aren&rsquo;t enough — I want identical <em>subnets</em>, so the
management gateway is <code>.33</code> and the hosts are <code>.40</code>/<code>.41</code> in every pod.
NSX allocates subnets from <code>privateIPs</code> in creation order, and a fresh VPC
allocates deterministically. So the topology is applied in a <strong>fixed
order</strong> — trunk, mgmt, vMotion, vSAN — and every pod realizes the same
map:</p>
<table>
	<thead>
			<tr>
					<th>Subnet</th>
					<th>Realized</th>
					<th>VLAN</th>
					<th>Hosts</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>sn-trunk</td>
					<td>172.30.0.0/27</td>
					<td>—</td>
					<td>(carries the tags)</td>
			</tr>
			<tr>
					<td>sn-mgmt</td>
					<td>172.30.0.32/27</td>
					<td>1610</td>
					<td>.40 / .41, gw .33</td>
			</tr>
			<tr>
					<td>sn-vmotion</td>
					<td>172.30.0.64/27</td>
					<td>1611</td>
					<td>.70 / .71, gw .65</td>
			</tr>
			<tr>
					<td>sn-vsan</td>
					<td>172.30.0.96/27</td>
					<td>1612</td>
					<td>.100 / .101, gw .97</td>
			</tr>
	</tbody>
</table>
<p>In the catalog blueprint that order is enforced with <code>dependsOn</code> between
the subnet resources — the one place a declarative tool needs to be told
about sequence. Skip it and two pods can come out with mgmt and vMotion
swapped, which works perfectly and confuses everyone.</p>
<p><img alt="NSX: nested-vpc-a expanded, the /16 private block" loading="lazy" src="/images/ui/u11-nsx-vpc-a-cidr.jpg"></p>
<h2 id="the-door-one-vip-per-host">The door: one VIP per host</h2>
<p>Each pod is unreachable from outside by design, so each host gets a
<code>VirtualMachineService</code> of type <code>LoadBalancer</code> publishing SSH and HTTPS.
The VIPs come from the org&rsquo;s <em>external</em> block, and they&rsquo;re the only
addresses that differ between pods:</p>
<table>
	<thead>
			<tr>
					<th>Pod</th>
					<th>VPC</th>
					<th>esx01 VIP</th>
					<th>esx02 VIP</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>a</td>
					<td>nested-vpc-a</td>
					<td>192.168.144.30</td>
					<td>.31</td>
			</tr>
			<tr>
					<td>b</td>
					<td>nested-vpc-b</td>
					<td>192.168.144.32</td>
					<td>.33</td>
			</tr>
			<tr>
					<td>c</td>
					<td>nested-vpc-c</td>
					<td>192.168.144.34</td>
					<td>.35</td>
			</tr>
	</tbody>
</table>
<p>Which is how the opening transcript works: three VIPs, three hosts, one
inside address.</p>
<h2 id="proving-the-isolation">Proving the isolation</h2>
<p>Claims are cheap. The test matrix, from a VM in a <em>fourth</em> VPC (the org
default):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">ping 172.30.0.140 (own VPC)....... REACHABLE
</span></span><span class="line"><span class="cl">ping 172.30.0.40  (pod space)..... unreachable
</span></span><span class="line"><span class="cl">curl http://172.31.0.2/ (shared).. shared-svc repo01
</span></span></code></pre></div><p><img alt="Isolation matrix: own-VPC reachable, pod space unreachable, shared service reachable" loading="lazy" src="/images/demo-c7-isolation.jpg"></p>
<p>Its own VPC&rsquo;s <code>172.30.0.140</code>: reachable. <code>172.30.0.40</code> — an address that
exists in three other VPCs simultaneously: unreachable, because from here
there is no such route. (The third line is the shared-services VPC, which
is <a href="/series/the-vpc-pod-papers/">the next post</a>.)</p>
<p>Inside each pod, east-west is normal: <code>esx01 → esx02</code> vmkping passes on
all three VLANs, in all three pods. And the detail I didn&rsquo;t expect: the
nested-ESXi appliance derives vmk0&rsquo;s MAC deterministically from its
config, so <strong>the three hosts share a MAC as well as an IP</strong>. Harmless — each
VPC is its own L2 domain — but a nice demonstration of how complete the
separation is.</p>
<h2 id="what-this-replaces">What this replaces</h2>
<table>
	<thead>
			<tr>
					<th></th>
					<th>Physical / VLAN-based pods</th>
					<th>VPC pods</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Identical addressing</td>
					<td>VRF per pod + NAT, fabric change per pod</td>
					<td>default behaviour</td>
			</tr>
			<tr>
					<td>Adding a pod</td>
					<td>switch config, IPAM, firewall rules</td>
					<td>one API call for the VPC, one blueprint request</td>
			</tr>
			<tr>
					<td>Isolation guarantee</td>
					<td>policy (auditable, breakable)</td>
					<td>topology (no route exists)</td>
			</tr>
			<tr>
					<td>Tenant self-service</td>
					<td>no</td>
					<td>yes — the VPC is a tenant object</td>
			</tr>
	</tbody>
</table>
<h2 id="why-this-matters-outside-the-lab">Why this matters outside the lab</h2>
<p>&ldquo;Identical environments&rdquo; sounds like a lab nicety. It&rsquo;s actually one of the
most requested things in enterprise IT, usually asked for in other words:</p>
<ul>
<li><strong>Training at scale</strong> — every seat in the room sees the same addresses as
the slides, so material is written once and never parameterised per pod.</li>
<li><strong>Per-engineer or per-team replicas</strong> of a reference environment, for
development and testing that behaves exactly like the original.</li>
<li><strong>Regulatory or business-unit separation</strong> on shared infrastructure
without VRF sprawl or a bespoke firewall estate — isolation is a property
of the topology, which is the easiest kind to evidence to an auditor.</li>
<li><strong>Blue/green copies</strong> of an environment for change rehearsal, then
cut-over or discard.</li>
</ul>
<p>On a physical network each of these is a project. On VCF with NSX VPCs it&rsquo;s
a template.</p>
<h2 id="rules-learned">Rules learned</h2>
<ul>
<li>Overlapping <code>privateIPs</code> across VPCs is <strong>supported and intentional</strong>.
Identical pods are a feature, not a hack.</li>
<li>Fresh VPCs realize subnets <strong>deterministically in creation order</strong> —
fix the order (<code>dependsOn</code> in a blueprint) and every pod gets the same
map.</li>
<li>Pods are unreachable from outside by construction; publish exactly what
you mean to via <code>LoadBalancer</code> VIPs from the external block.</li>
<li>Prove isolation from a <em>different</em> VPC, with a positive control (own
VPC reachable) beside the negative.</li>
<li>Expect duplicate MACs across pods from appliance images. It&rsquo;s fine.</li>
</ul>
<p><em>Previously: <a href="/posts/the-lb-that-must-exist-first/">the LB that must exist first</a>.
Next: one WSUS for pods that can&rsquo;t see each other.</em></p>
<hr>
<p><em>Lab environment; opinions my own. Output captured live, trimmed for length,
never edited for outcome.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>A datacenter in a catalog tile: nested ESXi pods via VCF Automation All Apps</title>
      <link>https://thenestedlab.com/posts/nested-esxi-via-vcfa-all-apps/</link>
      <pubDate>Wed, 16 Sep 2026 07:40:00 +0100</pubDate>
      <guid>https://thenestedlab.com/posts/nested-esxi-via-vcfa-all-apps/</guid>
      <description>The whole isolated pod — namespace, trunk subnets, binding maps, two dual-NIC nested ESXi hosts with an ISO attached, SSH/HTTPS VIPs — as one VCF Automation blueprint, published to the catalog. Anatomy of the blueprint, the ordering it enforces, and the three things it can&amp;rsquo;t express.</description>
      <content:encoded><![CDATA[<p>Everything in this series so far was built with <code>kubectl</code> and API calls.
That proves the platform. It doesn&rsquo;t make a <em>product</em>. This post turns the
pod into a <strong>catalog item</strong>: fill in a name, pick a VPC, click Request, and
a few minutes later there&rsquo;s a datacenter-in-miniature with two SSH prompts
waiting.</p>
<p><img alt="VCFA catalog: the nested-esxi-pod tile" loading="lazy" src="/images/ui/u1-catalog-tile.jpg"></p>
<h2 id="all-apps-in-one-paragraph">All Apps in one paragraph</h2>
<p>VCF Automation 9.1 has two provisioning models side by side. <strong>VM Apps</strong> is
the classic Aria Automation path — cloud templates through an IaaS engine
that drives vCenter. <strong>All Apps</strong> is the supervisor-native path: the
blueprint composes Kubernetes objects (a Supervisor Namespace, VM Service
VMs, NSX subnets, VKS clusters) and the vSphere Supervisor&rsquo;s controllers
reconcile them. A blueprint is <code>formatVersion: 2</code>; its resources are
<code>CCI.Supervisor.Namespace</code> and <code>CCI.Supervisor.Resource</code> — the latter is
literally &ldquo;here&rsquo;s a manifest, apply it in that namespace.&rdquo;</p>
<p>That makes the blueprint a <em>composition</em> of the manifests from the earlier
posts, with two additions: inputs, and <code>dependsOn</code>.</p>
<h2 id="the-blueprint-section-by-section">The blueprint, section by section</h2>
<h3 id="inputs--the-form">Inputs — the form</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">inputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">podName</span><span class="p">:</span><span class="w">  </span>{<span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="nt">string, default</span><span class="p">:</span><span class="w"> </span><span class="nt">nested-pod, pattern</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;^[a-z0-9]([-a-z0-9]*[a-z0-9])?$&#39;</span>}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">vpcName</span><span class="p">:</span><span class="w">  </span>{<span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="nt">string, description</span><span class="p">:</span><span class="w"> </span><span class="l">Must exist and be Realized before deploying.}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">esxOva</span><span class="p">:</span><span class="w">   </span>{<span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="nt">string, default</span><span class="p">:</span><span class="w"> </span><span class="l">vmi-61bb062ddfc506b79}  </span><span class="w"> </span><span class="c"># Nested ESXi 9.1 appliance</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">isoImage</span><span class="p">:</span><span class="w"> </span>{<span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="nt">string, default</span><span class="p">:</span><span class="w"> </span><span class="l">vmi-39f562e2ae9e9c501}  </span><span class="w"> </span><span class="c"># the ISO to attach</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">vmClass</span><span class="p">:</span><span class="w">  </span>{<span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="nt">string, default</span><span class="p">:</span><span class="w"> </span><span class="nt">best-effort-large, enum</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">best-effort-large, best-effort-xlarge, best-effort-2xlarge]}</span><span class="w">
</span></span></span></code></pre></div><p><img alt="The request form" loading="lazy" src="/images/ui/u2-request-form.jpg"></p>
<h3 id="the-namespace--with-libraries-attached">The namespace — with libraries attached</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">namespace</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">CCI.Supervisor.Namespace</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">generateName</span><span class="p">:</span><span class="w"> </span><span class="l">${input.podName}-       </span><span class="w"> </span><span class="c"># NOT name — new namespaces get a suffix</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">className</span><span class="p">:</span><span class="w"> </span><span class="l">large</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">regionName</span><span class="p">:</span><span class="w"> </span><span class="l">f06</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">vpcName</span><span class="p">:</span><span class="w"> </span><span class="l">${input.vpcName}             </span><span class="w"> </span><span class="c"># pins the namespace to the pod&#39;s VPC</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">storageClasses</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>{<span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="nt">vSAN Default Storage Policy, limit</span><span class="p">:</span><span class="w"> </span><span class="l">400000Mi}]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">zones</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>{<span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="nt">domain-c9, cpuLimit</span><span class="p">:</span><span class="w"> </span><span class="nt">40000M, memoryLimit</span><span class="p">:</span><span class="w"> </span><span class="l">64000Mi, ...}]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">contentSources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- {<span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="nt">ISO, type</span><span class="p">:</span><span class="w"> </span><span class="l">ContentLibrary}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- {<span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="nt">f06-vks-lib01, type</span><span class="p">:</span><span class="w"> </span><span class="l">ContentLibrary}</span><span class="w">
</span></span></span></code></pre></div><p><code>contentSources</code> is the line that closes the gap a lot of first attempts
hit: a VCFA-created namespace has <strong>no content library</strong>, so there are no
<code>VirtualMachineImage</code>s and nothing can be deployed. Declaring the libraries
here attaches them at creation.</p>
<h3 id="the-topology--ordered-on-purpose">The topology — ordered on purpose</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">snTrunk</span><span class="p">:</span><span class="w">   </span>{<span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="nt">CCI.Supervisor.Resource, properties</span><span class="p">:</span><span class="w"> </span>{<span class="nt">context</span><span class="p">:</span><span class="w"> </span><span class="l">${resource.namespace.id}, manifest: &lt;Subnet sn-trunk&gt;}}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">snMgmt</span><span class="p">:</span><span class="w">    </span>{<span class="nt">dependsOn</span><span class="p">:</span><span class="w"> </span><span class="nt">[snTrunk], ...  manifest</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;Subnet sn-mgmt&gt;}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">snVmotion</span><span class="p">:</span><span class="w"> </span>{<span class="nt">dependsOn</span><span class="p">:</span><span class="w"> </span><span class="nt">[snMgmt],  ...  manifest</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;Subnet sn-vmotion&gt;}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">bmMgmt</span><span class="p">:</span><span class="w">    </span>{<span class="nt">... manifest</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;SubnetConnectionBindingMap sn-mgmt -&gt; sn-trunk, vlan 1610&gt;}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">bmVmotion</span><span class="p">:</span><span class="w"> </span>{<span class="nt">... manifest</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;SubnetConnectionBindingMap sn-vmotion -&gt; sn-trunk, vlan 1611&gt;}</span><span class="w">
</span></span></span></code></pre></div><p>The <code>dependsOn</code> chain is the whole reason <a href="/posts/three-datacenters-one-ip-plan/">every pod has identical
CIDRs</a>: fresh VPCs realize subnets
in creation order, and the blueprint fixes that order.</p>
<p><img alt="Blueprint canvas and YAML side by side" loading="lazy" src="/images/ui/u3-blueprint-canvas-yaml.jpg"></p>
<h3 id="the-hosts--dual-nic-iso-attached-bootstrapped-by-ovf">The hosts — dual-NIC, ISO attached, bootstrapped by OVF</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">esx01</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">CCI.Supervisor.Resource</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">dependsOn</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">bmMgmt]                   </span><span class="w"> </span><span class="c"># no point booting before VLAN 1610 exists</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">properties</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">manifest</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">VirtualMachine               </span><span class="w"> </span><span class="c"># vmoperator.vmware.com/v1alpha5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">hardware</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">cdrom</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="l">... the ISO, declared, connected ... ]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">network</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">interfaces</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="l">eth0 -&gt; sn-trunk, eth1 -&gt; sn-trunk ]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">bootstrap</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">vAppConfig</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="l">guestinfo.hostname / ipaddress / vlan / ... ]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">wait</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">fields</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>{<span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="nt">status.powerState, value</span><span class="p">:</span><span class="w"> </span><span class="l">PoweredOn}]</span><span class="w">
</span></span></span></code></pre></div><p>(Abridged — the full resource carries the image references, VM class,
guest ID and the complete <code>guestinfo</code> set.)</p>
<p>Two vNICs, both on the trunk — <a href="/series/the-vpc-pod-papers/">the nested equivalent of a VCF host&rsquo;s two
pNICs</a>. The ISO rides along as a declarative
CD-ROM. And the <code>wait</code> block makes the deployment&rsquo;s <em>completion</em> mean
something: the request doesn&rsquo;t finish until the host is powered on.</p>
<h3 id="the-doors--one-vip-per-host">The doors — one VIP per host</h3>
<p>A <code>VirtualMachineService</code> of type <code>LoadBalancer</code> per host, selecting it by
label and publishing 22 and 443, and a blueprint <strong>output</strong> that reads the
VIP back out of the service&rsquo;s status:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">outputs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">esx01Ssh</span><span class="p">:</span><span class="w"> </span>{<span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;ssh root@${resource.esx01Access.object.status.loadBalancer.ingress[0].ip}&#34;</span>}<span class="w">
</span></span></span></code></pre></div><p>The outputs surface in the deployment view — the requester gets the SSH
command, not a scavenger hunt.</p>
<p><img alt="Deployment topology after a successful request" loading="lazy" src="/images/ui/u4-deployment-topology.jpg"></p>
<p><img alt="Request → deployment in progress → complete" loading="lazy" src="/images/u7-catalog-request-flow.gif">
<em>The request flow, end to end.</em></p>
<h2 id="what-the-blueprint-cannot-express-yet">What the blueprint cannot express (yet)</h2>
<p>Three cluster-scoped objects have <strong>no blueprint resource type</strong>, and they
must exist <em>before</em> the request — <a href="/posts/the-lb-that-must-exist-first/">in this order</a>:</p>
<ol>
<li><code>VPC</code> — <code>privateIPs: 172.30.0.0/16</code>, same in every pod</li>
<li><code>VPCAttachment</code> — connectivity profile with the service gateway; the LB
creation fails loudly without it</li>
<li><code>LoadBalancer</code> — silently, permanently required before the namespace</li>
</ol>
<p>Today that&rsquo;s a short script or a runbook step per pod. The honest framing:
the blueprint is the <em>pod</em>; the VPC is the <em>tenancy</em>, and tenancy is
still created one layer up. I&rsquo;d expect that layer to become blueprintable;
until then, keep the three calls next to the blueprint in version control.</p>
<h2 id="publishing-one-version-at-a-time">Publishing: one version at a time</h2>
<p>Blueprint → <code>BlueprintVersion</code> → release. Validation happens at <em>version</em>
time, not create time, and the result lives in <code>status.validationMessages</code>
rather than the HTTP code — a 200 with <code>ContentValid: False</code> is a thing.
And only <strong>one</strong> version can be published: unrelease 1.0.0 before releasing
1.1.0, or you get a 409. (The full list of sharp edges is
<a href="/series/the-vpc-pod-papers/">its own post</a>.)</p>
<h2 id="why-this-matters-outside-the-lab">Why this matters outside the lab</h2>
<p>This is where platform engineering turns into a service. The difference
between &ldquo;we can build you an environment&rdquo; and &ldquo;request one from the
catalog&rdquo; is the difference between days and minutes — and between a
bespoke build and one that is consistent, quota-controlled and recorded
every time. For an organisation that means:</p>
<ul>
<li><strong>Time-to-environment</strong> measured in minutes, requested by the people who
need it, without a queue.</li>
<li><strong>Consistency by construction</strong> — every environment comes from the same
definition, so support, training material and runbooks all match.</li>
<li><strong>Governance built in</strong> — quotas, ownership, history and clean teardown
are properties of the deployment record, not a spreadsheet.</li>
</ul>
<p>The nested-ESXi pod is one catalog item. The same approach delivers any
environment shape: application stacks for developers, sandboxes for a
proof of concept, demo kits for a sales team, isolated builds for a
partner.</p>
<h2 id="rules-learned">Rules learned</h2>
<ul>
<li>All Apps blueprints are <strong>compositions of manifests</strong>: <code>CCI.Supervisor.Namespace</code>
plus <code>CCI.Supervisor.Resource</code> per object. If it works with <code>kubectl</code>, it
works in a blueprint.</li>
<li><code>generateName</code>, not <code>name</code>, for the namespace; <code>contentSources</code> to attach
libraries at creation; <code>zones</code>/<code>storageClasses</code> flat, not wrapped.</li>
<li><code>dependsOn</code> is how you get <strong>deterministic CIDRs</strong> — order the subnets.</li>
<li><code>wait.fields</code> turns &ldquo;request complete&rdquo; into &ldquo;host is powered on&rdquo;.</li>
<li>VPC / VPCAttachment / LoadBalancer are <strong>prerequisites outside the
blueprint</strong>, in that order, before every request.</li>
<li>One published version per blueprint; validation in <code>status</code>, not the
HTTP response.</li>
</ul>
<p><em>Previously: <a href="/posts/shared-services-for-isolated-tenants/">shared services for isolated tenants</a>.
This closes the Pod Papers&rsquo; core arc — the companion posts on
<a href="/series/the-vpc-pod-papers/">dual-NIC</a>, <a href="/series/the-vpc-pod-papers/">no-DHCP bootstrap</a>
and <a href="/series/the-vpc-pod-papers/">blueprint gotchas</a> fill in the details.</em></p>
<hr>
<p><em>Lab environment; opinions my own. Blueprint <code>nested-esxi-pod</code> 1.1.0 is
live in the lab catalog; YAML above trimmed for length.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Dual-NIC nested hosts: what redundancy means when the fabric is virtual</title>
      <link>https://thenestedlab.com/posts/dual-nic-nested-hosts/</link>
      <pubDate>Wed, 16 Sep 2026 07:30:00 +0100</pubDate>
      <guid>https://thenestedlab.com/posts/dual-nic-nested-hosts/</guid>
      <description>VCF wants two pNICs per host. In a nested lab the second vNIC adds no physical redundancy — so why add it? Because bringup validation and uplink teaming expect it, and because the failover test tells you something real about the trunk. vmnic0 down, 0% loss, and the SSH session watching it never dropped.</description>
      <content:encoded><![CDATA[<p>&ldquo;Naturally, a VCF host has at least two NICs. Are we testing that, or have
you virtualised it away?&rdquo;</p>
<p>Fair question, and the honest answer has two halves. In a nested lab the
<em>physical</em> redundancy is provided by the outer host — its vDS, its NSX
uplinks — and a second vNIC on the nested VM adds precisely none. But VCF
doesn&rsquo;t know it&rsquo;s nested. Bringup&rsquo;s host validation and the vDS uplink
teaming it configures <strong>expect two vmnics</strong>, and a host with one gets
flagged. So the nested hosts get two vNICs, both on the trunk subnet, and
the question becomes: does failover between them actually work inside a
VPC?</p>
<h2 id="the-setup">The setup</h2>
<p>Both vNICs attach to the same <code>sn-trunk</code> subnet — <a href="/posts/nested-esxi-nsx-vpc/">the trunk from part
1</a> — and ESXi sees them as two 10G vmnics:</p>
<p><img alt="Host Client: vmnic0 and vmnic1, both 10 Gbit/s on vSwitch0" loading="lazy" src="/images/ui/u13-hostclient-dual-nics.jpg"></p>
<p>vSwitch0 teams them active/active with the default originating-port-ID
policy; every portgroup (Management 1610, vMotion 1611, vSAN 1612) inherits
it. Nothing you wouldn&rsquo;t do on metal.</p>
<h2 id="the-test-pull-a-nic-while-watching-from-inside">The test: pull a NIC while watching from inside</h2>
<p>The interesting bit isn&rsquo;t whether pings continue — it&rsquo;s <em>which</em> session
I&rsquo;m watching from. I&rsquo;m SSH&rsquo;d to <code>esx01</code> <strong>through its public VIP</strong>, which
means my session traverses the NSX LB → the VPC → the trunk port → whichever
vmnic happens to carry vmk0. If failover breaks anything, it breaks the
terminal I&rsquo;m typing in.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">[root@esx01-a:~] esxcli network nic list
</span></span><span class="line"><span class="cl">Name    ...  Admin Status  Link Status  Speed  MAC Address
</span></span><span class="line"><span class="cl">vmnic0  ...  Up            Up           10000  04:50:56:00:5c:03
</span></span><span class="line"><span class="cl">vmnic1  ...  Up            Up           10000  04:50:56:00:68:00
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">[root@esx01-a:~] esxcli network nic down -n vmnic0    # FAIL THE FIRST NIC
</span></span><span class="line"><span class="cl">vmnic0  ...  Down          Down             0  04:50:56:00:5c:03
</span></span><span class="line"><span class="cl">vmnic1  ...  Up            Up           10000  04:50:56:00:68:00
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">[root@esx01-a:~] vmkping -I vmk1 172.30.0.71          # vMotion VLAN, now over vmnic1
</span></span><span class="line"><span class="cl">3 packets transmitted, 3 packets received, 0% packet loss
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">[root@esx01-a:~] vmkping -I vmk2 172.30.0.101         # vSAN VLAN, now over vmnic1
</span></span><span class="line"><span class="cl">3 packets transmitted, 3 packets received, 0% packet loss
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">[root@esx01-a:~] esxcli network nic up -n vmnic0      # restore
</span></span><span class="line"><span class="cl"># session never dropped.
</span></span></code></pre></div><p><img alt="Before / after: vmnic0 down, every VLAN still passing" loading="lazy" src="/images/c6-nic-failover-beforeafter.gif"></p>
<p><img alt="Full failover transcript" loading="lazy" src="/images/demo-c6-nic-failover.jpg"></p>
<p>Every VLAN moved to vmnic1. Zero loss on vMotion and vSAN. And the
management session — the one <em>most</em> likely to notice — never blinked.</p>
<h2 id="why-this-is-a-real-result-not-a-party-trick">Why this is a real result, not a party trick</h2>
<p>Think about what just happened at the NSX layer. vmk0&rsquo;s MAC — a MAC ESXi
synthesised, not the vNIC&rsquo;s — was being learned on trunk port A. When
vmnic0 went down, the same MAC appeared on trunk port B mid-flow, with an
established TCP session riding on it.</p>
<p>On a <strong>standard</strong> VPC subnet port that is exactly the scenario SpoofGuard
exists to stop: the port&rsquo;s address bindings pin one MAC, and a frame from a
different MAC — or the <em>same</em> MAC arriving on a different port — is dropped.
Part 1 showed that killing the host on a standard subnet before it ever
spoke. This test shows the trunk subnet tolerating the live migration of a
foreign MAC between two of its ports, which is the property nested vSphere
(and anything else with a vSwitch inside a VM) fundamentally needs.</p>
<p>So the second vNIC buys three things, none of them physical redundancy:</p>
<ol>
<li><strong>Bringup and vLCM stop complaining</strong> about a single-uplink host.</li>
<li><strong>The teaming policy you&rsquo;ll configure in production gets exercised</strong> —
uplink failover, active/standby for vSAN, whatever you&rsquo;re rehearsing.</li>
<li><strong>A live proof that the trunk carries MAC mobility</strong>, which is the
real assurance that the design isn&rsquo;t relying on a quiet network.</li>
</ol>
<h2 id="what-it-does-not-buy-and-how-to-say-so">What it does <em>not</em> buy, and how to say so</h2>
<p>If someone asks &ldquo;is this host redundant?&rdquo;, the answer is &ldquo;the nested host
believes it is; actual redundancy lives one layer down.&rdquo; In a training pod
that&rsquo;s the correct and useful answer — students configure and test failover
exactly as they would on metal, and the outer platform does the real work.
In a reproduction lab for a customer NIC-teaming issue, it&rsquo;s usually enough
too: most teaming bugs are in ESXi&rsquo;s policy handling, not in the copper.</p>
<p>Where it&rsquo;s genuinely insufficient: anything about physical link behaviour —
LACP negotiation, LLDP, flapping, MTU mismatch on one uplink. The virtual
fabric never fails asymmetrically, so it can&rsquo;t reproduce those.</p>
<h2 id="why-this-matters-outside-the-lab">Why this matters outside the lab</h2>
<p>The practical value here is knowing <em>what a nested environment can and
can&rsquo;t prove</em> — which is what lets you decide when a virtual lab is enough
and when it isn&rsquo;t. For training, upgrade rehearsals, configuration and
policy testing and the vast majority of &ldquo;how does it behave when…&rdquo;
questions, nested is enough and dramatically cheaper. For physical link
behaviour — LACP, optics, asymmetric faults — you still want metal. Being
able to make that call confidently is worth more than the test itself.</p>
<h2 id="rules-learned">Rules learned</h2>
<ul>
<li>Give nested VCF hosts <strong>two vNICs on the same trunk subnet</strong>. Bringup,
vLCM and vDS teaming expect ≥ 2 vmnics; humouring them costs nothing.</li>
<li>Test failover <strong>from a session that depends on it</strong> (SSH via the VIP).
Pings passing while your terminal dies is not success.</li>
<li>The trunk subnet tolerates a <strong>vmk MAC moving between ports mid-flow</strong>
— that&rsquo;s the property standard subnets lack and nested vSphere needs.</li>
<li>Be precise in the write-up: nested dual-NIC gives <em>policy</em> realism, not
<em>physical</em> redundancy. Physical link faults can&rsquo;t be reproduced here.</li>
<li>Rebuilds re-run the vmk config: the appliance creates vmk0 only;
vmk1/vmk2, their VLANs and override gateways are applied post-boot.</li>
</ul>
<p><em>Companion to <a href="/posts/nested-esxi-nsx-vpc/">nested ESXi inside an NSX VPC</a>.</em></p>
<hr>
<p><em>Lab environment; opinions my own. Output captured live, trimmed for length,
never edited for outcome.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>One catalog item, one VCF instance: building a lab factory</title>
      <link>https://thenestedlab.com/posts/one-catalog-item-one-vcf-instance/</link>
      <pubDate>Wed, 16 Sep 2026 06:40:00 +0100</pubDate>
      <guid>https://thenestedlab.com/posts/one-catalog-item-one-vcf-instance/</guid>
      <description>How an interactive PowerShell script grew into a catalog-driven factory that stands up complete nested VCF 9.1 instances — hosts, bringup, supervisor, fleet components — from a single request form. The design rules that made it survivable, and the traps that shaped them.</description>
      <content:encoded><![CDATA[<p>Every nested VCF lab starts the same way: a heroic PowerShell script.
Ours was <code>esxihostdeploy.ps1</code> — ovftool plus PowerCLI, an interactive menu
asking which environment, which ESX version, which role, how many hosts.
It worked. It also lived on one person&rsquo;s machine, prompted for credentials,
and knew nothing about everything that comes <em>after</em> the hosts exist.</p>
<p>This post is about what it became: a set of VCF Automation catalog items
where requesting <strong>one form</strong> produces a complete nested VCF 9.1 instance —
ESXi hosts, bringup (vCenter, NSX, SDDC Manager), a vSphere Supervisor,
VCF Automation, Operations, identity — with the environment number as
practically the only real input.</p>
<h2 id="the-shape-of-the-factory">The shape of the factory</h2>
<p>Three stages, each a catalog item, plus a wrapper that chains them:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">Stage 1  Nested ESX Hosts        VM Apps template + vRO actions
</span></span><span class="line"><span class="cl">         (the old script, reborn declaratively)
</span></span><span class="line"><span class="cl">Stage 2  Deploy VCF 9.1 Instance vRO workflow driving the VCF Installer API
</span></span><span class="line"><span class="cl">         (spec generated, validated, bringup started)
</span></span><span class="line"><span class="cl">Day-N    Supervisor · NSX Edge · VCF Automation · Ops Logs/Networks/RTM ·
</span></span><span class="line"><span class="cl">         Identity (AD)           one catalog item each
</span></span><span class="line"><span class="cl">Wrapper  &#34;Deploy VCF Stack&#34;      one form, checkbox per component
</span></span></code></pre></div><p><img alt="The factory catalog: hosts, bringup, every day-N component, and the wrapper — ten tiles" loading="lazy" src="/images/ui/f1-f00-factory-catalog.jpg"></p>
<p>The wrapper&rsquo;s form has a checkbox per component; ticking one reveals that
component&rsquo;s tab with every field pre-populated. One lab password feeds every
credential. Tick everything, click request, and go get coffee.</p>
<h2 id="rule-1-derive-everything-from-one-number">Rule 1: derive everything from one number</h2>
<p>Each lab environment is <code>f0X</code>, and <em>everything</em> scales from X by formula:</p>
<table>
	<thead>
			<tr>
					<th>Element</th>
					<th>Pattern</th>
					<th>f03 example</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Names</td>
					<td><code>f0X-m01-*</code></td>
					<td><code>f03-m01-vc01.res.lab</code></td>
			</tr>
			<tr>
					<td>Subnets</td>
					<td><code>10.(20+X).&lt;sub&gt;.0/24</code></td>
					<td><code>10.23.1.0/24</code> (mgmt)</td>
			</tr>
			<tr>
					<td>VLANs</td>
					<td><code>2X0n</code></td>
					<td>2307 (edge TEP)</td>
			</tr>
	</tbody>
</table>
<p>The bringup spec — hundreds of lines of JSON the VCF Installer wants — is
generated by a vRO action from a known-good reference spec plus X. Nobody
edits a deployment spec by hand, which means nobody typo-breaks a bringup at
2am. When the old script did this, the formulas lived in string
concatenation; now they live in one action with the reference spec beside it.</p>
<p>The same philosophy carried into stage 1: the script&rsquo;s &ldquo;next free esxNN
index&rdquo; scan became a vRO action bound to the request form, its VLAN/IP
arithmetic became template expressions, its <code>--prop:guestinfo.*</code> flags
became <code>ovfProperties</code> in the template. Porting a script isn&rsquo;t rewriting
it — it&rsquo;s finding the declarative home for each behaviour.</p>
<h2 id="rule-2-never-wait-for-anything-you-can-watch-instead">Rule 2: never wait for anything you can watch instead</h2>
<p>The hard constraint that shaped the whole design: a VCF Automation request
gets about <strong>two hours</strong> before the platform gives up on it. A full VCF
bringup takes longer than that. So the wrapper <em>never waits</em>:</p>
<ul>
<li>Bringup is <strong>fire-and-forget</strong> — the workflow authenticates to the
installer, validates the spec, starts the task, and hands back a
<code>watchTaskId</code>. Re-attach any time to check on it.</li>
<li>Fleet deployments (VCF Automation, Ops for Logs/Networks, metrics) are
server-side tasks; the items submit with <code>waitForCompletion=false</code>.</li>
<li>The supervisor item submits enablement and returns; vCenter carries on.</li>
<li>Only fast, deterministic steps (identity configuration, minutes) run to
completion inside the request.</li>
</ul>
<p><img alt="F06-Mgmt-VCF: the wrapper deployment, Create Successful, 13:12 → 14:05" loading="lazy" src="/images/ui/f3-f00-stack-deployment-success.jpg">
<em>A whole VCF instance as one deployment record — the request finished in under an hour while the build ran on for twelve.</em></p>
<p>Result: a full-stack kick-off <em>completes</em> as a request in well under an hour (53 minutes on the run pictured),
while the actual multi-hour build continues as watchable server-side tasks.
The request&rsquo;s job isn&rsquo;t to do the work — it&rsquo;s to <strong>start the work
correctly</strong> and tell you where to watch it.</p>
<p>The corollary: a failed component is recorded and the remaining components
still run. You fix one thing and re-run one item, not the world.</p>
<h2 id="rule-3-plan-mode-for-infrastructure">Rule 3: plan mode for infrastructure</h2>
<p>Every item in the chain supports <code>validateOnly</code> — and the wrapper cascades
it. Tick everything, set validateOnly, and the entire stack is <em>planned</em>
against the live environment with zero changes: specs generated,
prerequisites checked, name/IP collisions caught. A smoke runner exercises
exactly this on every change to the automation itself.</p>
<p>If you build nothing else into your lab automation, build this. The number
of 2am bringups saved by a five-minute dry run is not small.</p>
<h2 id="the-traps-that-shaped-the-rules">The traps that shaped the rules</h2>
<p>Some of the design above exists because of scars:</p>
<ul>
<li><strong>Hardware validation hates virtual NVMe.</strong> Bringup&rsquo;s HCL check will
block nested hosts; the spec generator has to account for it, or you
discover it two hours in — twice, if you&rsquo;re us.</li>
<li><strong>Small disks, surprising layouts.</strong> Nested hosts with 64 GB disks ship
ESX-OSDATA at essentially the whole disk; a post-provision step relocates
scratch or stage-2 fills the disk with logs.</li>
<li><strong>DNS is a prerequisite, not a step.</strong> The installer&rsquo;s pre-flight wants
every record resolvable before it starts; a one-shot script creates the
per-environment records ahead of the request.</li>
<li><strong>vRO&rsquo;s content-source lag.</strong> A new or changed workflow takes 15–20
minutes of data-collection before the catalog sees it. Publish, wait,
<em>then</em> test — or you&rsquo;ll debug a ghost.</li>
<li><strong>Wrapper inputs are duplicated by necessity.</strong> vRO requires every
sub-workflow input to be passed explicitly, so adding an input to a
component means updating the wrapper&rsquo;s call too. Null-guards in each
component turn a forgotten field into a loud failure instead of a silent
default.</li>
</ul>
<h2 id="why-bother">Why bother?</h2>
<p>Because the payoff compounds. Once a full VCF instance is a catalog request,
everything downstream changes character: upgrade rehearsals happen on
freshly-built instances instead of precious pets; a broken environment is
redeployed, not repaired; and the lab stops being a snowflake collection and
becomes a <em>product</em> — versioned, validated, reproducible.</p>
<p><img alt="Three pods, identical IP plans, no route between them" loading="lazy" src="/images/product-01-hook.jpg">
<em>Where this is heading: the factory&rsquo;s output feeding per-student pods with identical addressing.</em></p>
<p>The factory&rsquo;s next customers, funnily enough, are the isolated VPC pods from
<a href="/series/the-vpc-pod-papers/">the other series on this blog</a> — same
philosophy, one layer further down.</p>
<h2 id="why-this-matters-outside-the-lab">Why this matters outside the lab</h2>
<p>A complete VCF instance from one form changes what an environment costs
to have. Environments that used to be precious — because building one took
a week — become disposable, and a lot follows from that:</p>
<ul>
<li><strong>Proofs of concept</strong> run on an environment built for the customer&rsquo;s
scenario, not on whatever happens to be free.</li>
<li><strong>Upgrade and migration rehearsals</strong> happen on a fresh instance of the
right version, then it&rsquo;s deleted.</li>
<li><strong>Training and enablement</strong> get a real VCF per person or per team.</li>
<li><strong>Reference builds</strong> exist for every supported release, on demand.</li>
</ul>
<p>This is how Comms-care provides a dedicated instance to every consultant.
The same factory, pointed at a customer&rsquo;s requirements, is a repeatable
way to deliver environments rather than a one-off project each time.</p>
<h2 id="rules-learned">Rules learned</h2>
<ul>
<li>Derive names, subnets and VLANs from a single environment number; generate
specs, never hand-edit them.</li>
<li>Respect the request-duration ceiling: start long work, return a task
handle, re-attach to watch. Never block a wrapper on an hours-long task.</li>
<li><code>validateOnly</code> on every item, cascaded by the wrapper — dry-run the whole
stack before touching anything.</li>
<li>Componentise failure: one broken step re-runs alone.</li>
<li>Pre-create DNS; expect HCL friction on virtual hardware; budget for vRO&rsquo;s
content-source lag.</li>
</ul>
<hr>
<p><em>Lab environment; opinions my own. The automation described builds nested
VCF 9.1 instances for lab and rehearsal use — patterns transfer, specifics
are ours.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Porting a PowerShell deploy script to a catalog item: the mapping table is the post</title>
      <link>https://thenestedlab.com/posts/porting-a-powershell-deploy-script/</link>
      <pubDate>Wed, 16 Sep 2026 06:20:00 +0100</pubDate>
      <guid>https://thenestedlab.com/posts/porting-a-powershell-deploy-script/</guid>
      <description>esxihostdeploy.ps1 was 400 lines of ovftool and PowerCLI behind a menu. It became a cloud template, two vRO actions and two subscriptions — and the interesting part is deciding where each behaviour belongs. The full mapping, four non-obvious decisions, and the &amp;lsquo;yes&amp;rsquo; that isn&amp;rsquo;t &amp;rsquo;true&amp;rsquo;.</description>
      <content:encoded><![CDATA[<p>Every lab has one: the script that builds the nested hosts. Ours was
<code>esxihostdeploy.ps1</code> — ovftool plus PowerCLI, an interactive menu for
environment, ESX version, role, size, host count, then a loop of
<code>ovftool --prop:guestinfo.*</code>, <code>Set-VM</code>, <code>New-NetworkAdapter</code>, <code>Set-HardDisk</code>.
It worked for years. It also prompted for credentials, lived on one
machine, and knew nothing about the bringup that came after.</p>
<p>Porting it to a VCF Automation catalog item (VM Apps — a cloud template
plus vRO) is not a rewrite. It&rsquo;s a <strong>sorting exercise</strong>: every behaviour in
the script has a natural home in the declarative model, and the skill is
finding it. Here&rsquo;s the whole table, then the four rows that took thought.</p>
<h2 id="the-mapping">The mapping</h2>
<table>
	<thead>
			<tr>
					<th>Script behaviour</th>
					<th>Where it lives now</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Environment menu (f01–f10)</td>
					<td><code>environment</code> input (enum)</td>
			</tr>
			<tr>
					<td>Version menu → OVA path on a share</td>
					<td><code>esxVersion</code> input → <strong>image mapping</strong> → content library item</td>
			</tr>
			<tr>
					<td>Role menu (management / workload / both)</td>
					<td><code>role</code> input; &ldquo;both&rdquo; = two requests</td>
			</tr>
			<tr>
					<td>Size menu / auto-detect from an existing host</td>
					<td><code>size</code> input (auto-detect dropped — see below)</td>
			</tr>
			<tr>
					<td>vCenter + ESXi credential prompts</td>
					<td>vCenter creds gone (cloud account); <code>esxiRootPassword</code> an encrypted input</td>
			</tr>
			<tr>
					<td>Next-free <code>esxNN</code> index scan (gap-filling)</td>
					<td>vRO <strong>action</strong> <code>getNextEsxHostIndexes</code>, bound to the request form</td>
			</tr>
			<tr>
					<td>VLAN / IP / gateway arithmetic</td>
					<td><strong>template expressions</strong> (same formulas)</td>
			</tr>
			<tr>
					<td><code>ovftool --prop:guestinfo.*</code></td>
					<td><code>ovfProperties</code> on <code>Cloud.vSphere.Machine</code></td>
			</tr>
			<tr>
					<td>Folder lookup / <code>New-Folder</code></td>
					<td><strong>allocation-phase subscription</strong> creates the folder if missing</td>
			</tr>
			<tr>
					<td><code>Set-VM</code> cpu / mem</td>
					<td><code>cpuCount</code> / <code>totalMemoryMB</code> in the template</td>
			</tr>
			<tr>
					<td>2× <code>New-NetworkAdapter</code> (Vmxnet3)</td>
					<td><code>networks</code> array, <code>deviceIndex</code> 0/1/2</td>
			</tr>
			<tr>
					<td>3× <code>Set-HardDisk</code> grow</td>
					<td><strong>post-provision subscription</strong></td>
			</tr>
			<tr>
					<td><code>NestedHVEnabled = $true</code></td>
					<td>post-provision subscription</td>
			</tr>
			<tr>
					<td>&ldquo;Power on after?&rdquo; prompt</td>
					<td><code>powerOn</code> input, honoured post-provision</td>
			</tr>
			<tr>
					<td>Summary table printed at the end</td>
					<td>the deployment view in the UI</td>
			</tr>
	</tbody>
</table>
<p>Five homes, in decreasing order of preference: <strong>input</strong>, <strong>template
expression</strong>, <strong>platform abstraction</strong> (image mapping, network profile,
cloud account), <strong>form action</strong> (read-only lookup at request time),
<strong>subscription</strong> (imperative work at a lifecycle stage). Push each
behaviour as far up that list as it will go.</p>
<h2 id="the-four-decisions-that-werent-obvious">The four decisions that weren&rsquo;t obvious</h2>
<h3 id="1-drop-auto-detect-the-platform-already-remembers">1. Drop auto-detect; the platform already remembers</h3>
<p>The script inspected an existing host to infer size. That was a workaround
for having no record. The catalog <em>is</em> the record — deployment history
shows what size every existing host was requested at — so the input
simply asks. Fewer moving parts, and the requester sees the choice.</p>
<h3 id="2-the-index-scan-is-a-form-action-not-a-workflow-step">2. The index scan is a form action, not a workflow step</h3>
<p>&ldquo;Next free <code>esx07</code>&rdquo; has to be known <em>at request time</em> so the requester
sees the names they&rsquo;ll get. That&rsquo;s a <strong>vRO action bound to the custom
form</strong> (<code>getNextEsxHostIndexes(environment, role, count)</code> → array of
strings), not a step inside provisioning. Forms can call actions; use it
for anything that&rsquo;s a lookup.</p>
<h3 id="3-rename-and-folder-go-in-compute-allocation-hardware-goes-in-post-provision">3. Rename and folder go in <em>Compute Allocation</em>, hardware goes in <em>Post Provision</em></h3>
<p>Two blocking subscriptions, filtered by a custom property on the template:</p>
<table>
	<thead>
			<tr>
					<th></th>
					<th>Subscription 1</th>
					<th>Subscription 2</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Topic</td>
					<td>Compute allocation</td>
					<td>Compute post provision</td>
			</tr>
			<tr>
					<td>Runnable</td>
					<td>&ldquo;Set VM Name &amp; Folder&rdquo;</td>
					<td>&ldquo;Finalize Hardware&rdquo;</td>
			</tr>
			<tr>
					<td>Does</td>
					<td>sets <code>resourceNames</code>, creates folder</td>
					<td>grows 3 disks, <code>NestedHVEnabled</code>, power on</td>
			</tr>
			<tr>
					<td>Timeout</td>
					<td>10 min</td>
					<td>30 min</td>
			</tr>
	</tbody>
</table>
<p>The rename <em>must</em> be at allocation — it&rsquo;s the only stage where a workflow
output named <code>resourceNames</code> is applied to the machine. Disk growth and
nested-HV need a VM that exists, so they wait for post-provision. Both
are blocking: the deployment doesn&rsquo;t proceed until they return.</p>
<h3 id="4-nestedesx-yes--not-true">4. <code>nestedEsx: 'yes'</code> — not <code>true</code></h3>
<p>The subscription condition is
<code>event.data.customProperties.nestedEsx == &quot;yes&quot;</code>. Why not <code>&quot;true&quot;</code>?
Because boolean-looking strings can arrive in the event payload as typed
booleans, and <code>true == &quot;true&quot;</code> is false in the condition evaluator <em>and</em>
in the vRO code. It fails silently — the subscription just never fires.
<code>yes</code> can&rsquo;t be coerced. Small thing; two hours.</p>
<h2 id="what-stayed-exactly-the-same">What stayed exactly the same</h2>
<p>The formulas. <code>10.(20+X).&lt;sub&gt;.0/24</code>, VLAN <code>2X0n</code>, gateway <code>.254</code> — they
were string concatenation in PowerShell and they&rsquo;re template expressions
now, character for character. The <code>guestinfo.*</code> property names — identical,
because the OVA didn&rsquo;t change. Porting a script well means most of it
survives; only the <em>plumbing</em> moves.</p>
<h2 id="the-bits-that-still-bite">The bits that still bite</h2>
<ul>
<li><strong>Network profile without IP ranges.</strong> Addressing is injected via
<code>guestinfo</code>, not the platform&rsquo;s IPAM. Tag the trunk portgroup, add no
ranges, or IPAM and guestinfo will disagree.</li>
<li><strong>Two template revisions in the repo.</strong> v1 is what the guide documents;
v2 grew later. Both kept deliberately, both labelled. Check which one
the org has <em>imported</em> before editing either.</li>
<li><strong>Content-source lag.</strong> A new or changed vRO action needs ~15–20 minutes
of data collection before the form sees it. Publish, wait, then test.</li>
<li><strong>Small disks and OSDATA.</strong> Nested hosts with 64 GB disks ship
ESX-OSDATA at essentially the whole disk. Templates now provision 128 GB;
a relocate-scratch script mitigates existing hosts.</li>
</ul>
<p><img alt="The Nested ESX request form: environment, version, role, size, count — and Host Indexes already computed by the form action" loading="lazy" src="/images/ui/f6-f00-nested-esx-form.jpg">
<em>Every menu prompt from the script is now a field; <code>Host Indexes</code> is the form action&rsquo;s answer to &ldquo;next free esxNN&rdquo;.</em></p>
<h2 id="why-this-matters-outside-the-lab">Why this matters outside the lab</h2>
<p>Almost every organisation has these scripts: valuable, trusted, and stuck
on one person&rsquo;s machine. The message of this post for them is that
modernising doesn&rsquo;t mean rewriting. The logic survives; what changes is
where it lives — behind a request form with access control, an audit
trail, consistent inputs and a deployment record. That&rsquo;s how a team turns
tribal knowledge into a service without losing the years of edge cases the
script already handles.</p>
<h2 id="rules-learned">Rules learned</h2>
<ul>
<li>Porting is <strong>sorting</strong>: input → expression → platform abstraction →
form action → subscription. Push each behaviour as far up as it goes.</li>
<li>Lookups the requester needs to <em>see</em> are <strong>form actions</strong>.</li>
<li>Rename at <strong>allocation</strong> (<code>resourceNames</code> output); hardware at
<strong>post-provision</strong>. Both blocking.</li>
<li>Filter subscriptions on a custom property, and make its value a word
that can&rsquo;t be coerced to a boolean.</li>
<li>Drop workarounds for missing state; the catalog is the state.</li>
<li>Keep the formulas. Move the plumbing.</li>
</ul>
<p><em>Part of <a href="/series/the-lab-factory/">The Lab Factory</a>. Next: <a href="/series/the-lab-factory/">driving the
VCF Installer API from vRO</a>.</em></p>
<hr>
<p><em>Lab environment; opinions my own.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Driving the VCF Installer API from vRO: generate, validate, start, walk away</title>
      <link>https://thenestedlab.com/posts/driving-the-vcf-installer-api-from-vro/</link>
      <pubDate>Wed, 16 Sep 2026 06:10:00 +0100</pubDate>
      <guid>https://thenestedlab.com/posts/driving-the-vcf-installer-api-from-vro/</guid>
      <description>Stage 2 of the lab factory: a vRO workflow that turns an environment number into a complete VCF 9.1 deployment spec, runs the installer&amp;rsquo;s own validation, starts bringup and hands back a task id — because the request dies long before the eight-hour build does. Plus how the wrapper slips the two-hour leash.</description>
      <content:encoded><![CDATA[<p>The <a href="/posts/porting-a-powershell-deploy-script/">nested hosts exist</a>. Now
they need to become a VCF instance: vCenter, NSX, SDDC Manager, the fleet
components. The VCF 9.1 Installer appliance does that from a deployment
spec — a few hundred lines of JSON — through an API. This post is the vRO
workflow that drives it, and the three design constraints that shaped it:
nobody edits the spec by hand, the request can&rsquo;t outlive two hours, and
nested hosts fail hardware validation.</p>
<p>Unlike stage 1 this isn&rsquo;t VM provisioning, so it&rsquo;s not a cloud template.
It&rsquo;s a <strong>vRO workflow published directly as a catalog item</strong> through an
Orchestrator content source.</p>
<h2 id="the-spec-is-generated-never-edited">The spec is generated, never edited</h2>
<p>A vRO action, <code>buildVcfDeploymentSpec(environment, hostFqdns, labPassword, …)</code>, returns the whole spec as a string. Its structure was reconciled
against a <em>validated</em> export from a real bringup — the installer UI lets
you export the spec it accepted — and everything variable derives from the
environment number X:</p>
<table>
	<thead>
			<tr>
					<th>Element</th>
					<th>Pattern</th>
					<th>f03</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Names</td>
					<td><code>f0X-m01-*</code></td>
					<td><code>f03-m01-vc01.res.lab</code></td>
			</tr>
			<tr>
					<td>Subnets</td>
					<td><code>10.(20+X).&lt;sub&gt;.0/24</code></td>
					<td><code>10.23.1.0/24</code> (mgmt)</td>
			</tr>
			<tr>
					<td>VLANs</td>
					<td><code>2X0&lt;sub&gt;</code></td>
					<td>2301 mgmt … 2306 TEP</td>
			</tr>
			<tr>
					<td>Gateways</td>
					<td><code>.254</code></td>
					<td><code>10.23.1.254</code></td>
			</tr>
			<tr>
					<td>vMotion / vSAN ranges</td>
					<td><code>.1–.16</code></td>
					<td><code>10.23.3.1-16</code></td>
			</tr>
			<tr>
					<td>NSX TEP pool</td>
					<td><code>.6.1–.6.32</code></td>
					<td><code>10.23.6.1-32</code></td>
			</tr>
			<tr>
					<td>SDDC Manager</td>
					<td><code>f0X-vcf01.res.lab</code></td>
					<td><code>f03-vcf01.res.lab</code></td>
			</tr>
	</tbody>
</table>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">n</span>   <span class="o">=</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">environment</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mi">1</span><span class="p">),</span> <span class="mi">10</span><span class="p">);</span>   <span class="c1">// &#34;f03&#34; -&gt; 3
</span></span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">pfx</span> <span class="o">=</span> <span class="nx">environment</span> <span class="o">+</span> <span class="s2">&#34;-m01&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="nx">net</span> <span class="o">=</span> <span class="s2">&#34;10.&#34;</span> <span class="o">+</span> <span class="p">(</span><span class="mi">20</span> <span class="o">+</span> <span class="nx">n</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">vlan</span><span class="p">(</span><span class="nx">o</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="mi">2000</span> <span class="o">+</span> <span class="p">(</span><span class="nx">n</span> <span class="o">*</span> <span class="mi">100</span><span class="p">)</span> <span class="o">+</span> <span class="nx">o</span><span class="p">;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">gw</span><span class="p">(</span><span class="nx">sub</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">net</span> <span class="o">+</span> <span class="s2">&#34;.&#34;</span> <span class="o">+</span> <span class="nx">sub</span> <span class="o">+</span> <span class="s2">&#34;.254&#34;</span><span class="p">;</span> <span class="p">}</span>
</span></span></code></pre></div><p>Static across environments: DNS, NTP, subdomain, component sizes, vSAN ESA
FTT=1, and the component build versions pinned to the installer binaries.
One lab password feeds every credential field (the UI export scrubs them;
the action puts them back per the API schema).</p>
<p>Two spec-level decisions worth stealing:</p>
<ul>
<li><code>skipEsxThumbprintValidation: true</code> instead of carrying per-host
<code>sslThumbprint</code>. Supported, and the right trade-off for a lab.</li>
<li>Ops and Automation are <strong>checkboxes</strong> that add their blocks to the spec
— and the installer only accepts a <code>licenseServerSpec</code> when Ops is
present, so the action adds them together or not at all.</li>
</ul>
<h2 id="the-workflow-authenticate--validate--start--return">The workflow: authenticate → validate → start → return</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">1. POST /v1/tokens                 installer login
</span></span><span class="line"><span class="cl">2. POST /v1/sddcs/validations      the installer&#39;s OWN pre-flight on the spec
</span></span><span class="line"><span class="cl">   (poll until COMPLETED; fail on any FAILED check)
</span></span><span class="line"><span class="cl">3. if validateOnly -&gt; return the validation report; touch nothing
</span></span><span class="line"><span class="cl">4. POST /v1/sddcs                  start bringup -&gt; sddcTaskId
</span></span><span class="line"><span class="cl">5. return { sddcTaskId, installerUrl }
</span></span></code></pre></div><p>Step 2 is the <a href="/posts/validateonly-everywhere/">validateOnly</a> story made
concrete: the installer will tell you, in seconds, that
<code>f03-m01-nsx01.res.lab</code> doesn&rsquo;t resolve, that an IP is in use, that a host
isn&rsquo;t reachable. Two hours into a bringup is a bad time to learn that.
Every one of the ~40 DNS records the pre-flight wants is created ahead of
time by a one-shot PowerShell script (<code>New-LabEnvDnsRecords.ps1</code>) — DNS is
a prerequisite, not a step.</p>
<h2 id="the-two-hour-leash-and-how-to-slip-it">The two-hour leash, and how to slip it</h2>
<p>A request from the catalog carries a token with a roughly <strong>two-hour</strong>
lifetime, and a bringup takes around <strong>eight</strong>. So by default the workflow
is fire-and-forget: <code>waitForCompletion=false</code>, return the task id, watch
progress in the installer UI. A <code>watchTaskId</code> input lets you re-attach
later and poll an already-running bringup from a new request.</p>
<p>The wrapper that chains <em>everything</em> — hosts, bringup, then the day-N
components that need bringup to be finished — has a neater trick. The
catalog-bound parent deploys the hosts, submits bringup, and then
<strong>re-executes itself as a plain vRO run</strong> (Orchestrator → Run, no catalog
token, no two-hour kill) carrying the hidden <code>bringupWatchTaskId</code>. That
continuation polls the installer task to completion — eight hours, fine —
and then runs certificates, fleet items, edge, supervisor and identity.
Watch it under <em>Orchestrator → Activity → Runs</em>. The catalog request
itself completes in under an hour — 47 minutes on the run pictured below — having
<em>started the work correctly and handed off</em>.</p>
<p><img alt="Orchestrator runs: the catalog-bound parent (13:12→14:00) and the continuation it spawned (14:00 → 01:56 next day)" loading="lazy" src="/images/ui/f5-f00-vro-runs-parent-continuation.jpg">
<em>Two rows, one build. The parent returns inside the catalog&rsquo;s window; the continuation waits out the bringup and does the day-N work.</em></p>
<p><img alt="The deployment&rsquo;s stackSummary output: hosts ready, bringup completed, continuation started — watch it in Orchestrator › Activity › Runs" loading="lazy" src="/images/ui/f4-f00-stack-outputs-continuation.jpg"></p>
<h2 id="nested-host-frictions">Nested-host frictions</h2>
<p>Three things a physical bringup never meets:</p>
<ul>
<li><strong>HCL validation vs virtual NVMe.</strong> The installer&rsquo;s hardware check
blocks the virtual NVMe controller. Fix at the vLCM layer:
<code>enforce_hcl_validation=false</code> on the image policy. The vSAN health test
<code>nvmeonhcl</code> also complains; silenced via the vSAN API, best-effort with
manual fallback.</li>
<li><strong>DVS compatibility appears late.</strong> After bringup, NSX takes 1–2 hours
to settle before the supervisor&rsquo;s zones endpoint stops returning 500.
If the supervisor stage fails &ldquo;No compatible DVS&rdquo; on a fresh instance,
wait and re-run just that item.</li>
<li><strong>TSM-SSH.</strong> Bringup wants SSH on the hosts; the wrapper enables it
host-direct via SOAP before submitting.</li>
</ul>
<h2 id="stale-schema-the-failure-that-looks-like-a-bug-and-isnt">Stale schema: the failure that looks like a bug and isn&rsquo;t</h2>
<p>Add an input to the vRO workflow after the catalog item exists and the
form will show the new field, the request will record its value, and the
workflow will receive <strong>null</strong> — Service Broker keeps the old request
schema until the content source re-imports. The workflow null-guards every
boolean and aborts with &ldquo;inputs not mapped&rdquo; rather than running with
silently-wrong options. Fix: re-import the content source, confirm the
schema, submit a <em>new</em> request (resubmitting an old one reuses the old
payload).</p>
<p>There are actually three async layers between &ldquo;publish&rdquo; and &ldquo;mappable
request&rdquo; — vRO processing the import, the catalog schema after re-import,
and the form service still enforcing the previous custom form for a minute
or two. Same symptom for all three. Check timing before assuming a bug.</p>
<h2 id="why-this-matters-outside-the-lab">Why this matters outside the lab</h2>
<p>Repeatable, generated VCF deployments matter well beyond a lab: a second
site, a disaster-recovery instance, a new business unit, an environment per
supported release. Generating the specification from a validated reference
removes the class of errors that comes from editing hundreds of lines of
JSON by hand, and running the installer&rsquo;s own validation first turns
&ldquo;find out in hour two&rdquo; into &ldquo;find out in minute one&rdquo;. It&rsquo;s the difference
between a VCF deployment being a project and being a procedure.</p>
<h2 id="rules-learned">Rules learned</h2>
<ul>
<li><strong>Generate the spec</strong> from a validated export plus one number. Nobody
hand-edits JSON at 2am.</li>
<li>Run the <strong>installer&rsquo;s own validation</strong> first, and make it a mode you
can request on its own.</li>
<li>Pre-create DNS. Enable SSH. Disable HCL enforcement on virtual NVMe.</li>
<li>Respect the request lifetime: <strong>start, return a task id, re-attach</strong>.
For a long chain, have the workflow re-run itself outside the catalog.</li>
<li>Null-guard every input and fail loud; stale schemas are a fact of life
after adding inputs.</li>
<li>On a fresh instance, give NSX an hour before you expect DVS
compatibility.</li>
</ul>
<p><em>Part of <a href="/series/the-lab-factory/">The Lab Factory</a>. Previously:
<a href="/posts/porting-a-powershell-deploy-script/">porting the host script</a>.</em></p>
<hr>
<p><em>Lab environment; opinions my own. Bringup verified end-to-end on a
rebuilt environment: 305/305 tasks, <code>COMPLETED_WITH_SUCCESS</code>.</em></p>
]]></content:encoded>
    </item>
  </channel>
</rss>
