<?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>Powershell on The Nested Lab</title>
    <link>https://thenestedlab.com/tags/powershell/</link>
    <description>Recent content in Powershell on The Nested Lab</description>
    <generator>Hugo</generator>
    <language>en-gb</language>
    <lastBuildDate>Wed, 16 Sep 2026 06:20:00 +0100</lastBuildDate>
    <atom:link href="https://thenestedlab.com/tags/powershell/index.xml" rel="self" type="application/rss+xml" />
    <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>
  </channel>
</rss>
