<?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>The Lab Factory on The Nested Lab</title>
    <link>https://thenestedlab.com/series/the-lab-factory/</link>
    <description>Recent content in The Lab Factory on The Nested Lab</description>
    <generator>Hugo</generator>
    <language>en-gb</language>
    <lastBuildDate>Wed, 16 Sep 2026 06:40:00 +0100</lastBuildDate>
    <atom:link href="https://thenestedlab.com/series/the-lab-factory/index.xml" rel="self" type="application/rss+xml" />
    <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>validateOnly everywhere: plan mode for infrastructure</title>
      <link>https://thenestedlab.com/posts/validateonly-everywhere/</link>
      <pubDate>Wed, 16 Sep 2026 06:30:00 +0100</pubDate>
      <guid>https://thenestedlab.com/posts/validateonly-everywhere/</guid>
      <description>Terraform has plan. Kubernetes has &amp;ndash;dry-run. Your vRO workflows have nothing — unless you give them a validateOnly input and make the wrapper cascade it. A short argument for the single most valuable checkbox in the lab factory, with the failures it caught.</description>
      <content:encoded><![CDATA[<p>Terraform has <code>plan</code>. Kubernetes has <code>--dry-run=server</code>. Ansible has
<code>--check</code>. Every mature infrastructure tool grew a way to say &ldquo;tell me what
you&rsquo;d do, then don&rsquo;t&rdquo; — because the alternative is finding out at 2am, two
hours into a bringup, that a hostname doesn&rsquo;t resolve.</p>
<p>vRO workflows don&rsquo;t come with one. This is the case for adding it to every
single one you write, and cascading it through every wrapper.</p>
<h2 id="the-shape">The shape</h2>
<p>Every catalog item in <a href="/posts/one-catalog-item-one-vcf-instance/">the lab factory</a>
has a boolean input, <code>validateOnly</code>, default false. When true the workflow
does <em>everything it can without changing anything</em>:</p>
<ul>
<li>authenticate to every endpoint it would touch</li>
<li>resolve every name it would use, and fail on the ones that don&rsquo;t</li>
<li>generate every spec it would submit, and run the target&rsquo;s own validation
API on it where one exists (the VCF Installer has one; use it)</li>
<li>check for collisions — names, IPs, existing objects</li>
<li>report what it <em>would</em> have created, then return <code>CREATE_SUCCESSFUL</code></li>
</ul>
<p>The wrapper — the one form that chains hosts, bringup, supervisor, fleet
components, identity — has the same checkbox, and <strong>cascades</strong> it to every
component. Tick everything, tick validateOnly, request. Thirty seconds to
a few minutes later you have a full-stack plan against the <em>live</em>
environment, and nothing has moved.</p>
<h2 id="what-it-caught">What it caught</h2>
<p>Not hypothetically. On a built environment, the cascaded dry run of the
whole stack reported:</p>
<ul>
<li>edge cluster: <strong>already exists</strong> — correctly recorded, wrapper carried on</li>
<li>Ops for Logs: <strong>IP_IN_USE</strong> on the planned address — right, it&rsquo;s deployed</li>
<li>Ops for Networks: same</li>
<li>supervisor: the existing one would be reused; the per-service plan
listed which services were already active</li>
<li>identity: bind succeeded, group resolved, no changes needed</li>
</ul>
<p>That&rsquo;s a plan output. On a <em>fresh</em> environment the same run has caught, at
various times: a DNS record missing for one of ~40 required names (the
installer&rsquo;s own pre-flight found it, in seconds, instead of bringup
finding it in hour two); a stale content-library image ID; a form field
arriving <code>null</code> because a custom form hadn&rsquo;t finished re-importing — which
is a <em>publishing</em> bug the dry run surfaced before anyone requested
anything real.</p>
<h2 id="the-argument-against-answered">The argument against, answered</h2>
<p>&ldquo;It doubles the code.&rdquo; It doesn&rsquo;t — it moves the <code>if (!validateOnly)</code> guard
around the mutating call, and the validation logic is code you should have
had anyway. What it <em>does</em> force is separating &ldquo;compute what to do&rdquo; from
&ldquo;do it&rdquo;, which is how the workflows should have been structured in the
first place.</p>
<p>&ldquo;Some things can&rsquo;t be validated without doing them.&rdquo; True. Say so in the
result summary — &ldquo;would deploy X; no pre-validation available&rdquo; — rather
than skipping the item. Partial plans are still plans.</p>
<p>&ldquo;We have a test environment.&rdquo; You have <em>a</em> test environment. A dry run
against the <em>target</em> is what catches the collision with the thing that&rsquo;s
already there.</p>
<h2 id="make-it-the-smoke-test">Make it the smoke test</h2>
<p>The best consequence: a validateOnly request against a known environment
is a <strong>regression test for the automation itself</strong>, runnable on every
change. The factory&rsquo;s smoke runner does exactly this — request every item
with <code>validateOnly: true</code>, assert <code>CREATE_SUCCESSFUL</code>, diff the plan
summary against the last run. It takes minutes and it has caught more
bugs in the workflows than any amount of code review.</p>
<p><img alt="Deploy VCF Stack request form: one checkbox per component, and validateOnly" loading="lazy" src="/images/ui/f2-f00-stack-form-validateonly.jpg">
<em>The same form, real or dry-run. One checkbox decides.</em></p>
<h2 id="why-this-matters-outside-the-lab">Why this matters outside the lab</h2>
<p>For anyone who has sat through a failed change window, the value is
obvious: a full dry run against the <em>real</em> estate before anything moves.
Fewer failed changes, shorter windows, and a plan output that answers the
change board&rsquo;s questions before they&rsquo;re asked. It also gives auditors
something they rarely get from infrastructure automation — evidence of what
was going to happen, produced by the same tooling that then did it.</p>
<h2 id="rules-learned">Rules learned</h2>
<ul>
<li>Add <code>validateOnly</code> to <strong>every</strong> workflow. Default false. Wrappers
cascade it.</li>
<li>Dry-run does everything but mutate: auth, resolve, generate, call the
target&rsquo;s validator, check collisions, report.</li>
<li>Where a step truly can&rsquo;t be pre-validated, <em>say so</em> in the summary.
Never skip it silently.</li>
<li>A dry run against the real target is a plan. A dry run on every change
is a smoke test. Same checkbox.</li>
<li>The refactor it forces — compute, <em>then</em> act — is the one you wanted.</li>
</ul>
<p><em>Part of <a href="/series/the-lab-factory/">The Lab Factory</a>.</em></p>
<hr>
<p><em>Lab environment; opinions my own.</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>
