<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>LucD notes</title>
	<atom:link href="https://www.lucd.info/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.lucd.info/</link>
	<description>My PowerShell ramblings</description>
	<lastBuildDate>Fri, 23 Sep 2022 16:32:41 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.9</generator>

<image>
	<url>https://www.lucd.info/wp-content/uploads/2018/12/cropped-120px-Tibetan_Dharmacakra-32x32.png</url>
	<title>LucD notes</title>
	<link>https://www.lucd.info/</link>
	<width>32</width>
	<height>32</height>
</image> 
<atom:link rel="hub" href="https://pubsubhubbub.appspot.com"/><atom:link rel="hub" href="https://pubsubhubbub.superfeedr.com"/><atom:link rel="hub" href="https://websubhub.com/hub"/>	<item>
		<title>A Hitchhikers Guide to SRS 1.0.0</title>
		<link>https://www.lucd.info/2020/12/23/a-hitchhikers-guide-to-srs-1-0-0/</link>
					<comments>https://www.lucd.info/2020/12/23/a-hitchhikers-guide-to-srs-1-0-0/#comments</comments>
		
		<dc:creator><![CDATA[LucD]]></dc:creator>
		<pubDate>Wed, 23 Dec 2020 14:08:53 +0000</pubDate>
				<category><![CDATA[Cloud-init]]></category>
		<category><![CDATA[Photon]]></category>
		<category><![CDATA[PowerCLI]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[REST API]]></category>
		<category><![CDATA[SRS]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[vSphere]]></category>
		<category><![CDATA[YAML]]></category>
		<category><![CDATA[Appliance]]></category>
		<guid isPermaLink="false">https://www.lucd.info/?p=7381</guid>

					<description><![CDATA[Sometimes announcements tend to disappear in the cracks of time. When the Script [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>Sometimes announcements tend to disappear in the cracks of time. When the <a href="https://github.com/vmware/script-runtime-service-for-vsphere/releases/tag/v1.0.0" target="_blank" rel="noopener">Script Runtime Service for vSphere (SRS) 1.0.0</a> was announced, I had the feeling just that happened.</p>
<p>When version <strong>1.0.0</strong> of this open-sourced (!) product was released, I had expected much more buzz on social media from <a href="https://code.vmware.com/web/tool/12.1.0/vmware-powercli" target="_blank" rel="noopener">VMware PowerCLI</a> users.&nbsp;</p>



<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="1000" height="426" src="https://www.lucd.info/wp-content/uploads/2020/12/SRS_1.png" alt="" class="wp-image-7400" srcset="https://www.lucd.info/wp-content/uploads/2020/12/SRS_1.png 1000w, https://www.lucd.info/wp-content/uploads/2020/12/SRS_1-300x128.png 300w, https://www.lucd.info/wp-content/uploads/2020/12/SRS_1-768x327.png 768w, https://www.lucd.info/wp-content/uploads/2020/12/SRS_1-720x307.png 720w" sizes="(max-width: 1000px) 100vw, 1000px" /></figure>



<p>This appliance does in fact bring an answer to a wish that many PowerShell/PowerCLI users have had for years: a &#8220;<em><strong>Scripting Host</strong></em>&#8220;!</p>



<p>This <strong>Hitchhikers Guide to SRS 1.0.0</strong> post will show how I build my own customised SRS appliance, and how I use it to run PowerShell/PowerCLI scripts.</p>



<span id="more-7381"></span>



<h2 class="wp-block-heading">Introduction</h2>



<p>When you visit the <a href="https://github.com/vmware/script-runtime-service-for-vsphere" target="_blank" rel="noopener">Script Runtime Service for vSphere (SRS) repository</a>, you&#8217;ll notice that this <strong>open-sourced</strong> project comes with extensive <strong>documentation</strong>.</p>



<p>Always a great characteristic for an open-sourced project!</p>



<p>The installation as a VM&nbsp; (from the <a href="https://github.com/vmware/script-runtime-service-for-vsphere/releases/tag/v1.0.0" target="_blank" rel="noopener">downloadable OVF files</a>) or in a Kubernetes cluster is well documented.</p>



<p>The available documentation in the SRS repo contains instructions on how to build, and customise, your own SRS appliance under the <a href="https://github.com/vmware/script-runtime-service-for-vsphere/blob/master/BUILD_AND_RUN.md" target="_blank" rel="noopener">Build and Run</a> page.</p>
<p>This blog post documents how I did exactly that.</p>
<p>I wanted to have a fully automated, self-documenting, and repeatable method, think CI, to roll out my own SRS station.</p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="892" src="https://www.lucd.info/wp-content/uploads/2020/12/SRS-Build-1024x892.png" alt="" class="wp-image-7405" srcset="https://www.lucd.info/wp-content/uploads/2020/12/SRS-Build-1024x892.png 1024w, https://www.lucd.info/wp-content/uploads/2020/12/SRS-Build-300x261.png 300w, https://www.lucd.info/wp-content/uploads/2020/12/SRS-Build-768x669.png 768w, https://www.lucd.info/wp-content/uploads/2020/12/SRS-Build-720x627.png 720w, https://www.lucd.info/wp-content/uploads/2020/12/SRS-Build.png 1394w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<ol class="wp-block-list"><li>Roll out the Builder station and install the prerequisites.</li><li>Start the Build of the SRS Appliance on the Builder station</li><li>Retrieve the OVF/OVA files, created from the SRS Appliance, that came out of the build</li><li>Use the OVF/OVA to deploy your SRS station</li></ol>



<h2 class="wp-block-heading">Creating the SRS Builder</h2>



<h3 class="wp-block-heading">The Prerequisites</h3>



<p>To create an SRS OVA/OVF yourself you need a &#8220;builder&#8221; station. And on that station there need to be a number of packages installed.&nbsp;</p>
<p>I included the installation of these packages in the &#8220;build&#8221; I describe in the next section.</p>
<p>The <a href="https://github.com/vmware/script-runtime-service-for-vsphere/blob/master/appliance/buildappliance.sh" target="_blank" rel="noopener">buildappliance.sh</a> script, which you will need to run on the builder station, will use the packages to setup and configure an SRS appliance (VMware Photon based), and as the last step will generate an OVA file from the appliance.</p>
<p>That OVA file can then be used to deploy your SRS VM.</p>



<p>This is also the place where you can install extra applications and extra PowerShell modules that will be included in your DIY SRS OVA.</p>



<h3 class="wp-block-heading">Create the Builder station</h3>



<h4>Preparation</h4>



<p>I used an <a href="https://cloud-images.ubuntu.com/bionic/current/" target="_blank" rel="noopener">Ubuntu 18.04 LTS</a> station as the Builder station. As the download of the ISO is also automated, I use the <strong>Daily Build</strong>.</p>



<pre class="lang:ps decode:true  ">
#region Get the Ubuntu Bionic Cloud Image OVA Daily Build

$uri = 'https://cloud-images.ubuntu.com/bionic/current/'
$ovaName = 'bionic-server-cloudimg-amd64.ova'
$repository = 'D:\Repository\Linux\Ubuntu\'

$currentOVA = Get-ChildItem -Path "$repository\$ovaName" -ErrorAction SilentlyContinue
if (-not $currentOVA -or $currentOVA.LastWriteTime.Date -lt $now.Date) {
  $sWeb = @{
    Uri = "$uri$ovaName"
    OutFile = "$repository$ovaName"
    Verbose = $false
  }
  Invoke-WebRequest @sWeb
  $currentOVA = Get-ChildItem -Path "$repository\$ovaName"
}
#endregion
</code></pre>



<p>I also check if the <strong>DNS A</strong>&nbsp;and <strong>PTR</strong> records for the Builder station are present in DNS. The Builder station I used has an FQDN of <strong>srsbuilder.local.lab</strong> with an IP address of <strong>192.168.10.88</strong>.</p>



<pre class="lang:ps decode:true  ">
$hostName = 'srsbuilder'
$domain = 'local.lab'
$ipAddress = '192.168.10.88'

$if = Get-Netadapter
$sDns = @{
   AddressFamily = 'IPv4'
   InterfaceIndex = $if.ifIndex
}
$dns = Get-DnsClientServerAddress @sDns
$dnsServer = $dns.ServerAddresses | Get-Random
$ip = $ipAddress.Split('.')
$reverseZone = "$($ip[2]).$($ip[1]).$($ip[0]).in-addr.arpa"

try{
  Write-Verbose (Get-LogText -Text "Check DNS A record for $hostname.$domain with $ipAddress")
  $sQDns = @{
    Name = "$hostName.$domain"
    Server = $dnsServer
    ErrorAction = 'Stop'
    Verbose = $false
  }
  Resolve-DnsName @sQDns  | Out-Null
  Write-Verbose (Get-LogText -Text "DNS A record exists for $hostname.$domain with $ipAddress")
}
catch{
  Write-Verbose (Get-LogText -Text "Create A record for $hostname.$domain with $ipAddress")
  $sADns = @{
    Name = $hostName
    ZoneName = $domain
    IPv4Address = $ipAddress
    A = $true
    CreatePtr = $true
    ComputerName = $dnsServer
  }
  Add-DnsServerResourceRecord @sADns | Out-Null
}
try {
  Write-Verbose (Get-LogText -Text "Check DNS PTR record for $hostname.$domain with $ipAddress")
  $sQdns = @{
    Name = $ipAddress
    Type = 'PTR'
    Server = $dnsServer
    Verbose = $false
    ErrorAction = 'Stop'
  }
  Resolve-DnsName @sQdns | Out-Null
  Write-Verbose (Get-LogText -Text "DNS PTR record exists for $hostname.$domain with $ipAddress")
}
catch {
  Write-Verbose (Get-LogText -Text "Create PTR record for $hostname.$domain with $ipAddress")
  $sADns = @{
    Name = "$($ip[3])"
    PtrDomainName = "$hostname.$domain"
    ZoneName = $reverseZone
    ComputerName = $dnsServer
  }
  Add-DnsServerResourceRecordPtr @sADns | Out-Null
}
</code></pre>



<h4>Deploy the Builder</h4>



<p>For the rollout of this Builder station, I used the Cloud-Init method I described in <a href="https://www.lucd.info/2019/12/07/cloud-init-part-2-advanced-ubuntu/" target="_blank" rel="noopener">Cloud-Init – Part 2 – Advanced Ubuntu</a>. I had to make some small changes to the <strong>Install-CloudInitVM</strong> function from that post, so I was able to specify the memory size and the disk size. The default sizes were insufficient to run the SRS Appliance build.</p>



<p>The updated function now has <strong>MemoryGB</strong> and <strong>DiskGB</strong> parameters.</p>



<p>I also had to add a snippet to handle the issue with the $PSScriptRoot parameter when running the code from the Visual Studio Code editor.</p>



<pre class="lang:ps decode:true  ">
function Install-CloudInitVM {
  <#
.SYNOPSIS
  Deploy a VM from an OVA file and use cloud-init for the configuration
  .DESCRIPTION
  This function will deploy an OVA file.
  The function transfer the user-data to the cloud-init process on the VM with
  one of the OVF properties.
.NOTES
  Author:  Luc Dekens
  Version:
  1.0 05/12/19  Initial release
.PARAMETER OvaFile
  Specifies the path to the OVA file
.PARAMETER VmName
  The displayname of the VM
.PARAMETER ClusterName
  The cluster onto which the VM shall be deployed
.PARAMETER DsName
  The datastore on which the VM shall be deployed
.PARAMETER PgName
  The portgroupname to which the VM shall be connected
.PARAMETER CloudConfig
  The path to the YAML file containing the user-data
.PARAMETER Credential
  The credentials for a user in the VM's guest OS
.EXAMPLE
  $sCloudInitVM = @{
    OvaFile = '.\bionic-server-cloudimg-amd64.ova'
    VmName = $vmName
    ClusterName = $clusterName
    DsName = $dsName
    PgName = $pgName
    CloudConfig = '.\user-data.yaml'
    Credential = $cred
  }
  Install-CloudInitVM @sCloudInitVM
#>

  [cmdletbinding()]
  param(
    [string]$OvaFile,
    [string]$VmName,
    [string]$ClusterName,
    [string]$DsName,
    [string]$PgName,
    [string]$CloudConfig,
    [PSCredential[]]$Credential,
    [int]$MemoryGB,
    [int]$DiskGB
  )

#region Bypass for known issue in VSC ()
# See https://github.com/PowerShell/vscode-powershell/issues/633

  if ($psISE) {
    $dir = Split-Path -Path $psISE.CurrentFile.FullPath
  }
  else {
    if ($profile -match "VScode") {
      $dir = Split-Path $psEditor.GetEditorContext().CurrentFile.Path
    }
    else {
      $dir = $PSScriptRoot
    }
  }
#endregion

  $waitJob = (Get-Command -Name "$dir\Wait-Job.ps1").ScriptBlock
  $userData = Get-Content -Path "$dir\$CloudConfig" -Raw

  Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Starting deployment of $vmName"

  $start = Get-Date

  $vm = Get-VM -Name $vmName -ErrorAction SilentlyContinue
  if ($vm) {
    Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Cleaning up"
    if ($vm.PowerState -eq 'PoweredOn') {
      Stop-VM -VM $vm -Confirm:$false | Out-Null
    }
    Remove-VM -VM $vm -DeletePermanently -Confirm:$false
  }

  $ovfProp = Get-OvfConfiguration -Ovf $ovaFile
  $ovfProp.Common.user_data.Value = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($userData))
  $ovfProp.NetworkMapping.VM_Network.Value = $pgName

  $sApp = @{
    Source = $ovaFile
    Name = $vmName
    Datastore = Get-Datastore -Name $dsName
    DiskStorageFormat = 'Thin'
    VMHost = Get-Cluster -Name $clusterName | Get-VMHost | Get-Random
    OvfConfiguration = $ovfProp
  }
  Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Importing OVA"
  $vm = Import-VApp @sApp

  if($MemoryGB){
    $vm = Set-VM -VM $vm -MemoryGB $MemoryGB -Confirm:$false
  }
  if($DiskGB){
    $hd = Get-HardDisk -VM $vm | Set-HardDisk -CapacityGB $DiskGB -Confirm:$false
  }

  Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Starting the VM"
  Start-VM -VM $vm -Confirm:$false -RunAsync | Out-Null

  Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Waiting for cloud-init to finish"

  $User = $Credential.GetNetworkCredential().UserName
  $Password = $Credential.GetNetworkCredential().Password

  $sJob = @{
    Name = 'WaitForCloudInit'
    ScriptBlock = $waitJob
    ArgumentList = $vm.Name, $User, $Password, $global:DefaultVIServer.Name, $global:DefaultVIServer.SessionId
  }
  Start-Job @sJob | Receive-Job -Wait

  Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Deployment complete"

  Write-Verbose "<code>nDeployment took $([math]::Round((New-TimeSpan -Start $start -End (Get-Date)).TotalSeconds,0)) seconds"
}
</code></pre>



<p>The call to the <strong>Install-CloudInitVM</strong> functions is rather straightforward.</p>



<pre class="lang:ps decode:true  ">
$vmName = 'SRSBuilder'

# Get credential for logging on to the guest
# Replace with your secrets manager application when
# running this on PSv6 or higher

$viCred = Get-VICredentialStoreItem -Host $vmName
$secPassword = ConvertTo-SecureString -String $viCred.Password -AsPlainText -Force
$cred = [Management.Automation.PSCredential]::new($viCred.User, $secPassword)

$sCloudInitVM = @{
  OvaFile = 'D:\Repository\Linux\Ubuntu\bionic-server-cloudimg-amd64.ova'
  VmName = $vmName
  ClusterName = 'cluster'
  DsName = 'vsanDatastore'
  PgName = 'vdPg1'
  CloudConfig = 'user-data-srsbuilder.yaml'
  Credential = $cred
  MemoryGB = 4
  DiskGB = 25
  Verbose = $true
}
Install-CloudInitVM @sCloudInitVM
</code></pre>



<p>Notice how I used the <strong>VICredentialStore</strong> to store and retrieve the credentials for the Builder station. When you run this from a PowerShell version greater than 5.1, you will have to replace that part with calls to the secrets manager of your choice.</p>



<p>The real strength of using this <strong>Cloud-Init</strong> method is that one can use a <strong>YAML</strong> file to specify how the target station, the Builder station, in this case, needs to be configured. And also specify which packages shall be installed on the target station as part of the deployment.</p>



<p>The following <strong>extract</strong> of my YAML file shows the packages that are required. It also includes pulling down the SRS appliance.</p>



<pre class="lang:yaml decode:true  ">
# SRS 1.0
# A) dotnet sdk
# 1) add the Microsoft package signing key
- wget https://packages.microsoft.com/config/ubuntu/18.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
# 2) add the package repository
- dpkg -i packages-microsoft-prod.deb
# 3) donet sdk
- apt-get update
- apt-get install -y apt-transport-https
- apt-get update
- apt-get install -y dotnet-sdk-3.1
# B) docker
- curl -fsSL https://get.docker.com -o get-docker.sh
- sh get-docker.sh
# C) PowerShell
- apt-get install -y wget apt-transport-https software-properties-common
- wget -q https://packages.microsoft.com/config/ubuntu/18.04/packages-microsoft-prod.deb
- dpkg -i packages-microsoft-prod.deb
- apt-get update
- add-apt-repository universe
- apt-get install -y powershell
# D) VMware PowerCLI
- pwsh -C '& {Install-Module -Name VMware.PowerCLI -Scope AllUsers -Force -Confirm:$false -AllowClobber}'
# E) OvfTool
- /root/ovftool-ftp.sh
- chmod 744 /root/VMware-ovftool-4.4.1-16812187-lin.x86_64.bundle
- /root/VMware-ovftool-4.4.1-16812187-lin.x86_64.bundle --eulas-agreed --required --console
# F) packer
- curl -fsSL https://apt.releases.hashicorp.com/gpg | apt-key add -
- apt-get update
- apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main"
- apt-get update
- apt-get install packer
# X) Additional modules
- pwsh -C '& {Install-Module -Name ImportExcel -Scope AllUsers -Force -Confirm:$false -AllowClobber}'
- pwsh -C '& {Install-Module -Name Posh-Ssh -Scope AllUsers -Force -Confirm:$false -AllowClobber}'
# Y) Clone SRS repo
- mkdir /root/SRS
- git clone https://github.com/vmware/script-runtime-service-for-vsphere /root/SRS
# End SRS 1.0
</code></pre>



<h4>Verify the Builder</h4>



<p>Once the Builder station is deployed, it is useful to check that everything the creation of the SRS Appliance needs, is present.</p>



<p>A <strong>word of warning</strong>, the following code includes all <strong>versions as hard-coded</strong>. This is not ideal, and especially for products that have a daily build, this will require updating the code each time. Since this was, at the time of writing this post, not my top priority, I postponed looking for a more portable solution to a later date.</p>



<pre class="lang:ps decode:true  ">
#region Helper functions
function Get-LogText {
  param([string]$Text)

  $dt = (Get-Date).ToString('yyyyMMdd HH:mm:ss.fff')
  $app = Split-Path -Path $MyInvocation.ScriptName -Leaf
  "$dt - $app - $Text"
}
#endregion

#region Preamble

$vmName = 'SrsBuilder'

$now = Get-Date

$viCredObj = Get-VICredentialStoreItem -Host $vmName
$sObj = @{
  TypeName = 'PSCredential'
  ArgumentList = $viCredObj.User,(ConvertTo-SecureString -String $viCredObj.Password -AsPlainText -Force)
}
$cred = New-Object @sObj

$vm = Get-VM -Name $vmName
#endregion

#region dotnet SDK
$check_dotnet = @'
dotnet --version
'@
$sInvoke = @{
  VM = $vm
  GuestCredential = $cred
  ScriptText = $check_dotnet
  ScriptType = 'bash'
}
$result = Invoke-VMScript @sInvoke
if($result.ScriptOutput.Trim("</code>n") -ne '3.1.404'){
  Write-Host (Get-LoGText -Text "Dotnet SDK installation failure") -ForegroundColor red
}
else{
  Write-Host (Get-LogText -Text "Dotnet SDK $($result.ScriptOutput.Trim("<code>n")) installation OK") -ForegroundColor green
}
#endregion

#region Docker
$check_docker = @'
docker --version
'@
$sInvoke = @{
  VM = $vm
  GuestCredential = $cred
  ScriptText = $check_docker
  ScriptType = 'bash'
}
$result = Invoke-VMScript @sInvoke
if ($result.ScriptOutput.Trim("</code>n") -ne 'Docker version 20.10.1, build 831ebea') {
  Write-Host (Get-LoGText -Text "Docker installation failure") -ForegroundColor red
} else {
  Write-Host (Get-LogText -Text "$($result.ScriptOutput.Trim("<code>n")) installation OK") -ForegroundColor green
}
#endregion

#region PowerShell v7
$check_PSv7 = @'
pwsh -C '& {$PSVersionTable.PSVersion.ToString()}'
'@
$sInvoke = @{
  VM = $vm
  GuestCredential = $cred
  ScriptText = $check_PSv7
  ScriptType = 'bash'
}
$result = Invoke-VMScript @sInvoke
if ($result.ScriptOutput.Trim("</code>n") -ne '7.1.0') {
  Write-Host (Get-LoGText -Text "PowerShell installation failure") -ForegroundColor red
} else {
  Write-Host (Get-LogText -Text "PowerShell $($result.ScriptOutput.Trim("<code>n")) installation OK") -ForegroundColor green
}
#endregion

#region VMware PowerCLI
$check_PowerCLI = @'
pwsh -C '& {(Get-Module -Name VMware.PowerCLI -ListAvailable).Version.ToString()}'
'@
$sInvoke = @{
  VM = $vm
  GuestCredential = $cred
  ScriptText = $check_PowerCLI
  ScriptType = 'bash'
}
$result = Invoke-VMScript @sInvoke
if ($result.ScriptOutput.Trim("</code>n") -ne '12.1.0.17009493') {
  Write-Host (Get-LoGText -Text "VMware PowerCLI installation failure") -ForegroundColor red
} else {
  Write-Host (Get-LogText -Text "VMware PowerCLI $($result.ScriptOutput.Trim("<code>n")) installation OK") -ForegroundColor green
}
#endregion

#region VMware ovftool
$check_ovftool = @'
ovftool --version
'@
$sInvoke = @{
  VM = $vm
  GuestCredential = $cred
  ScriptText = $check_ovftool
  ScriptType = 'bash'
}
$result = Invoke-VMScript @sInvoke
if ($result.ScriptOutput.Trim("</code>n") -ne 'VMware ovftool 4.4.1 (build-16812187)') {
  Write-Host (Get-LoGText -Text "VMware OVFTool installation failure") -ForegroundColor red
} else {
  Write-Host (Get-LogText -Text "$($result.ScriptOutput.Trim("<code>n")) installation OK") -ForegroundColor green
}
#endregion

#region Packer
$check_packer = @'
packer --version
'@
$sInvoke = @{
  VM = $vm
  GuestCredential = $cred
  ScriptText = $check_packer
  ScriptType = 'bash'
}
$result = Invoke-VMScript @sInvoke
if ($result.ScriptOutput.Trim("</code>n") -ne '1.6.6') {
  Write-Host (Get-LoGText -Text "Packer installation failure") -ForegroundColor red
} else {
  Write-Host (Get-LogText -Text "Packer $($result.ScriptOutput.Trim("<code>n")) installation OK") -ForegroundColor green
}
#endregion

#region Additional

#region ImportExcel
$check_ImportExcel = @'
pwsh -C '& {(Get-Module -Name ImportExcel -ListAvailable).Version.ToString()}'
'@
$sInvoke = @{
  VM = $vm
  GuestCredential = $cred
  ScriptText = $check_ImportExcel
  ScriptType = 'bash'
}
$result = Invoke-VMScript @sInvoke
if ($result.ScriptOutput.Trim("</code>n") -ne '7.1.1') {
  Write-Host (Get-LoGText -Text "ImportExcel installation failure") -ForegroundColor red
} else {
  Write-Host (Get-LogText -Text "ImportExcel $($result.ScriptOutput.Trim("<code>n")) installation OK") -ForegroundColor green
}
#endregion

#region Posh-Ssh
$check_PoshSSH = @'
pwsh -C '& {(Get-Module -Name Posh-SSH -ListAvailable).Version.ToString()}'
'@
$sInvoke = @{
  VM = $vm
  GuestCredential = $cred
  ScriptText = $check_PoshSSH
  ScriptType = 'bash'
}
$result = Invoke-VMScript @sInvoke
if ($result.ScriptOutput.Trim("</code>n") -ne '2.3.0') {
  Write-Host (Get-LoGText -Text "Posh-SSH installation failure") -ForegroundColor red
} else {
  Write-Host (Get-LogText -Text "Posh-SSH $($result.ScriptOutput.Trim("`n")) installation OK") -ForegroundColor green
}
#endregion

#endregion
</code></pre>



<h3>Run the Build</h3>



<p>When the Builder station is deployed, and we verified that all prerequisites are in place, we can start the deployment of the SRS Applicance. From which, if all goes well, the OVA file will be generated.</p>



<p>During my experimenting with building the SRS Appliance, I stumbled on two what I suspect are issues.</p>



<p>As a bypass I build the SRS Appliance for now with <strong>Photon V3 Rev 2</strong>.</p>



<p>I did not succeed in building the SRS Appliance with <strong>Photon V3 Rev 3</strong> image. I opened an Issue for that, and I&#8217;m curious to know if the issue is a Photon issue or something in my environment.</p>
<p>Another issue, which seems to be a real, known issue, is that you apparently can not use a <strong>Distributed Switch Portgroup</strong> to build the SRS Appliance. The underlying issue seems to stem from the <strong>Packer</strong> application, more specifically the <strong>VMware-Iso</strong> builder. This builder uses <strong>VNC</strong> to &#8216;talk&#8217; with the ESXi node on which the SRS Appliance is created.</p>



<p>There are two issues here:</p>
<ul>
<li>VNC is not included anymore in ESXi since 6.7</li>
<li>With the ESXi API you can not interact with a VDS</li>
</ul>



<p>The VNC issue is resolved by changing some Packer settings. But for the VDS issue, there is no solution besides switching to the vSphere-Iso builder in the Packer step, so I had to build the SRS Appliance on a <strong>VSS Portgroup</strong>.</p>



<p>I packaged the preparation steps and the start of the SRS Appliance build in a function, named <strong>Invoke-SRSBuild</strong>.</p>



<pre class="lang:ps decode:true  ">
function Invoke-SRSBuild {
  [CmdletBinding()]
  param(
    [Parameter(Mandatory)]
    [String]$BuilderName,
    [Parameter(Mandatory)]
    [PSCredential]$Credential,
    [Switch]$PrepOnly,
    [Switch]$BuildOnly,
    [Parameter(Mandatory)]
    [ValidateSet('V3Rev2', 'V3Rev3')]
    [String]$PhotonVersion,
    [Parameter(Mandatory)]
    [String]$Portgroup,
    [Switch]$PackerLog
  )

  #region Helper functions
  function Get-LogText {
    param([string]$Text)

    $dt = (Get-Date).ToString('yyyyMMdd HH:mm:ss.fff')
    $app = Split-Path -Path $MyInvocation.ScriptName -Leaf
    "$dt - $app - $Text"
  }

  function Remove-SRSFolder {
    Param(
      [Parameter(Mandatory = $true)]
      [VMware.VimAutomation.ViCore.Types.V1.DatastoreManagement.StorageResource]$Datastore
    )

    $dsBrowser = Get-View -Id $Datastore.ExtensionData.Browser
    $spec = New-Object -TypeName VMware.Vim.HostDatastoreBrowserSearchSpec
    $spec.MatchPattern = 'SRS_Appliance'
    $spec.Query = New-Object -TypeName Vmware.Vim.FolderFileQuery
    $spec.Details = New-Object -TypeName VMware.Vim.FileQueryFlags
    $result = $dsBrowser.SearchDatastore("[$($Datastore.Name)]", $spec)

    if ($result.File.Count -gt 0 -and $result.File[0]) {
      $dc = Get-VMHost -Datastore $Datastore | Get-Datacenter
      $fileMgr = Get-View FileManager
      $fileMgr.DeleteDatastoreFile("[$($Datastore.Name)] $($result.File[0].Path)", $dc.ExtensionData.MoRef)
    }
  }
  #endregion

  #region Bypass for known issue in VSC ()
  # See https://github.com/PowerShell/vscode-powershell/issues/633

  if ($psISE) {
    $dir = Split-Path -Path $psISE.CurrentFile.FullPath
  } else {
    if ($profile -match "VScode") {
      $dir = Split-Path $psEditor.GetEditorContext().CurrentFile.Path
    } else {
      $dir = $PSScriptRoot
    }
  }
  #endregion

  #region Preamble

  $vm = Get-VM -Name $BuilderName
  #endregion

  if (-not $BuildOnly.IsPresent) {

    #region SSH service
    $ssh = Get-VMHostService -VMHost $vm.VMHost | Where-Object { $_.Label -eq 'SSH' }
    if ($ssh.Running) {
      Write-Verbose (Get-LogText -Text "SSH is running on $($vm.VMHost.Name)")
    } else {
      Write-Verbose (Get-LogText -Text "Starting SSH on $($vm.VMHost.Name)")
      $ssh = Start-VMHostService -HostService $ssh -Confirm:$false
      if (-not $ssh.Running) {
        Write-Verbose (Get-LogText -Text "Could not start SSH on $($vm.VMHost.Name)")
        throw "SSH service not started on $($vm.VMHost.Name)"
      }
    }
    #endregion

    #region GuestIPHack
    $esxcli = Get-EsxCli -VMHost $vm.VMHost -V2
    $sOpt = @{
      option = '/Net/GuestIPHack'
    }
    $opt = $esxcli.system.settings.advanced.list.Invoke($sOpt)
    if ($opt.IntValue -eq 1) {
      Write-Verbose (Get-LogText -Text "GuestIPHack setting is correct on $($vm.VMHost.Name)")
    } else {
      Write-Verbose (Get-LogText -Text "GuestIPHack setting is not correct on $($vm.VMHost.Name)")
      $sOpt.Add('intvalue', [long]1)
      if ($esxcli.system.settings.advanced.set.Invoke($sOpt)) {
        Write-Verbose (Get-LogText -Text "GuestIPHack setting corrected on $($vm.VMHost.Name)")
      } else {
        Write-Verbose (Get-LogText -Text "Could not change GuestIPHack setting on $($vm.VMHost.Name)")
        throw "Could net set GuestIPHack"
      }
    }
    #endregion

    #region Make sure folder doesn't exist
    $ds = Get-Datastore -RelatedObject $vm
    Remove-SRSFolder($ds)
    #endregion

    #region Update photon-build.json
    # Suspected issue when using a vdPortgroup

    $jsonFile = New-TemporaryFile
    $photonBuilder = @{
      builder_host = $vm.VMHost.Name
      builder_host_username = $viCredObj.User
      builder_host_password = $viCredObj.Password
      builder_host_datastore = (Get-Datastore -RelatedObject $vm).Name
      builder_host_portgroup = $Portgroup
    }
    $photonBuilder | ConvertTo-Json | Set-Content -Path $jsonFile
    $sCopy = @{
      VM = $vm
      GuestCredential = $Credential
      LocalToGuest = $true
      Source = $jsonFile
      Destination = '/root/SRS/appliance/photon-builder.json'
      Force = $true
      Confirm = $false
    }
    Copy-VMGuestFile @sCopy
    Remove-Item -Path $jsonFile -Confirm:$false
    #endregion

    #region Update photon-version.json
    $photonTab = Get-Content -Path "$dir\Photon-version.json" | ConvertFrom-Json
    $photonSelected = $photonTab.Photon | Where-Object { $_.Label -eq $PhotonVersion }
    $jsonFile = New-TemporaryFile
    $sCopy = @{
      VM = $vm
      GuestCredential = $Credential
      GuestToLocal = $true
      Source = '/root/SRS/appliance/photon-version.json'
      Destination = $jsonFile
      Force = $true
      Confirm = $false
    }
    Copy-VMGuestFile @sCopy
    $photonBuilder = (Get-Content -Path $jsonFile | ConvertFrom-Json)[0]
    if ($photonBuilder.iso_url -ne $photonSelected.Uri -or $photonBuilder.iso_checksum -ne $photonSelected.CheckSum) {
      $photonBuilder.iso_url = $photonSelected.Uri
      $photonBuilder.iso_checksum = $photonSelected.CheckSum
      @($photonBuilder) | ConvertTo-Json | Set-Content -Path $jsonFile
      $sCopy = @{
        VM = $vm
        GuestCredential = $Credential
        LocalToGuest = $true
        Source = $jsonFile
        Destination = '/root/SRS/appliance/photon-version.json'
        Force = $true
        Confirm = $false
      }
      Copy-VMGuestFile @sCopy
      Remove-Item -Path $jsonFile -Confirm:$false

    }
    #endregion

    #region Handle VNC for ESXi 6.7 and later
    if ([Version]$vm.VMHost.Version -ge [Version]'6.7.0') {
      $updateJSON = 'sed' +
      ' -i.bak -e ''/vnc_disable_password/ i \      "vnc_over_websocket": true,''' +
      ' -e ''/vnc_disable_password/ i \      "insecure_connection": true,''' +
      ' -e ''/vnc_disable_password/d'' ~/SRS/appliance/photon.json'

      $sInvoke = @{
        VM = $vm
        GuestCredential = $Credential
        ScriptText = $updateJSON
        ScriptType = 'bash'
      }
      $result = Invoke-VMScript @sInvoke
    }
    #endregion

  }

  if (-not $PrepOnly.IsPresent) {

    #region Get PowerCLI folder path
    $findDir = 'pwsh -C ''& {Split-Path -Path (Split-Path -Path (Get-Module -Name VMware.PowerCLI -ListAvailable).ModuleBase)}'''
    $sInvoke = @{
      VM = $vm
      GuestCredential = $Credential
      ScriptText = $findDir
      ScriptType = 'bash'
    }
    $pcliPath = (Invoke-VMScript @sInvoke).ScriptOutput
    #endregion

    #region Start build
    $sInvoke = @{
      VM = $vm
      GuestCredential = $Credential
      ScriptText = "/root/SRS/build.sh $pcliPath"
      ScriptType = 'bash'
    }
    if ($PackerLog.IsPresent) {
      $packerLogName = 'packer.log'
      $sInvoke.ScriptText = "export PACKER_LOG=1;export PACKER_LOG_PATH='$($packerLogName)';$($sInvoke.ScriptText)"
    }
    $buildResult = Invoke-VMScript @sInvoke
    $buildResult.ScriptOutput > "$dir\Buildlog.txt"
    #endregion

    #region Rettrieve Packer log
    if($PackerLog.IsPresent){
      $sCopy = @{
        VM = $vm
        GuestCredential = $Credential
        GuestToLocal = $true
        Source = "/root/SRS/appliance/$($packerLogName)"
        Destination = "$dir\$($packerLogName)"
        Force = $true
        Confirm = $false
      }
      Copy-VMGuestFile @sCopy
    }
    #endregion

    #region Clean up
    # Packer leaves an orphaned entry in the VCSA behind

    $endpointVM = Get-VM -Name 'SRS_Appliance' -ErrorAction SilentlyContinue
    if($endpointVM){
      while($endpointVM.PowerState -ne 'PoweredOff'){
        sleep2
        $endpointVM = Get-VM -Name 'SRS_Appliance' -ErrorAction SilentlyContinue
      }
      Remove-VM -VM $endpointVM   -DeletePermanently -ErrorAction SilentlyContinue -Confirm:$false
    }
    #endregion
  }
}
</code></pre>



<p>The call to the <strong>Invoke-SRSBuilder</strong> function is again straight-forward.</p>



<pre class="lang:ps decode:true  ">
$vmName = 'SRSBuilder'

#region Credential
# Get the credentials for the SRS Builder station
# Alternative method required when using PSv6 or later

$viCredObj = Get-VICredentialStoreItem -Host $vmName
$sObj = @{
  TypeName = 'PSCredential'
  ArgumentList = $viCredObj.User, (ConvertTo-SecureString -String $viCredObj.Password -AsPlainText -Force)
}
$cred = New-Object @sObj
#endregion

$sBuild = @{
  BuilderName = $vmName
  Credential = $cred
  PhotonVersion = 'V3Rev2'
  Portgroup = 'PG1'
  PackerLog = $true
}
Invoke-SRSBuild @sBuild
</code></pre>



<h3 class="wp-block-heading">The OVA</h3>



<p>When the call to the <strong>build.sh</strong> script completes successfully, there will be <strong>OVA/OVF</strong> files created on the Builder station.</p>
<p>Since the Builder station is, in my setup, a temporary station, I need to download those files to a more permanent location. The following snippet uses the <strong>Get-ScpFile</strong> cmdlet from the <a href="https://www.powershellgallery.com/packages/Posh-SSH" target="_blank" rel="noopener">Posh-Ssh</a> module to do that.</p>



<pre class="lang:ps decode:true  ">
#requires -Modules Posh-Ssh

#region Preamble

$vmName = 'SrsBuilder'

$now = Get-Date

$viCredObj = Get-VICredentialStoreItem -Host $vmName
$sObj = @{
  TypeName = 'PSCredential'
  ArgumentList = $viCredObj.User, (ConvertTo-SecureString -String $viCredObj.Password -AsPlainText -Force)
}
$cred = New-Object @sObj

$vm = Get-VM -Name $vmName

$fqdn = (Resolve-DnsName -Name $vm.ExtensionData.Guest.IpAddress).NameHost
#endregion

#region Download OVA
$sSCP = @{
  ComputerName = $fqdn
  Credential = $cred
  RemoteFile = '/root/SRS/appliance/output-vmware-iso/SRS_Appliance_1.0.0.ova'
  LocalFile = 'D:\OVA\SRS\V1.0.0\Rev2-VSS\SRS_Appliance_1.0.0.ova'
  AcceptKey = $true
}
Get-SCPFile @sSCP
#endregion
</code></pre>



<h2 class="wp-block-heading">Deploy SRS</h2>



<p>Once we have the OVA available, it is a piece of cake to deploy our SRS VM with the <a href="https://vdc-repo.vmware.com/vmwb-repository/dcr-public/f17594eb-bbe1-44a7-b7ac-b2da546936c2/21da7740-996d-481e-83e4-05d8fa7db18d/doc/Get-OvfConfiguration.html" target="_blank" rel="noreferrer noopener">Get-OvfConfiguration</a> and <a href="https://vdc-repo.vmware.com/vmwb-repository/dcr-public/f17594eb-bbe1-44a7-b7ac-b2da546936c2/21da7740-996d-481e-83e4-05d8fa7db18d/doc/Import-VApp.html" target="_blank" rel="noreferrer noopener">Import-VApp</a> cmdlets.</p>



<p>Note that deploying the SRS OVA to a Distributed Switch Portgroup is perfectly possible. The VDS issue mentioned earlier only comes into play when Packer with the VMware-Iso builder is involved.&nbsp;</p>



<pre class="lang:ps decode:true  ">
$vmName = 'srs1'
$clusterName = 'cluster'
$numCPU = 2
$memoryGB = 4
$dsName = 'vsanDatastore'
$harddiskGB = 2
$ovaPath = 'D:\OVA\SRS\V1.0.0\Rev2-VSS\SRS_Appliance_1.0.0.ova'
$networkName = 'vdPg1'

$cluster = Get-Cluster -Name $clusterName
$esx = Get-VMHost -Location $cluster | Get-Random
$ds = Get-Datastore -Name $dsName
$ovfProp = Get-OvfConfiguration -Ovf $ovaPath
$ovfProp.Common.guestinfo.hostname.Value = $vmName
$ovfProp.Common.guestinfo.ipaddress.Value = '192.168.10.43'
$ovfProp.Common.guestinfo.netmask.Value = '24'
$ovfProp.Common.guestinfo.gateway.Value = '192.168.10.1'
$ovfProp.Common.guestinfo.root_password.Value = 'Welcome2020!'
$ovfProp.Common.guestinfo.dns.Value = '192.168.10.2'
$ovfProp.Common.guestinfo.domain.Value = 'local.lab'
$ovfProp.Common.srs.vcaddress.Value = 'vcsa7.local.lab'
$ovfProp.Common.srs.vcpassword.Value = 'Welcome2020!'
$ovfProp.Common.srs.vcuser.Value = 'administrator@vsphere.local'
$ovfProp.Common.srs.vcthumbprint.Value = '5bf3246fde90fd3b7ab84144f50105114c2826e1'
$ovfProp.NetworkMapping.PG1.Value = $networkName

$vm = Import-VApp -Name $vmName -Source $ovaPath -VMHost $esx -OvfConfiguration $ovfProp -Datastore $ds
Start-VM -VM $vm -Confirm:$false

</code></pre>



<p>Your first test to see if the deployment went ok, is to try and access the swagger page on your SRS VM. The URI is <strong>https://&lt;your-SRS-FQDN&gt;/swagger</strong>. When all goes well, you will see a page like this.</p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="797" src="https://www.lucd.info/wp-content/uploads/2020/12/swagger-1024x797.png" alt="" class="wp-image-7437" srcset="https://www.lucd.info/wp-content/uploads/2020/12/swagger-1024x797.png 1024w, https://www.lucd.info/wp-content/uploads/2020/12/swagger-300x234.png 300w, https://www.lucd.info/wp-content/uploads/2020/12/swagger-768x598.png 768w, https://www.lucd.info/wp-content/uploads/2020/12/swagger-720x560.png 720w, https://www.lucd.info/wp-content/uploads/2020/12/swagger.png 1471w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading">Using SRS</h2>



<p>Now we can start using the SRS server to run our scripts. The way to do that is quite simple. The complete process is described in the <a href="https://github.com/vmware/script-runtime-service-for-vsphere/wiki/Run-Scripts" target="_blank" rel="noopener">Run Scripts</a> section in the SRS repository.</p>



<h3 class="wp-block-heading">A Simple Sample</h3>



<p>My test installation of SRS is done on a VM that is named <strong>SRS1</strong>.</p>
<p>A basic REST API call to verify that everything is working, is to call the <strong>api/about</strong> method.</p>



<pre class="lang:ps decode:true  ">
function Invoke-SrsMethod {
  [cmdletbinding()]
  param(
    [Parameter(Mandatory)]
    [String]$FQDN,
    [Parameter(Mandatory)]
    [String]$API,
    [Parameter(Mandatory)]
    [String]$Method
  )

  $sWeb = @{
    Uri = "https://$($FQDN)/$($API)"
    Method = $Method
  }

  try {
    $result = Invoke-WebRequest @sWeb
  }
  catch [System.Net.WebException] {
    switch ($error[0].Exception.Status) {
      ([System.Net.WebExceptionStatus]::TrustFailure) {
        if ($PSVersionTable.PSVersion.Major -lt 6) {
          if (-not ([System.Management.Automation.PSTypeName]"TrustAllCertsPolicy").Type) {
            Add-Type -TypeDefinition @"
using System.Net;
using System.Security.Cryptography.X509Certificates;
public class TrustAllCertsPolicy : ICertificatePolicy {
    public bool CheckValidationResult(
        ServicePoint srvPoint, X509Certificate certificate,
        WebRequest request, int certificateProblem)
    {
        return true;
    }
}
"@
          }
          if ([System.Net.ServicePointManager]::CertificatePolicy.ToString() -ne "TrustAllCertsPolicy") {
            [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
          }
        } else {
          $sWeb.Add('SkipCertificateCheck', $true)
        }
        $result = Invoke-WebRequest @sWeb
      }
      Default {
        Write-Error "Unhandled WebException $($error[0].Exception.Status)"
      }
    }
  }
  catch {
    Write-Error "Unhandled exception code $($error[0].Exception.gettype().Name)"
  }

  switch ($result.StatusCode) {
    200 {
      $result
    }
    Default {
      Write-Error "Unhandled StatusCode $($result.StatusCode)"
    }
  }
}

$vmName = 'srs1'

$vm = Get-VM -Name $vmName
$fqdn = (Resolve-DnsName -Name $vm.ExtensionData.Guest.IpAddress).NameHost

$about = Invoke-SrsMethod -FQDN $fqdn -API 'api/about' -Method 'Get'
$about.Content | ConvertFrom-Json
</code></pre>



<p>You notice that I created a function Invoke-SRSMethod instead of just calling the Invoke-WebRequest cmdlet directly. The reason for creating that function is that I needed to be able to bypass invalid certificates.</p>
<p>With PSv6 that has become easy, since the <a href="https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/invoke-webrequest?view=powershell-7.1" target="_blank" rel="noopener">Invoke-WebRequest</a> cmdlet now has the <strong>SkipCertificateCheck</strong> switch.&nbsp;</p>
<p>But I wanted to have a function that would also work in a PSV5.1 environment, hence the function and the logic in there.</p>
<p>When all goes well, that snippet should return the following.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="672" height="91" src="https://www.lucd.info/wp-content/uploads/2020/12/srs-call.png" alt="" class="wp-image-7442" srcset="https://www.lucd.info/wp-content/uploads/2020/12/srs-call.png 672w, https://www.lucd.info/wp-content/uploads/2020/12/srs-call-300x41.png 300w" sizes="auto, (max-width: 672px) 100vw, 672px" /></figure>



<h3>A PowerCLI example</h3>



<p>The full sequence for running code on the SRS station is perfectly explained in the <a href="https://github.com/vmware/script-runtime-service-for-vsphere/wiki/Run-Scripts" target="_blank" rel="noopener">Run Scripts</a> section on the <a href="https://github.com/vmware/script-runtime-service-for-vsphere" target="_blank" rel="noopener">SRS repository</a>.</p>



<p>A full, scripted example, excluding the earlier <strong>Invoke-SRSMethod</strong> function, could look like this.</p>



<pre class="lang:ps decode:true  ">
#region Preamble
$vmName = 'srs1'

$vm = Get-VM -Name $vmName
$fqdn = (Resolve-DnsName -Name $vm.ExtensionData.Guest.IpAddress).NameHost

# Get credential for logging on to the guest OS
$viCred = Get-VICredentialStoreItem -Host $global:DefaultVIServer.Name
$secPassword = ConvertTo-SecureString -String $viCred.Password -AsPlainText -Force
$cred = [Management.Automation.PSCredential]::new($viCred.User, $secPassword)

$headers = @{
  "accept" = "application/json"
  "content-type" = "application/json"
}
#endregion

#region Login
$sLogon = @{
  FQDN = $fqdn
  API = '/api/auth/login'
  Method = 'Post'
  Credential = $cred
  Headers = $headers
}
$logon = Invoke-SrsMethod @sLogon
$headers.Add('X-SRS-API-KEY', $logon.Headers['X-SRS-API-KEY'])
#endregion

#region Create Runspace
$sRSCreate = @{
  FQDN = $fqdn
  API = '/api/runspaces'
  Method = 'Post'
  Credential = $cred
  Headers = $headers
  Body = @{
    name = 'MyRS'
    run_vc_connection_script = $true
  }
}
$createRS = Invoke-SrsMethod @sRSCreate
$rs = $createRS.Content | ConvertFrom-Json
#endregion

#region Wait till RS is ready
while ($rs.state -eq 'Creating') {
  $sRSGet = @{
    FQDN = $fqdn
    API = "/api/runspaces/$($rs.Id)"
    Method = 'Get'
    Headers = $headers
  }
  $getRS = Invoke-SrsMethod @sRSGet
  $rs = $getRS.Content | ConvertFrom-Json
}
#endregion

#region Run Script
$code = {
  Get-VM
}
$sSCRCreate = @{
  FQDN = $fqdn
  API = '/api/script-executions'
  Method = 'Post'
  Headers = $headers
  Body = @{
    runspace_id = $rs.id
    name = 'MyScript'
    script = $code.ToString()
    script_parameters = @()
  }
}
$createSCR = Invoke-SrsMethod @sSCRCreate
$scr = $createSCR.Content | ConvertFrom-Json
#endregion

#region Wait for Script to end
while($scr.state -eq 'running'){
$sSCRCreate = @{
  FQDN = $fqdn
  API = "/api/script-executions/$($scr.id)"
  Method = 'Get'
  Headers = $headers
}
$getSCR = Invoke-SrsMethod @sSCRCreate
$scr = $getSCR.Content | ConvertFrom-Json
}
#endregion

#region Retrieve Script output
$sSCROut = @{
  FQDN = $fqdn
  API = "/api/script-executions/$($scr.id)/output"
  Method = 'Get'
  Headers = $headers
}
$outSCR = Invoke-SrsMethod @sSCROut
$outSCR.Content | ConvertFrom-Json
#endregion

#region Retrieve Script streams
'information', 'error', 'warning', 'debug', 'verbose' |
ForEach-Object -Process {
  $sSCRStr = @{
    FQDN = $fqdn
    API = "/api/script-executions/$($scr.id)/streams/$($_)"
    Method = 'Get'
    Headers = $headers
  }
  $getStream = Invoke-SrsMethod @sScrStr
  if($getStream.Content -ne '[]'){
    Write-Host "--- $_ ---" -ForegroundColor Green
    $getStream.Content | ConvertFrom-Json | Out-Default
    Write-Host "------------" -ForegroundColor Green
  }
}
#endregion

#region Remove Runspace
$sRSDel = @{
  FQDN = $fqdn
  API = "/api/runspaces/$($rs.id)"
  Method = 'Delete'
  Headers = $headers
}
$rsDel = Invoke-SrsMethod @sRSDel
$rsDel.Content | ConvertFrom-Json
#endregion

#region Logout
$sLogout = @{
  FQDN = $fqdn
  API = "/api/auth/logout"
  Method = 'Post'
  Headers = $headers
}
$srsLogout = Invoke-SrsMethod @sLogout
#endregion
</code></pre>



<h3 class="wp-block-heading">A Function</h3>



<p>That turned out to be a whole lot of code to just run a simple Get-VM.</p>



<p>But since we will be using most of the time the same logic, we can easily turn that into a function. That will make submitting code to run on the SRS a lot simpler.</p>



<p>And while we are at it, let&#8217;s include an option to run the code from an existing .ps1 file.</p>



<pre class="lang:ps decode:true  ">
Function Invoke-SRSScript {
  [cmdletbinding()]
  param(
    [Parameter(Mandatory = $true)]
    [String]$SRSHost,
    [Parameter(ParameterSetName = 'ScriptBlock', Mandatory = $true)]
    [scriptblock]$Code,
    [Parameter(ParameterSetName = 'ScriptFile', Mandatory = $true)]
    [String]$Path,
    [Parameter(Mandatory = $true)]
    [PSCredential]$VCCredential
  )

  #region Helper functions
  function Invoke-SrsMethod {
    [cmdletbinding()]
    param(
      [Parameter(Mandatory)]
      [String]$FQDN,
      [Parameter(Mandatory)]
      [String]$API,
      [Parameter(Mandatory)]
      [String]$Method,
      [PSCredential]$Credential,
      [PSObject]$Headers,
      [PSObject]$Body
    )

    $sWeb = @{
      Uri = "https://$($FQDN)$($API)"
      Method = $Method
    }
    if ($Credential) {
      $sWeb.Add('Credential', $Credential)
    }
    if ($Headers) {
      $sWeb.Add('Headers', $Headers)
    }
    if ($Body) {
      $sWeb.Add('Body', ($Body | ConvertTo-Json))
    }

    try {
      $result = Invoke-WebRequest @sWeb
    } catch [System.Net.WebException] {
      switch ($error[0].Exception.Status) {
        ([System.Net.WebExceptionStatus]::TrustFailure) {
          if ($PSVersionTable.PSVersion.Major -lt 7) {
            if (-not ([System.Management.Automation.PSTypeName]"TrustAllCertsPolicy").Type) {
              Add-Type -TypeDefinition @"
using System.Net;
using System.Security.Cryptography.X509Certificates;
public class TrustAllCertsPolicy : ICertificatePolicy {
    public bool CheckValidationResult(
        ServicePoint srvPoint, X509Certificate certificate,
        WebRequest request, int certificateProblem)
    {
        return true;
    }
}
"@
            }
            if ([System.Net.ServicePointManager]::CertificatePolicy.ToString() -ne "TrustAllCertsPolicy") {
              [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
            }
          } else {
            $sWeb.Add('SkipCertificateCheck', $true)
          }
          $result = Invoke-WebRequest @sWeb
        }
        Default {
          Write-Error "Unhandled WebException $($error[0].Exception.Status)"
        }
      }
    } catch {
      Write-Error "Unhandled exception code $($error[0].Exception.gettype().Name)"
      $error[0].Exception | Format-Custom
    }

    switch ($result.StatusCode) {
      200 {
        $result
      }
      202 {
        $result
      }
      401 {
        Write-Error "Unauthorized $($result.StatusCode)"
      }
      500 {
        Write-Error "Server error $($result.StatusCode)"
      }
      Default {
        Write-Error "Unhandled StatusCode $($result.StatusCode)"
      }
    }
  }
  #endregion

  #region Preamble

  $vm = Get-VM -Name $SRSHost
  $fqdn = (Resolve-DnsName -Name $vm.ExtensionData.Guest.IpAddress).NameHost

  switch ($PSCmdlet.ParameterSetName){
    'ScriptBlock' {
      $strCode = $Code.ToString()
    }
    'ScriptFile' {
      $strCode = Get-Content -Path $Path | Out-String
    }
  }

  $headers = @{
    "accept" = "application/json"
    "content-type" = "application/json"
  }
  #endregion

  #region Login
  $sLogon = @{
    FQDN = $fqdn
    API = '/api/auth/login'
    Method = 'Post'
    Credential = $VCCredential
    Headers = $headers
  }
  $logon = Invoke-SrsMethod @sLogon
  $headers.Add('X-SRS-API-KEY', $logon.Headers['X-SRS-API-KEY'])
  #endregion

  #region Create Runspace
  $sRSCreate = @{
    FQDN = $fqdn
    API = '/api/runspaces'
    Method = 'Post'
    Credential = $cred
    Headers = $headers
    Body = @{
      name = 'MyRS'
      run_vc_connection_script = $true
    }
  }
  $createRS = Invoke-SrsMethod @sRSCreate
  $rs = $createRS.Content | ConvertFrom-Json
  #endregion

  #region Wait till RS is ready
  while ($rs.state -eq 'Creating') {
    $sRSGet = @{
      FQDN = $fqdn
      API = "/api/runspaces/$($rs.Id)"
      Method = 'Get'
      Headers = $headers
    }
    $getRS = Invoke-SrsMethod @sRSGet
    $rs = $getRS.Content | ConvertFrom-Json
  }
  #endregion

  #region Run Script
  $sSCRCreate = @{
    FQDN = $fqdn
    API = '/api/script-executions'
    Method = 'Post'
    Headers = $headers
    Body = @{
      runspace_id = $rs.id
      name = 'MyScript'
      script = $strCode
      script_parameters = @()
    }
  }
  $createSCR = Invoke-SrsMethod @sSCRCreate
  $scr = $createSCR.Content | ConvertFrom-Json
  #endregion

  #region Wait for Script to end
  while ($scr.state -eq 'running') {
    $sSCRCreate = @{
      FQDN = $fqdn
      API = "/api/script-executions/$($scr.id)"
      Method = 'Get'
      Headers = $headers
    }
    $getSCR = Invoke-SrsMethod @sSCRCreate
    $scr = $getSCR.Content | ConvertFrom-Json
  }
  #endregion

  #region Retrieve Script output
  $sSCROut = @{
    FQDN = $fqdn
    API = "/api/script-executions/$($scr.id)/output"
    Method = 'Get'
    Headers = $headers
  }
  $outSCR = Invoke-SrsMethod @sSCROut
  $outSCR.Content | ConvertFrom-Json
  #endregion

  #region Retrieve Script streams
  $streams = 'information','error','warning','debug','verbose'

  $streams | ForEach-Object -Process {
    $sSCRStr = @{
      FQDN = $fqdn
      API = "/api/script-executions/$($scr.id)/streams/$($_)"
      Method = 'Get'
      Headers = $headers
    }
    $getStream = Invoke-SrsMethod @sScrStr
    if ($getStream.Content -ne '[]') {
      $out = ($getStream.Content | ConvertFrom-Json).message | Out-String
      switch($_){
        'information' {
          Write-Information -MessageData $out
        }
        'error' {
          Write-Error -Message $out
        }
        'warning' {
          Write-Warning -Message $out
        }
        'debug' {
          Write-Debug -Message $out
        }
        'verbose' {
          Write-Verbose -Message $out
        }
      }
    }
  }
  #endregion

  #region Remove Runspace
  $sRSDel = @{
    FQDN = $fqdn
    API = "/api/runspaces/$($rs.id)"
    Method = 'Delete'
    Headers = $headers
  }
  $rsDel = Invoke-SrsMethod @sRSDel
  $rsDel.Content | ConvertFrom-Json
  #endregion

  #region Logout
  $sLogout = @{
    FQDN = $fqdn
    API = "/api/auth/logout"
    Method = 'Post'
    Headers = $headers
  }
  $srsLogout = Invoke-SrsMethod @sLogout
  #endregion

}
</code></pre>



<p>This <strong>Invoke-SRSScript</strong> function can be used in two variations (parametersets). With the <strong>Code</strong> parameter, you pass a ScriptBlock.</p>



<pre class="lang:ps decode:true  ">
$sScript = @{
  SRSHost = 'srs1'
  Code = { Get-VM }
  VCCredential = $cred
}
Invoke-SRSScript @sScript
</code></pre>



<p>With the <strong>Path</strong> parameter, you point to a .ps1 file.</p>



<pre class="lang:ps decode:true  ">$sScript = @{
  SRSHost = 'srs1'
  Path = 'D:\Git\SRS-Explore\Use\Sample.ps1'
  VCCredential = $cred
}
</code></pre>



<h2 class="wp-block-heading">Some Administration</h2>



<p>The SRS comes with a number of preset values, see the <a href="https://github.com/vmware/script-runtime-service-for-vsphere/wiki/Initial-Configuration" target="_blank" rel="noopener">Initial Configuration</a> documentation. In some situations, you might want to change one or more of these settings.</p>
<p>The obvious solution is to rebuild your SRS OVA with the desired settings, and re-deploy your SRS VM.&nbsp;</p>
<p>But you can also change these settings on the fly on an existing and running SRS. The following snippet is just an example where I change the <strong>script-runtime-service</strong> setting from the default 10 minutes to 20 minutes.</p>



<pre class="lang:ps decode:true  ">
$vmName = 'srs1'

# Get the SRS credentials

$viCred = Get-VICredentialStoreItem -Host $vmName
$secPassword = ConvertTo-SecureString -String $viCred.Password -AsPlainText -Force
$cred = [Management.Automation.PSCredential]::new($viCred.User, $secPassword)

$vm = Get-VM -Name $vmName

#region Change SRS setting

$code = @'
kubectl get cm service-settings -n script-runtime-service -o yaml | sed -e 's/\("MaxRunspaceIdleTimeMinutes": \).*/\120,/' | kubectl apply -f -
'@

$sInvoke = @{
  VM = $vm
  GuestCredential = $cred
  ScriptType = 'bash'
  ScriptText = $code
}
Invoke-VMScript @sInvoke
#endregion

#region Check new SRS settings

$code = @'
kubectl get cm service-settings -n script-runtime-service -o yaml
'@

$sInvoke = @{
  VM = $vm
  GuestCredential = $cred
  ScriptType = 'bash'
  ScriptText = $code
}
Invoke-VMScript @sInvoke

#endregion
</code></pre>



<p>Note that such a change will not be applied immediately, it might take some time (we are talking minutes).</p>



<h2 class="wp-block-heading">Epilogue</h2>



<p>This concludes, for now, my somewhat lengthy <strong>Hitchhikers Guide to SR</strong>S<strong> 1.0.0</strong>, which resulted from my playing/testing/exploring the Script Runtime Service for vSphere (SRS) 1.0.0.</p>



<p>Is this something to should have gotten more attention when it was released?</p>
<p>Definitely!</p>



<p>I will surely be doing some more experimenting with the new and shining tool.</p>
<p>Enjoy!</p>


]]></content:encoded>
					
					<wfw:commentRss>https://www.lucd.info/2020/12/23/a-hitchhikers-guide-to-srs-1-0-0/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
			</item>
		<item>
		<title>How to Mount a Content Library ISO on a VM</title>
		<link>https://www.lucd.info/2020/12/13/how-to-mount-a-content-library-iso-on-a-vm/</link>
					<comments>https://www.lucd.info/2020/12/13/how-to-mount-a-content-library-iso-on-a-vm/#comments</comments>
		
		<dc:creator><![CDATA[LucD]]></dc:creator>
		<pubDate>Sun, 13 Dec 2020 13:53:20 +0000</pubDate>
				<category><![CDATA[Content Library]]></category>
		<category><![CDATA[ISO]]></category>
		<category><![CDATA[mount]]></category>
		<category><![CDATA[PowerCLI]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[Virtual Machine]]></category>
		<category><![CDATA[Mount]]></category>
		<guid isPermaLink="false">https://www.lucd.info/?p=7356</guid>

					<description><![CDATA[While Content Libraries are becoming more and more used, there are still some [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>While <a href="https://blogs.vmware.com/vsphere/2019/12/the-evolution-of-content-library.html" target="_blank" rel="noopener">Content Libraries</a> are becoming more and more used, there are still some features that are not yet implemented in PowerCLI. So is it for example not possible to mount an ISO file located in a Content Library on a VM. The <a href="https://vdc-repo.vmware.com/vmwb-repository/dcr-public/f17594eb-bbe1-44a7-b7ac-b2da546936c2/21da7740-996d-481e-83e4-05d8fa7db18d/doc/Set-CDDrive.html" target="_blank" rel="noopener">Set-CDDrive</a> cmdlet is currently <a href="https://powercli.ideas.aha.io/ideas/PCLI-I-172" target="_blank" rel="noopener">lacking this functionality</a>, while the <a href="https://docs.vmware.com/en/VMware-vSphere/7.0/com.vmware.vsphere.vm_admin.doc/GUID-BE1C18D2-8FF0-4F41-AA35-A4BA71D62EB4.html" target="_blank" rel="noopener">Web Client</a> offers this option.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="621" height="250" src="https://www.lucd.info/wp-content/uploads/2020/12/cl-iso-mount-1.png" alt="" class="wp-image-7369" srcset="https://www.lucd.info/wp-content/uploads/2020/12/cl-iso-mount-1.png 621w, https://www.lucd.info/wp-content/uploads/2020/12/cl-iso-mount-1-300x121.png 300w" sizes="auto, (max-width: 621px) 100vw, 621px" /></figure>



<p>Like often, and one of the <a href="https://code.vmware.com/web/tool/vmware-powercli" target="_blank" rel="noopener">VMware PowerCLI</a> features I absolutely like, when a cmdlet is missing a feature, you can fall back on the API to solve the issue.</p>



<span id="more-7356"></span>



<h2>Introduction</h2>



<p>Like so often, the reason I had a closer look at this issue was due to a <a href="https://communities.vmware.com/t5/VMware-PowerCLI-Discussions/Mount-a-content-library-iso-on-a-live-VM/td-p/2816413/jump-to/first-unread-message" target="_blank" rel="noopener">question</a> in <a href="https://communities.vmware.com/t5/VMware-PowerCLI-Discussions/bd-p/2805" target="_blank" rel="noopener">PowerCLI Community</a> on VMTN.</p>



<p>My first step in such cases is always to have a look at the <a href="https://code.vmware.com/apis/968/vsphere" target="_blank" rel="noopener">vSphere Web Services API</a>.</p>



<p>But unfortunately, the <a href="https://vdc-download.vmware.com/vmwb-repository/dcr-public/b50dcbbf-051d-4204-a3e7-e1b618c1e384/538cf2ec-b34f-4bae-a332-3820ef9e7773/vim.VirtualMachine.html#reconfigure" target="_blank" rel="noopener">ReconfigVM_Task</a> method documentation didn&#8217;t make me any wiser. There is apparently no object, property, or method to mount an ISO from a Content Library onto a VM.</p>



<p>In the next step I had a look at the <a href="https://developer.vmware.com/docs/vsphere-automation/latest/" target="_blank" rel="noopener">vSphere REST API Reference</a>. But in there I could also not find anything specific about Content Library ISOs and VM CD drives.</p>



<p>When all else fails, <strong>look at the code</strong>!</p>



<p>And with <a href="https://blogs.vmware.com/PowerCLI/2018/08/introducing-code-capture.html" target="_blank" rel="noopener">Code Capture</a> we have just that option!</p>



<p>The first thing I noticed while looking at the generated code, there are no &#8216;special&#8217; object, properties, or method used. Just a plain <strong>ReconfigVM_Task</strong> call with an <strong>edit</strong> action on the CD drive device.</p>



<p>The only special feature was the value in the FileName property.</p>



<p>That value definitely pointed to the ISO file in the Content Library. At first I tried to compose that value by hand/script. But it turned out that some parts of the value were not obtainable via normal ways.</p>



<p>I found out then when I use the <a href="https://vdc-repo.vmware.com/vmwb-repository/dcr-public/f17594eb-bbe1-44a7-b7ac-b2da546936c2/21da7740-996d-481e-83e4-05d8fa7db18d/doc/about_vimdatastore.html" target="_blank" rel="noopener">vimdatastore</a> provider, the returned <strong>FullDatastorePath</strong> contained exactly the value I neededd.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="172" src="https://www.lucd.info/wp-content/uploads/2020/12/CL-ISO-1024x172.png" alt="" class="wp-image-7374" srcset="https://www.lucd.info/wp-content/uploads/2020/12/CL-ISO-1024x172.png 1024w, https://www.lucd.info/wp-content/uploads/2020/12/CL-ISO-300x50.png 300w, https://www.lucd.info/wp-content/uploads/2020/12/CL-ISO-768x129.png 768w, https://www.lucd.info/wp-content/uploads/2020/12/CL-ISO-720x121.png 720w, https://www.lucd.info/wp-content/uploads/2020/12/CL-ISO.png 1493w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading">The Code</h2>



<pre class="lang:ps decode:true  ">
function Set-CDDriveCLIso {

    <#
    .SYNOPSIS
      Mount an ISO from a Content Library
      .DESCRIPTION
      This function will mount an ISO located on a Content Library
      on a CDDrive of a VM
    .NOTES
      Author:  Luc Dekens
      Version:
      1.0 24/12/20  Initial release
    .PARAMETER VM
      Specifies the virtual machines on whose guest operating systems
      you want to run the script.
    .PARAMETER CDDrive
      Specifies the CDDrive on which the ISO will be mounted
    .PARAMETER ContentLibrary
      Specifies the Content Library on which the ISO is located.
    .PARAMETER ContentLibraryIso
      Specifies the ISO item on the Content Library
    .EXAMPLE
      $cl = Get-ContentLibrary -Name MyCL
      $iso = Get-ContentLibraryItem -ContentLibrary $cl -Name MyISO
      Get-VM -Name 'MyVM' | Get-CDDrive -Name 'CD/DVD drive 0' |
      Set-CDDriveCLIso -ContentLibraryIso $iso -Confirm:$false
    .EXAMPLE
      $cd = Get-VM -Name MyVM | Get-CDDrive
      Set-CDDriveCLIso -CDDrive $cd -ContentLibraryIso $iso -Confirm:$false
    #>
    
    [cmdletbinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    param(
        [parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [VMware.VimAutomation.ViCore.Types.V1.VirtualDevice.CDDrive]$CDDrive,
        [parameter(Mandatory = $true)]
        [VMware.VimAutomation.ViCore.Types.V1.ContentLibrary.ContentLibraryItem]$ContentLibraryISO
    )
    
    $target = "VM:$($CDDrive.Parent.Name) CD:$($CDDrive.Name)"
    $action = "Mount ISO $($ContentLibraryISO.Name) from $($ContentLibraryISO.ContentLibrary.Name)"
        
    if ($PSCmdlet.ShouldProcess($target, $action)) {
        $driveName = -join ((65..90) | 
                Get-Random -Count 3 | 
                ForEach-Object -Process { [char]$_ })
        $filter = "$($ContentLibraryISO.Name)*.iso" 
        
        $ds = Get-Datastore -Name $ContentLibraryISO.ContentLibrary.Datastore
        
        New-PSDrive -Name $driveName -PSProvider VimDatastore -Root '\' -Location $ds | Out-Null
        $clPath = Get-ChildItem -Path "$($driveName):" -Filter "$($ContentLibraryIso.Id)" -Recurse |
            Select-Object -ExpandProperty FolderPath
        $isoPath = Get-ChildItem -Path "$($driveName):\$($clPath.Split(' ')[1])" -Filter $filter -Recurse | 
            Select-Object -ExpandProperty DatastoreFullPath
        Remove-PSDrive -Name $driveName -Confirm:$false | Out-Null
            
        $spec = New-Object VMware.Vim.VirtualMachineConfigSpec
            
        $change = New-Object VMware.Vim.VirtualDeviceConfigSpec
        $change.Operation = [Vmware.vim.VirtualDeviceConfigSpecOperation]::edit
            
        $dev = $cd.ExtensionData
        $dev.Backing = New-Object VMware.Vim.VirtualCdromIsoBackingInfo
        $dev.Backing.FileName = $isoPath
            
        $change.Device += $dev
        $change.Device.Connectable.Connected = $true
            
        $spec.DeviceChange = $change
            
        $vm.ExtensionData.ReconfigVM($spec)
    
        Get-CDDrive -Id $CDDrive.Id
    }
}
</code></pre>



<h3 class="wp-block-heading">Annotations</h3>



<p><strong>Line 34-37</strong>: The parameters that the function requires are objects returned by the <a href="https://vdc-repo.vmware.com/vmwb-repository/dcr-public/f17594eb-bbe1-44a7-b7ac-b2da546936c2/21da7740-996d-481e-83e4-05d8fa7db18d/doc/Get-CDDrive.html" target="_blank" rel="noreferrer noopener">Get-CDDrive</a> and <a href="https://vdc-repo.vmware.com/vmwb-repository/dcr-public/f17594eb-bbe1-44a7-b7ac-b2da546936c2/21da7740-996d-481e-83e4-05d8fa7db18d/doc/Get-ContentLibraryItem.html" target="_blank" rel="noreferrer noopener">Get-ContentLibraryItem</a> cmdlets. The function does not implement <a href="https://vdc-repo.vmware.com/vmwb-repository/dcr-public/f17594eb-bbe1-44a7-b7ac-b2da546936c2/21da7740-996d-481e-83e4-05d8fa7db18d/doc/about_obn.html" target="_blank" rel="noreferrer noopener">OBN</a> support.</p>



<p><strong>Line 40-41</strong>: This provides meaningful content for the message returned when the <strong>WhatIf</strong> switch is used.</p>



<p><strong>Line 44-46</strong>: To avoid conflicts with PSDrives the user might already have defined, the function generates a <strong>3-letter, random string</strong> that will be used as the drivename.</p>



<p><strong>Line 52-53</strong>: To avoid a situation where the same ISO file might be uploaded to two different Content Libraries, the function first finds the path to the folder that corresponds with the requested Content Library.</p>



<p><strong>Line 54-55</strong>: This search finds the actual path to the desired ISO file.</p>



<p><strong>Line 60-72</strong>: A &#8216;regular&#8217; call to the ReconfigVM_Task method.</p>



<p><strong>Line 74</strong>: The function returns the CDDrive object, just like the regular Set-CDDrive cmdlet does. But since there is currently not yet support for ISO files from a Content Library, the filepath looks like a regular path to an ISO file. The Web Client does show that this is an ISO from a Content Library.</p>



<h2 class="wp-block-heading">Sample Run</h2>



<p>Using the function is rather straightforward.</p>



<pre class="lang:ps decode:true  ">
$vmName = 'photonps'
$cdName = 'CD/DVD drive 1'
$cLibName = 'ConLib1'
$cLibItemName = 'photon-minimal-3.0-a0f216d'
    
$vm = Get-VM -Name $vmName
    
$cl = Get-ContentLibrary -Name $cLibName
$iso = Get-ContentLibraryItem -Name $cLibItemName -ContentLibrary $cl
    
Get-CDDrive -VM $vm -Name $cdName |
Set-CDDriveCLISO -ContentLibraryISO $iso -Confirm:$false
</code></pre>



<p>The Web Client shows that an ISO from a Content Library is loaded.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="780" height="207" src="https://www.lucd.info/wp-content/uploads/2020/12/wc-cl-iso.png" alt="" class="wp-image-7378" srcset="https://www.lucd.info/wp-content/uploads/2020/12/wc-cl-iso.png 780w, https://www.lucd.info/wp-content/uploads/2020/12/wc-cl-iso-300x80.png 300w, https://www.lucd.info/wp-content/uploads/2020/12/wc-cl-iso-768x204.png 768w, https://www.lucd.info/wp-content/uploads/2020/12/wc-cl-iso-720x191.png 720w" sizes="auto, (max-width: 780px) 100vw, 780px" /></figure>



<p>Enjoy!</p>


]]></content:encoded>
					
					<wfw:commentRss>https://www.lucd.info/2020/12/13/how-to-mount-a-content-library-iso-on-a-vm/feed/</wfw:commentRss>
			<slash:comments>5</slash:comments>
		
		
			</item>
		<item>
		<title>Crescendo ma non troppo</title>
		<link>https://www.lucd.info/2020/12/10/crescendo-module-preview-1/</link>
					<comments>https://www.lucd.info/2020/12/10/crescendo-module-preview-1/#respond</comments>
		
		<dc:creator><![CDATA[LucD]]></dc:creator>
		<pubDate>Thu, 10 Dec 2020 12:53:27 +0000</pubDate>
				<category><![CDATA[Crescendo]]></category>
		<category><![CDATA[Module]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[VMware Tools]]></category>
		<category><![CDATA[Preview]]></category>
		<guid isPermaLink="false">https://www.lucd.info/?p=7328</guid>

					<description><![CDATA[No, this is not a post about music. This post will be about [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>No, this is not a post about music.</p>
<p>This post will be about my first experiences with the <a href="https://www.powershellgallery.com/packages/Microsoft.PowerShell.Crescendo/0.4.0" target="_blank" rel="noopener">PowerShell Crescendo</a> module, which was just released in <a href="https://devblogs.microsoft.com/powershell/announcing-powershell-crescendo-preview-1/" target="_blank" rel="noopener">Preview 1</a>.</p>



<div class="wp-block-image"><figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="285" height="223" src="https://www.lucd.info/wp-content/uploads/2020/12/vmw-crescendo-2.png" alt="" class="wp-image-7331"/></figure></div>



<p>Now, why would I care to write a post about a preview of a module?</p>



<p>The reason is quite simple. This module is, in my opinion, covering something that was missing in PowerShell. Namely the possibility to use <strong>native commands</strong>, and present the results as <strong>objects</strong>.</p>
<p>The blog post by <a href="https://twitter.com/theJasonHelmick" target="_blank" rel="noopener">Jason Helmick</a> I linked to earlier explains this all in more detail.</p>



<p>What follows are my first, admittedly simple, trials using the <a href="https://www.powershellgallery.com/packages/Microsoft.PowerShell.Crescendo/0.4.0" target="_blank" rel="noopener">Crescendo module</a>. If you don&#8217;t want to copy code from this post, I created the <a class="" href="https://github.com/lucdekens/VMW-Crescendo" data-pjax="#js-repo-pjax-container">VMW-Crescendo</a> repository that contains all the files.</p>



<span id="more-7328"></span>



<h2 class="wp-block-heading">An Example</h2>



<p>The first example I tackled is the DOS command <a href="https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/tasklist" target="_blank" rel="noopener">tasklist</a>, which displays a list of the running processes.&nbsp;</p>



<p>Not to complicate things at the start, I decided to only implement the module for running <strong>tasklist</strong> on the <strong>local computer</strong>.</p>
<p>And needless to say, since this is a DOS command, it will <strong>not run</strong> on other platforms besides Windows.</p>



<p>The idea behind the <strong>Crescendo module</strong> is that you write in <a href="https://en.wikipedia.org/wiki/JSON" target="_blank" rel="noopener">JSON</a>, how the cmdlet(s) you will create use the native command. This not only includes specifying which <strong>parameters</strong> are used but also how the returned <strong>output</strong> will be converted to an <strong>object</strong>.</p>



<p>Since PowerShell contains cmdlets to convert to and from JSON, I decided to try and write those <strong>definitions in PowerShell</strong>. And then convert that code to JSON format, which I could then feed to the Crescendo cmdlet.</p>



<h3 class="wp-block-heading">The Code</h3>



<pre class="lang:ps decode:true  ">#Requires -Version 7.0

#region An example

$taskConfig = @{
  '$schema' = './Microsoft.PowerShell.Crescendo.Schema.json'
  'Verb' = 'Get'
  'Noun' = 'TaskList'
  'OriginalName' = 'tasklist'
  'OriginalCommandElements' = @("/V")
  'OutputHandlers' = @(
        @{
            'ParameterSetName' = 'Default'
            'Handler' = {
              $labels = $args&#91;0]&#91;1]
              $index = 0
              $fieldInfo = $args&#91;0]&#91;2].Split(' ') | ForEach-Object -Process {
                @{
                  start = $index
                  length = $_.Length
                }
                $index += $_.Length + 1
              }
              $args&#91;0] | Select-Object -skip 3 -PipelineVariable row | ForEach-Object -Process {
                $obj = &#91;ordered]@{}
                $fieldInfo | ForEach-Object -Process {
                  $obj.Add($labels.SubString($_.start,$_.length).Trim(' '),$row.SubString($_.start,$_.length).Trim(' '))
                }
                New-Object -TypeName PSObject -Property $obj
              }
            }
        }
  )
}

$taskConfig | ConvertTo-Json | Set-Content -Path "$PSScriptRoot\tasklist.json"

Export-CrescendoModule -ConfigurationFile "$PSScriptRoot\tasklist.json" -ModuleName "$PSScriptRoot\TaskList.psm1" -Force -Confirm:$false

#endregion
</code></pre>



<h3 class="wp-block-heading">Annotations</h3>



<p><strong>Line 1</strong>: Although the blog post mentions that a module generated with Crescendo can be used on PowerShell 5.1 and 7, I noticed that since the generated code uses the ternary operator, this not really the case. Hence the requirement to run this on PowerShell v7.</p>



<p><strong>Line 5-35</strong>: As I mentioned earlier, I created the definition of the cmdlets to be created in PowerShell.</p>



<p><strong>Line 10</strong>: By default, the tasklist command is started with the <strong>/V</strong> (for verbose) parameter.</p>



<p><strong>Line 11-33</strong>: The <strong>OutputHandler</strong> is where you define how the output of the native command will be converted to an object. I did have to use some &#8216;trickery&#8217; to avoid having to use hard-coded offset. Luckily the one can use the underlining to determine the start and length of each column.</p>



<p><strong>Line 37</strong>: The Powershell object is converted to JSON code, and saved to a file.</p>



<p><strong>Line 39</strong>: The Export-CrescendoModule cmdlet takes the JSON file and generates the module.</p>





<h3 class="wp-block-heading">Sample Run</h3>



<p>The generated modules, at least in this Preview 1, only generate a <strong>.psm1</strong> file. You can use the <strong>Import-Module</strong> pointing to that .psm1 file.</p>



<p>The module contains the <strong>Get-TaskList</strong> cmdlet, that requires no further parameters and returns a series of objects, containing the results.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="823" height="535" src="https://www.lucd.info/wp-content/uploads/2020/12/gettasklist.png" alt="" class="wp-image-7337" srcset="https://www.lucd.info/wp-content/uploads/2020/12/gettasklist.png 823w, https://www.lucd.info/wp-content/uploads/2020/12/gettasklist-300x195.png 300w, https://www.lucd.info/wp-content/uploads/2020/12/gettasklist-768x499.png 768w, https://www.lucd.info/wp-content/uploads/2020/12/gettasklist-720x468.png 720w" sizes="auto, (max-width: 823px) 100vw, 823px" /></figure>



<p>Since the Get-TaskList cmdlet returns objects, we can now easily use other PowerShell cmdlets to further handle the returned data. For example:</p>



<pre class="lang:ps decode:true  ">
Get-TaskList | Export-Csv -Path .\TaskList.Csv -NoTypeInformation -UseCulture
</code></pre>



<p>The resulting CSV file contains all the data.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="894" height="295" src="https://www.lucd.info/wp-content/uploads/2020/12/tasklist-csv.png" alt="" class="wp-image-7340" srcset="https://www.lucd.info/wp-content/uploads/2020/12/tasklist-csv.png 894w, https://www.lucd.info/wp-content/uploads/2020/12/tasklist-csv-300x99.png 300w, https://www.lucd.info/wp-content/uploads/2020/12/tasklist-csv-768x253.png 768w, https://www.lucd.info/wp-content/uploads/2020/12/tasklist-csv-720x238.png 720w" sizes="auto, (max-width: 894px) 100vw, 894px" /></figure>



<h2 class="wp-block-heading">VMware Tools Debug Levels</h2>



<p>The second example I concocted, uses one of the native commands that come with the installation of the VMware Tools. The <a href="https://docs.vmware.com/en/VMware-Tools/10.2.0/com.vmware.vsphere.vmwaretools.doc/GUID-009A09B7-60F0-4B38-8908-C7D712E7E63E.html" target="_blank" rel="noopener">VMwareToolboxCmd.exe</a> has the option to <strong>get</strong> and <strong>set</strong> the <a href="https://kb.vmware.com/s/article/1007873" target="_blank" rel="noopener">debug levels</a> on the VMware Tools services.&nbsp;</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="790" height="233" src="https://www.lucd.info/wp-content/uploads/2020/12/dos-cmd.png" alt="" class="wp-image-7344" srcset="https://www.lucd.info/wp-content/uploads/2020/12/dos-cmd.png 790w, https://www.lucd.info/wp-content/uploads/2020/12/dos-cmd-300x88.png 300w, https://www.lucd.info/wp-content/uploads/2020/12/dos-cmd-768x227.png 768w, https://www.lucd.info/wp-content/uploads/2020/12/dos-cmd-720x212.png 720w" sizes="auto, (max-width: 790px) 100vw, 790px" /></figure>



<p>Note that you need to run this command in <strong>Administrator mode</strong>.</p>



<h3 class="wp-block-heading">The Code</h3>



<pre class="lang:ps decode:true  ">
#Requires -Version 7.0

#region VMware Tools debug levels

$getDebugConfig = @{
  '$schema' = './Microsoft.PowerShell.Crescendo.Schema.json'
  'Verb' = 'Get'
  'Noun' = 'ToolsDebugSetting'
  'OriginalName' = "$($env:ProgramFiles)\VMware\VMware Tools\VMwareToolboxCmd.exe"
  'Description' = 'Retrieve the debug levels for VMware Tools'
  'OriginalCommandElements' = 'logging','level','get'
  'Parameters' = @(
    @{
      'Name' = 'Svc'
      'OriginalName' = 'vmsvc'
      'ParameterType' = 'switch'
      'Description' = 'Primary VMware Tools service'
    },
    @{
      'Name' = 'Vss'
      'OriginalName' = 'vmvss'
      'ParameterType' = 'switch'
      'Description' = 'Volume Shadow Copy service'
    },
    @{
      'Name' = 'Resolution'
      'OriginalName' = 'vmresset'
      'ParameterType' = 'switch'
      'Description' = 'Resolution Set Tool'
    },
    @{
      'Name' = 'Tray'
      'OriginalName' = 'vmtray'
      'ParameterType' = 'switch'
      'Description' = 'The Tray icon in Windows'
    },
    @{
      'Name' = 'User'
      'OriginalName' = 'vmusr'
      'ParameterType' = 'switch'
      'Description' = 'The VMware Tools user service'
    },
    @{
      'Name' = 'Toolbox'
      'OriginalName' = 'toolbox'
      'ParameterType' = 'switch'
      'Description' = 'Toolbox UI'
    },
    @{
      'Name' = 'ToolBoxCmd'
      'OriginalName' = 'toolboxcmd'
      'ParameterType' = 'switch'
      'Description' = 'Toolbox command line'
    }
  )
  'OutputHandlers' = @(
        @{
            'ParameterSetName' = 'Default'
            'Handler' = {
              $service,$level = $args[0].Split('=')
              New-Object -TypeName PSObject -Property @{
                "$($service -replace '.level','')" = $level.Trim(' ')
              }
            }
        }
  )
}
$getDebugConfig | ConvertTo-Json | Set-Content -Path "$PSScriptRoot\getdebug.json"

$setDebugConfig = @{
  '$schema' = './Microsoft.PowerShell.Crescendo.Schema.json'
  'Verb' = 'Set'
  'Noun' = 'ToolsDebugSetting'
  'OriginalName' = "$($env:ProgramFiles)\VMware\VMware Tools\VMwareToolboxCmd.exe"
  'Description' = 'Retrieve the debug levels for VMware Tools'
  'OriginalCommandElements' = 'logging', 'level', 'set'
  'Parameters' = @(
    @{
      'Name' = 'Svc'
      'OriginalName' = 'vmsvc'
      'ParameterType' = 'switch'
      'Description' = 'Primary VMware Tools service'
    },
    @{
      'Name' = 'Vss'
      'OriginalName' = 'vmvss'
      'ParameterType' = 'switch'
      'Description' = 'Volume Shadow Copy service'
    },
    @{
      'Name' = 'Resolution'
      'OriginalName' = 'vmresset'
      'ParameterType' = 'switch'
      'Description' = 'Resolution Set Tool'
    },
    @{
      'Name' = 'Tray'
      'OriginalName' = 'vmtray'
      'ParameterType' = 'switch'
      'Description' = 'The Tray icon in Windows'
    },
    @{
      'Name' = 'User'
      'OriginalName' = 'vmusr'
      'ParameterType' = 'switch'
      'Description' = 'The VMware Tools user service'
    },
    @{
      'Name' = 'Toolbox'
      'OriginalName' = 'toolbox'
      'ParameterType' = 'switch'
      'Description' = 'Toolbox UI'
    },
    @{
      'Name' = 'ToolBoxCmd'
      'OriginalName' = 'toolboxcmd'
      'ParameterType' = 'switch'
      'Description' = 'Toolbox command line'
    },
    @{
      'Name' = 'Level'
      'OriginalName' = ''
      'ParameterType' = 'string'
      'Mandatory' = $true
      'Description' = 'New debug level'
    }
    )
    }
$setDebugConfig | ConvertTo-Json | Set-Content -Path "$PSScriptRoot\setdebug.json"

$sCrecendo = @{
  ConfigurationFile = "$($PSScriptRoot)\getdebug.json", "$($PSScriptRoot)\setdebug.json"
  ModuleName = "$($PSScriptRoot)\ToolsDebug.psm1"
  Force = $true
  Confirm = $false
  Verbose = $true
}
Export-CrescendoModule @sCrecendo
#endregion
</code></pre>



<h3 class="wp-block-heading">Annotations</h3>



<p><strong>Line 1</strong>: Although the blog post mentions that a module generated with Crescendo can be used on PowerShell 5.1 and 7, I noticed that since the generated code uses the ternary operator, this not really the case. Hence the requirement to run this on PowerShell v7.</p>



<p><strong>Line 5-66</strong>: This part is the definition for the <strong>Get-ToolsDebugSettings</strong> cmdlet.</p>



<p><strong>Line 11</strong>: With the <strong>OriginalCommandElements</strong> property we define which parameters shall be always added to the native command.</p>



<p><strong>Line 12-55</strong>: Each possible service is defined as a Switch parameter. Each entry also has the cmdlet parameter name (<strong>Name</strong>) and how it shall be translated to a native command parameter (<strong>OriginalName</strong>).</p>



<p><strong>Line 59-63</strong>: The Output Handler takes the line that is returned and converts it into an object.</p>



<p><strong>Line 68</strong>: The PowerShell object is converted into JSON and save to a file.</p>



<p><strong>Line 70-128</strong>: This part is the definition for the <strong>Set-ToolsDebugSettings</strong> cmdlet.</p>



<p><strong>Line 120-126</strong>: The Set cmdlet will have an additional parameter (<strong>Level</strong>) that indicates which new debug level shall be set for the selected service. Note that the debuglevel is added as-is, no parameter on the native command.</p>



<h3 class="wp-block-heading">Sample Run</h3>



<p>The cmdlets from the generated module need to be called from an <strong>elevated prompt</strong> (same as the native command).</p>



<pre class="lang:ps decode:true  ">
#Requires -Version 7.0

Import-Module "$PSScriptRoot\ToolsDebug.psm1" -Force

Get-ToolsDebugSetting -Vss
Set-ToolsDebugSetting -Vss -Level debug
Get-ToolsDebugSetting -Vss
</code></pre>



<p>And just as with the native command we can query and set the debug levels.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="904" height="354" src="https://www.lucd.info/wp-content/uploads/2020/12/tools-debug.png" alt="" class="wp-image-7353" srcset="https://www.lucd.info/wp-content/uploads/2020/12/tools-debug.png 904w, https://www.lucd.info/wp-content/uploads/2020/12/tools-debug-300x117.png 300w, https://www.lucd.info/wp-content/uploads/2020/12/tools-debug-768x301.png 768w, https://www.lucd.info/wp-content/uploads/2020/12/tools-debug-720x282.png 720w" sizes="auto, (max-width: 904px) 100vw, 904px" /></figure>



<h2 class="wp-block-heading">In Conclusion</h2>



<p>Is this new <strong>Crescendo</strong> module something that you should be using immediately?</p>
<p>Definitely not, this is a module, based on an interesting concept, that only just entered <strong>Preview 1</strong>.</p>
<p>But it is surely a concept that is interesting.<br />I&#8217;ll be watching where this goes, and what can be done with it.</p>
<p>Enjoy!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.lucd.info/2020/12/10/crescendo-module-preview-1/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Cloud-init &#8211; Part 5 &#8211; Running Containers</title>
		<link>https://www.lucd.info/2019/12/11/cloud-init-part-5-running-containers/</link>
					<comments>https://www.lucd.info/2019/12/11/cloud-init-part-5-running-containers/#comments</comments>
		
		<dc:creator><![CDATA[LucD]]></dc:creator>
		<pubDate>Wed, 11 Dec 2019 14:30:53 +0000</pubDate>
				<category><![CDATA[Cloud-init]]></category>
		<category><![CDATA[Containers]]></category>
		<category><![CDATA[Photon]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[container]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[ubuntu]]></category>
		<guid isPermaLink="false">http://www.lucd.info/?p=6917</guid>

					<description><![CDATA[In this last part of this series (for now) we will show how [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>In this last part of this series (for now) we will show how to use <strong>containers</strong> to run your PowerShell/PowerCLI scripts on the deployed instances. And although technically not a ‘<em>real</em>‘&nbsp;<a href="https://cloudinit.readthedocs.io/en/latest/" target="_blank" rel="noopener noreferrer">cloud-init</a>&nbsp;post, I consider it related to&nbsp;<a href="https://www.lucd.info/2019/12/06/cloud-init-part-1-the-basics/" target="_blank" rel="noopener noreferrer">Part 1</a>,&nbsp;<a href="https://www.lucd.info/2019/12/07/cloud-init-part-2-advanced-ubuntu/" target="_blank" rel="noopener noreferrer">Part 2</a>&nbsp;and&nbsp;<a href="https://www.lucd.info/2019/12/08/cloud-init-part-3-photon-os/" target="_blank" rel="noopener noreferrer">Part3</a> in this series.&nbsp;</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="730" height="349" src="https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-5-2.png" alt="" class="wp-image-6923" srcset="https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-5-2.png 730w, https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-5-2-300x143.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-5-2-720x344.png 720w" sizes="auto, (max-width: 730px) 100vw, 730px" /></figure>



<span id="more-6917"></span>



<h2 class="wp-block-heading">Introduction</h2>



<p>In <a rel="noreferrer noopener" aria-label="Post 4 (opens in a new tab)" href="https://www.lucd.info/2019/12/09/cloud-init-part-4-running-scripts/" target="_blank">Post 4</a> of the <a href="https://www.lucd.info/?s=cloud-init" target="_blank" rel="noreferrer noopener" aria-label="cloud-init series (opens in a new tab)">cloud-init series</a>, we ran our PowerShell/PowerCLI scripts natively in the Guest OS of the instance.</p>



<p>That might have an impact in some situations. When the first script installs for example the VMware PowerCLI modules, these modules will stay installed in the Guest OS. Consider this as a kind of <strong>PowerShell tattooing</strong>.</p>



<p>Another task that might be somewhat of a challenge is testing your PowerShell scripts against multiple environments, different module versions, different OS versions&#8230; Again, when we make changes to the Guest OS, these changes might be <strong>hard to remove</strong>.</p>



<p>Besides deploying a new &#8216;cattle&#8217; station for each possible combination, we can also run our scripts in <a rel="noreferrer noopener" aria-label="Docker containers (opens in a new tab)" href="https://www.docker.com/" target="_blank">Docker containers</a>. Once the script is done, we throw away the container instance. And we are back to a pristine OS.</p>



<p>And as it turns out, containers also offer some new features that are harder, or even impossible, to implement when running locally in the Guest OS.</p>



<p>Since we will be using several docker commands in the following sections, it will be useful to have the <a href="https://docs.docker.com/reference/" target="_blank" rel="noreferrer noopener" aria-label="Docker Reference (opens in a new tab)">Docker Reference</a> documentation nearby. </p>



<h2 class="wp-block-heading">Install Docker</h2>



<h3 class="wp-block-heading">Ubuntu</h3>



<p>The Ubuntu OVAs we have been installing in the previous posts in this series, do not contain the docker package. We can easily include that in our YAML file under the <strong>runcmd</strong> section.</p>



<pre class="lang:yaml decode:true ">#cloud-config
hostname: ubuntubioniccontainer
fqdn: ubuntubioniccontainer.local.lab
write_files:
- path: /etc/netplan/50-cloud-init.yaml
  content: |
    network:
     version: 2
     ethernets:
      ens192:
       addresses: [192.168.10.84/24]
       gateway4: 192.168.10.1
       dhcp6: false
       nameservers:
         addresses:
           - 192.168.10.2
           - 192.168.10.3
         search:
           - local.lab
       dhcp4: false
       optional: true
- path: /etc/sysctl.d/60-disable-ipv6.conf
  owner: root
  content: |
    net.ipv6.conf.all.disable_ipv6=1
    net.ipv6.conf.default.disable_ipv6=1
runcmd:
- netplan --debug apply
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
- sysctl -w net.ipv6.conf.default.disable_ipv6=1
- apt -y autoremove
- apt-get update
- apt-get remove docker docker-engine docker.io
- apt install docker.io -y
- systemctl enable docker
- systemctl start docker
timezone: Europe/Brussels
system_info:
  default_user:
    name: default-user
    lock_passwd: false
    sudo: ["ALL=(ALL) NOPASSWD:ALL"]
disable_root: false
ssh_pwauth: yes
users:
  - default
  - name: luc
    gecos: LucD
    lock_passwd: false
    groups: sudo, users, admin
    shell: /bin/bash
    sudo: ['ALL=(ALL) NOPASSWD:ALL']
chpasswd:
  list: |
    default-user:$6$2csGx3AAX0RPRMPB$85sEEZ76z0V93M9pvXGtTFrwlMOaV2VjRHB6IKPRrwc.4qEqcs2EnDzn8oUlcVVb9Biq.Pg7qQchLgNLuetkT0
    luc:$6$.4Kyvju2v5iR$bvZHOntkH9EqgGdMc7fKZKbB132LYHVIFgxN7Ho.mlMGloUayOaKijR6MvLp/ncf4SynTigjaOKGk11EF9gru/
    root:$6$m1TXY60tUAViCYb1$/w.CqV50.M0Nlt6mx2726.bxWZ7/TCqWjOOw4S3h1H8AN.SPWQnKoASXth2E3euBXwebU4Q7dpoGU1SiQontT1
  expire: false
package_upgrade: true
package_reboot_if_required: true</pre>



<h4 class="wp-block-heading">Annotations</h4>



<p><strong>Line 33-36</strong>: Install the docker package, start it and make sure it runs after a reboot.</p>



<p><strong>Line 33</strong>: If there should already be a docker package installed, this line removes that package. If no docker package is installed, the line does nothing.</p>



<h3 class="wp-block-heading">Photon</h3>



<p>The Photon OVA we use comes with docker installed. No need for any additional commands in the YAML file.</p>



<h2 class="wp-block-heading">Image microsoft/powershell</h2>



<p>In the following sections we will run a number of scripts in the microsoft/powershell container on Ubuntu and Photon. Only when there is a difference between both platforms, will the text mention the platforms.</p>



<h3 class="wp-block-heading">A Simple Run</h3>



<p>Let&#8217;s try running a simple PowerShell script in a container on the station.</p>



<pre class="lang:ps decode:true  ">$vmName = 'ubuntubioniccontainer'
$code = @'
sudo docker run --rm microsoft/powershell:latest pwsh -C 'whoami;Get-Process;pwd'
'@

$credItem = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credItem.User, (ConvertTo-SecureString $credItem.Password -AsPlainText -Force))

$sInvoke = @{
    VM = $vmName
    GuestCredential = $cred
    ScriptType = 'bash'
    ScriptText = $code
    NoIPinCert = $true
    Verbose = $true
}
Invoke-VMScriptPlus @sInvoke
</pre>



<h4 class="wp-block-heading">Annotations</h4>



<p><strong>Line 3</strong>: The docker command we sent to the station. The details of the different parts in this line are explained in the following table.</p>




<table id="tablepress-2" class="tablepress tablepress-id-2">
<tbody>
<tr class="row-1">
	<td class="column-1">run --rm</td><td class="column-2">Run a command in a container and clean up when the container exits</td>
</tr>
<tr class="row-2">
	<td class="column-1">microsoft/powershell:latest</td><td class="column-2">The name of the Image. In this case, the latest Microsoft provided stable PowerShell image</td>
</tr>
<tr class="row-3">
	<td class="column-1">pwsh -C</td><td class="column-2">The command and argument we send to the container. In this case we start PowerShell and use the Command parameter</td>
</tr>
<tr class="row-4">
	<td class="column-1">whoami;Get-Process;pwd</td><td class="column-2">A series of commands, separated by semi-columns, that will be executed by ‘pwsh’</td>
</tr>
</tbody>
</table>
<!-- #tablepress-2 from cache -->



<p>The output will be something like this.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="855" height="1024" src="https://www.lucd.info/wp-content/uploads/2019/12/crun1-855x1024.png" alt="" class="wp-image-6984" srcset="https://www.lucd.info/wp-content/uploads/2019/12/crun1-855x1024.png 855w, https://www.lucd.info/wp-content/uploads/2019/12/crun1-251x300.png 251w, https://www.lucd.info/wp-content/uploads/2019/12/crun1-768x919.png 768w, https://www.lucd.info/wp-content/uploads/2019/12/crun1-720x862.png 720w, https://www.lucd.info/wp-content/uploads/2019/12/crun1.png 944w" sizes="auto, (max-width: 855px) 100vw, 855px" /></figure>



<p>Notice how docker will search for and install the image (since it isn&#8217;t present). That is the output in the <mark>yellow</mark> box. The actual output of the commands we sent to <em>pwsh</em>, are in the <span style="background-color:#00d084" class="tadv-background-color">green</span> box.</p>



<p>The second time we run the same script, we will only see the actual output of the commands. And that&#8217;s because the image is already present.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="910" height="524" src="https://www.lucd.info/wp-content/uploads/2019/12/crun2.png" alt="" class="wp-image-6985" srcset="https://www.lucd.info/wp-content/uploads/2019/12/crun2.png 910w, https://www.lucd.info/wp-content/uploads/2019/12/crun2-300x173.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/crun2-768x442.png 768w, https://www.lucd.info/wp-content/uploads/2019/12/crun2-720x415.png 720w" sizes="auto, (max-width: 910px) 100vw, 910px" /></figure>



<h3 class="wp-block-heading">Modules</h3>



<p>Time to start diving into PowerCLI. For that we need to install the VMware PowerCLI modules of course.</p>



<pre class="lang:ps decode:true ">$vmName = 'ubuntubioniccontainer'

$script = @'
$ProgressPreference = "SilentlyContinue"
Install-Module -Name VMware.PowerCLI -AllowClobber -Force | Out-Null
Get-Module -Name VMware* -ListAvailable
'@

$docker = @'
sudo docker run --rm microsoft/powershell:latest pwsh -C '$($script.Replace("<code>r",'').Split("</code>n") -join ';')'
'@

$credItem = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credItem.User, (ConvertTo-SecureString $credItem.Password -AsPlainText -Force))

$sInvoke = @{
    VM = $vmName
    GuestCredential = $cred
    ScriptType = 'bash'
    ScriptText = $ExecutionContext.InvokeCommand.ExpandString($docker)
    NoIPinCert = $true
    Verbose = $true
}
Invoke-VMScriptPlus @sInvoke
</pre>



<h4 class="wp-block-heading">Annotations</h4>



<p><strong>Line 3-7</strong>: We use a here-string for the script we want to execute under <em>pwsh</em> in the container.</p>



<p><strong>Line 9-11</strong>: And another here-string for the <em>docker</em> command. </p>



<p><strong>Line 10</strong>: The script is running on a Windows box, hence the &lt;CR&gt;&lt;LF&gt; to &lt;LF&gt;. The second manipulation is to convert the array of lines to a single line, where the lines are separated with a semi-column. Note that the actual conversion does not happen here.</p>



<p><strong>Line 20</strong>: The script uses the ExpandString method to perform the text manipulations we described in the comments on <strong>Line 10</strong>. Also note that we don&#8217;t have to escape the dollar-signs in the $script part, since the variable substitution will not go that deep.</p>



<p>The result (don&#8217;t mind the funny characters for now).</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="937" height="794" src="https://www.lucd.info/wp-content/uploads/2019/12/crun3.png" alt="" class="wp-image-6992" srcset="https://www.lucd.info/wp-content/uploads/2019/12/crun3.png 937w, https://www.lucd.info/wp-content/uploads/2019/12/crun3-300x254.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/crun3-768x651.png 768w, https://www.lucd.info/wp-content/uploads/2019/12/crun3-720x610.png 720w" sizes="auto, (max-width: 937px) 100vw, 937px" /></figure>



<h3 class="wp-block-heading">ScreenSpace</h3>



<p>When we use all the defaults in the container, we seem to end up with a simple screen that has the default 80 x 25 dimensions. For better results, we can introduce bigger screens inside the container.</p>


<pre class="urvanov-syntax-highlighter-plain-tag">$vmName = 'ubuntubioniccontainer'

$script = @'
reset -w
$ProgressPreference = "SilentlyContinue"
Install-Module -Name VMware.PowerCLI -AllowClobber -Force | Out-Null
Get-Module -Name VMware* -ListAvailable
'@
$docker = @'
sudo docker run --rm --tty \
    -e TERM=xterm -e LINES=25 -e COLUMNS=135 \
    microsoft/powershell:latest \
    pwsh -C '$($script.Replace("`r",'').Split("`n") -join ';')'
'@

$credVM = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credVM.User, (ConvertTo-SecureString $credVM.Password -AsPlainText -Force))

$sInvoke = @{
    VM = $vmName
    GuestCredential = $cred
    ScriptType = 'bash'
    ScriptText = $ExecutionContext.InvokeCommand.ExpandString($docker)
    NoIPinCert = $true
    Verbose = $true
}
Invoke-VMScriptPlus @sInvoke</pre>


<h4 class="wp-block-heading">Annotations</h4>



<p><strong>Line 4</strong>: The <em>reset -w</em> command is a fix for an <a rel="noreferrer noopener" aria-label="issue (opens in a new tab)" href="https://github.com/moby/moby/issues/33794" target="_blank">issue</a> that Docker seems to have when a <strong>pseudo-terminal</strong> (the <strong>&#8211;tty</strong> parameter) is used. With the <em>reset -w</em> command, the terminal will reread the content of <strong>setupterm</strong> and adjust the lines and columns accordingly.</p>



<p><strong>Line 10</strong>: The <strong>&#8211;tty</strong> parameter allocates a pseudo-terminal to the container.</p>



<p><strong>Line 11</strong>: The -e parameter is used to set a number of environment variables for the container. In this case, specify that the container shall use an <strong>xterm</strong> with <strong>dimensions 25 x 135</strong>.</p>



<p>The changed screen is visible in the output (compare it with the width of the previous output).</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="521" src="https://www.lucd.info/wp-content/uploads/2019/12/crun4-1024x521.png" alt="" class="wp-image-7006" srcset="https://www.lucd.info/wp-content/uploads/2019/12/crun4-1024x521.png 1024w, https://www.lucd.info/wp-content/uploads/2019/12/crun4-300x153.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/crun4-768x391.png 768w, https://www.lucd.info/wp-content/uploads/2019/12/crun4-1536x782.png 1536w, https://www.lucd.info/wp-content/uploads/2019/12/crun4-720x367.png 720w, https://www.lucd.info/wp-content/uploads/2019/12/crun4.png 1756w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h3 class="wp-block-heading">Volumes</h3>



<p>With the method we just showed, we will have to incur the overhead of installing the VMware PowerCLI modules on each run, because we throw away the container after we used it.</p>



<p>But there is a solution for this, by using <a href="https://docs.docker.com/engine/reference/commandline/volume_create/" target="_blank" rel="noreferrer noopener" aria-label="docker volumes (opens in a new tab)">docker volumes</a>. They are kept inside the guest OS of the instance and can be attached to each container we run.</p>





<pre class="lang:ps decode:true  ">$vmName = 'ubuntubioniccontainer'

$script = @'
reset -w
\`$ProgressPreference = "SilentlyContinue"
if(-not (Get-Module -Name VMware.PowerCLI -ListAvailable)){
    Install-Module -Name VMware.PowerCLI -AllowClobber -Force | Out-Null
}
\`$WarningPreference = 'SilentlyContinue'
\`$sConf = @{
    InvalidCertificateAction = 'Ignore'
    ParticipateInCeip = \`$false
    DisplayDeprecationWarnings = \`$false
    Scope = 'User'
    Confirm = \`$false
}
Set-PowerCLIConfiguration @sConf | Out-Null
Connect-VIServer -Server $vcsaName -User $vcsaUser -Password $vcsaPswd | Out-Null
Get-VMHost
Disconnect-VIServer -Server $vcsaName -Confirm:\`$false
'@

$docker = @'
sudo docker volume create PCLIModules &gt; /dev/null
cat &gt; ~/script.ps1 &lt;&lt; EOF
$($script.Split("<code>r"))
EOF
sudo docker run --rm \
    --tty \
    -v /PCLIModules:/root/.local/share/powershell/Modules \
    -v ~:/Scripts \
    -e TERM=xterm -e LINES=25 -e COLUMNS=135 \
    microsoft/powershell:latest \
    pwsh -F /Scripts/script.ps1
rm ~/script.ps1
'@

$vcsaName = 'vcsa.local.lab'
$credVCSA = Get-VICredentialStoreItem -Host $vcsaName
$vcsaUser = $credVCSA.User
$vcsaPswd = $credVCSA.Password

$credVM = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credVM.User, (ConvertTo-SecureString $credVM.Password -AsPlainText -Force))

#$credItem = Get-VICredentialStoreItem -Host $vmName
#$cred = New-Object System.Management.Automation.PSCredential ($credItem.User, (ConvertTo-SecureString $credItem.Password -AsPlainText -Force))
$cred = New-Object System.Management.Automation.PSCredential ($vmUser, (ConvertTo-SecureString $vmPswd -AsPlainText -Force))

$dockerExpanded = $ExecutionContext.InvokeCommand.ExpandString($docker)
$sInvoke = @{
    VM = $vmName
    GuestCredential = $cred
    ScriptType = 'bash'
    ScriptText = $ExecutionContext.InvokeCommand.ExpandString($dockerExpanded)
    NoIPinCert = $true
}
Invoke-VMScriptPlus @sInvoke</pre>



<h4 class="wp-block-heading">Annotations</h4>



<p><strong>Line 5</strong>: We have some 'Inception' going on here, hence the '</code>' sequence before the dollar sign. In <strong>line 50</strong> we substitute the variables in the bash script itself. That way we can provide the script as an inline file in the bash script. But since bash also interprets that back-tick, we have to escape it. The back-tick itself is to avoid variable substitution that is done in line 55.</p>



<p><strong>Line 24</strong>: We create a docker volume named <strong>PCLIModules</strong>. If that volume already exists, the bash script will just continue.</p>



<p><strong>Line 25-27</strong>: Instead of having the PowerShell/PowerCLI script in the <strong>Command</strong> parameter of the <em>pwsh</em> command, we use an inline file. This effectively creates a .ps1 file in the home directory of the user in the guest OS.</p>



<p><strong>Line 30</strong>: The Volume parameter on the docker run command maps the docker volume <strong>PCLIModules</strong> to the folder  <strong>/root/.local/share/powershell/Modules</strong> in the container. With an Install-Module, the modules will be installed in this  <strong>/root/.local/share/powershell/Modules</strong> , and thus be stored on the docker volume <strong>PCLIModules</strong>. This volume can be reused on subsequent runs of the container, eliminating the need to install the VMware PowerCLI modules on each run.</p>



<p><strong>Line 31</strong>: On the docker run command we map the <strong>user's home directory</strong> (~) in the guest OS to a folder, named <strong>Scripts</strong> in the container. </p>



<p><strong>Line 34</strong>: The <em>pwsh</em> command that runs in the container uses the <strong>File</strong> parameter to run the .ps1 file created in lines 25-27.</p>



<p><strong>Line 35</strong>: The .ps1 file that was created before calling the <em>docker run</em> command, is now <strong>removed</strong>.</p>



<p><strong>Line 55</strong>: The second substitution round. In this case we substitute primarily all non-escaped (back-tick) dollar sign in the PowerShell script (at this point in the script present as an inline file in the bash script).</p>



<p>The result when we run the script.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="790" src="https://www.lucd.info/wp-content/uploads/2019/12/crun5-1024x790.png" alt="" class="wp-image-7028" srcset="https://www.lucd.info/wp-content/uploads/2019/12/crun5-1024x790.png 1024w, https://www.lucd.info/wp-content/uploads/2019/12/crun5-300x231.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/crun5-768x593.png 768w, https://www.lucd.info/wp-content/uploads/2019/12/crun5-720x556.png 720w, https://www.lucd.info/wp-content/uploads/2019/12/crun5.png 1274w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>Notice how the <strong>ScriptText</strong> property contains the bash script as it is presented to the Guest OS of the instance. In there the dollar signs are escaped (with a back-slash) to <strong>avoid substitution</strong> by bash.</p>



<h3 class="wp-block-heading">ANSI codes and Existing Scripts</h3>



<p>In this section, we will tackle two subjects.</p>



<ul class="wp-block-list"><li>Eliminate those 'funny' characters in the output</li><li>How to run my existing script, similar to what we did in <a rel="noreferrer noopener" aria-label="Part 4 - Running Script (opens in a new tab)" href="https://www.lucd.info/2019/12/09/cloud-init-part-4-running-scripts/" target="_blank">Part 4 - Running Script</a>s.</li></ul>



<p>As it turns out, these 'funny characters' are <a rel="noreferrer noopener" aria-label="ANSI escape codes (opens in a new tab)" href="https://en.wikipedia.org/wiki/ANSI_escape_code" target="_blank">ANSI escape codes</a>. In the container, we are using a pseudo-terminal, and as a result, the OS in the container sends those ANSI escape codes to position the cursor, change the text colour, but also those '...' (three dots) when PowerShell has to cut short the value of a property.</p>



<p>For now, the only way if found to eliminate those ANSI escape codes, is to find and replace them.</p>



<p>For using your existing scripts in this setup, we adopt the same strategy as we did in <a href="https://www.lucd.info/2019/12/09/cloud-init-part-4-running-scripts/" target="_blank" rel="noreferrer noopener" aria-label="Part 4 - Running Scripts (opens in a new tab)">Part 4 - Running Scripts</a>.</p>



<p>Our existing script, in <strong>Sample-PCLI-Script3.ps1</strong>, looks like this.</p>



<pre class="lang:ps decode:true  ">Get-VMHost |
    Select Name, NumCpu, Version |
    ConvertTo-Html
</pre>



<p>And the script to run all this looks like this</p>



<pre class="lang:ps decode:true ">$vmName = 'ubuntubioniccontainer'

$prefix = @'
reset -w
<code>$ProgressPreference = "SilentlyContinue"
if(-not (Get-Module -Name VMware.PowerCLI -ListAvailable)){
    Install-Module -Name VMware.PowerCLI -AllowClobber -Force | Out-Null
}
</code>$WarningPreference = 'SilentlyContinue'
<code>$sConf = @{
    InvalidCertificateAction = 'Ignore'
    ParticipateInCeip = </code>$false
    DisplayDeprecationWarnings = <code>$false
    Scope = 'User'
    Confirm = </code>$false
}
Set-PowerCLIConfiguration @sConf | Out-Null
Connect-VIServer -Server $vcsaName -User $vcsaUser -Password $vcsaPswd | Out-Null
'@
$script = Get-Content -Path .\Sample-PCLI-Script3.ps1
$suffix = @'
Disconnect-VIServer -Server $vcsaName -Confirm:<code>$false
'@

$docker = @'
sudo docker volume create PCLIModules &gt; /dev/null
cat &gt; ~/script.ps1 &lt;&lt; 'EOF'
$($prefix.Split("</code>r"))
$($script -join "<code>n")
$($suffix.Split("</code>r"))
EOF
sudo docker run --rm \
    --tty \
    -v /PCLIModules:/root/.local/share/powershell/Modules \
    -v ~:/Scripts \
    -e TERM=xterm -e LINES=25 -e COLUMNS=135 \
    microsoft/powershell:latest \
    pwsh -F /Scripts/script.ps1 &gt; ~/result.txt
cat ~/result.txt
rm ~/result.txt
rm ~/script.ps1
'@

$vcsaName = 'vcsa.local.lab'
$credVCSA = Get-VICredentialStoreItem -Host $vcsaName
$vcsaUser = $credVCSA.User
$vcsaPswd = $credVCSA.Password

$credVM = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credVM.User, (ConvertTo-SecureString $credVM.Password -AsPlainText -Force))

$dockerExpanded = $ExecutionContext.InvokeCommand.ExpandString($docker)
$sInvoke = @{
    VM = $vmName
    GuestCredential = $cred
    ScriptType = 'bash'
    ScriptText = $ExecutionContext.InvokeCommand.ExpandString($dockerExpanded)
    NoIPinCert = $true
    Verbose = $true
}
$result = Invoke-VMScriptPlus @sInvoke

# Remove ANSI Escape Sequences
($result.ScriptOutput.Split("<code>n") |
    Foreach-Object -Process {
        ($_ -replace "\xe2\x80\xa6","…") -replace "\x1b\[\?[1-9]h|\x1b\[[1-9]n|\x1b=|\x1b\]0;|\x07|\x1b\]0;", ''
    }) -join "</code>n"
</pre>



<h4 class="wp-block-heading">Annotations</h4>



<p><strong>Line 3, 20, 21</strong>: We split the code into three parts. A fixed prefix, the code of our existing script and a suffix.</p>



<p><strong>Line 27-31</strong>: We use the same concept of an inline file as we did in the previous section. The only difference is that we now have three parts instead of one.</p>



<p><strong>Line 64-67</strong>: The ANSI escape sequences are <strong>removed</strong> from the output. The <strong>RegEx expression</strong> does not cover all possible ANSI escape sequences, but it did the job for all the ones I encountered. On the other hand, the RegEx expression can easily be expanded.</p>



<p>The result, which is an HTML file, looks like this.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="357" height="209" src="https://www.lucd.info/wp-content/uploads/2019/12/crun6.png" alt="" class="wp-image-7043" srcset="https://www.lucd.info/wp-content/uploads/2019/12/crun6.png 357w, https://www.lucd.info/wp-content/uploads/2019/12/crun6-300x176.png 300w" sizes="auto, (max-width: 357px) 100vw, 357px" /></figure>



<p>The part where we filter away the ANSI code sequence is also used to translate some well-known characters. Remember the funny characters in the module list?</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="785" height="544" src="https://www.lucd.info/wp-content/uploads/2019/12/mod1.png" alt="" class="wp-image-7050" srcset="https://www.lucd.info/wp-content/uploads/2019/12/mod1.png 785w, https://www.lucd.info/wp-content/uploads/2019/12/mod1-300x208.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/mod1-768x532.png 768w, https://www.lucd.info/wp-content/uploads/2019/12/mod1-720x499.png 720w" sizes="auto, (max-width: 785px) 100vw, 785px" /></figure>



<p>With the first -replace operator in the previous code, we can change this back to the character we are used to. The hex sequence 0xE2, 0x80, 0xA6 that get back is, in fact, the code for the horizontal ellipsis, the 3 horizontal dots PowerShell uses to indicate a cut off value.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="763" height="540" src="https://www.lucd.info/wp-content/uploads/2019/12/mod2.png" alt="" class="wp-image-7051" srcset="https://www.lucd.info/wp-content/uploads/2019/12/mod2.png 763w, https://www.lucd.info/wp-content/uploads/2019/12/mod2-300x212.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/mod2-720x510.png 720w" sizes="auto, (max-width: 763px) 100vw, 763px" /></figure>



<p>There are most probably many other characters or character combinations that do not translate well, but at least we have a method to correct this.</p>



<h3 class="wp-block-heading">InFile &amp; OutFile</h3>



<p>Using inline files and the stdout stream for input and results works in most cases, but it might make things needlessly complex in others. To avoid that, we can make use of the <strong>InFile</strong> and <strong>OutFile</strong> parameters on the <a rel="noreferrer noopener" aria-label="Invoke-VMScriptPlus (opens in a new tab)" href="https://www.lucd.info/2019/11/17/invoke-vmscriptplus-v3/" target="_blank">Invoke-VMScriptPlus</a> function. The parameters allows us to pass one or more files along with the script, and retrieve one or more files back from the environment where the script runs.</p>



<p>If we use <strong>InFile</strong> and <strong>OutFile</strong> together with containers, there is some <a rel="noreferrer noopener" aria-label="Inception (opens in a new tab)" href="https://en.wikipedia.org/wiki/Inception" target="_blank">Inception</a> going on. The following schematic might make this a bit more visible.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="995" height="455" src="https://www.lucd.info/wp-content/uploads/2019/12/inception.png" alt="" class="wp-image-7055" srcset="https://www.lucd.info/wp-content/uploads/2019/12/inception.png 995w, https://www.lucd.info/wp-content/uploads/2019/12/inception-300x137.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/inception-768x351.png 768w, https://www.lucd.info/wp-content/uploads/2019/12/inception-720x329.png 720w" sizes="auto, (max-width: 995px) 100vw, 995px" /></figure>



<p>As you can see, the <strong>InFile</strong> files are stored in the guest OS of the instance. And then through the Volume parameter on the docker command, passed along into the container.</p>



<p>The same goes for the <strong>OutFile</strong> files but in the reverse sense.</p>



<p>We are going to execute an existing script. Note how to make a <strong>small change</strong> in the path of the resulting file. We have to use the path to the <strong>Scripts</strong> folder, which will be mapped through the <strong>Volume</strong> parameter.</p>



<pre class="lang:ps decode:true ">Get-VMHost |
Select Name, NumCpu, Version |
ConvertTo-Html | Out-File -FilePath \Scripts\report.html

Write-Host "Script completed"
</pre>



<p>And this is the script that will run that script in a container. </p>



<pre class="lang:ps decode:true ">$vmName = 'ubuntubioniccontainer'
$oldScript = '.\Sample-PCLI-Script.ps1'
$tempScript = 'script.ps1'

$script = @'
reset -w
<code>$ProgressPreference = "SilentlyContinue"
if(-not (Get-Module -Name VMware.PowerCLI -ListAvailable)){
    Install-Module -Name VMware.PowerCLI -AllowClobber -Force | Out-Null
}
</code>$WarningPreference = 'SilentlyContinue'
<code>$sConf = @{
    InvalidCertificateAction = 'Ignore'
    ParticipateInCeip = </code>$false
    DisplayDeprecationWarnings = <code>$false
    Scope = 'User'
    Confirm = </code>$false
}
Set-PowerCLIConfiguration @sConf | Out-Null
Connect-VIServer -Server $vcsaName -User $vcsaUser -Password $vcsaPswd | Out-Null

####### My Script ######
$(Get-Content -Path $oldScript -Raw)
########################

Disconnect-VIServer -Server $vcsaName -Confirm:<code>$false
'@

$docker = @'
sudo docker volume create PCLIModules &gt; /dev/null
sudo docker run --rm \
    --tty \
    -v /PCLIModules:/root/.local/share/powershell/Modules \
    -v </code>$(pwd):/Scripts \
    -e TERM=xterm -e LINES=25 -e COLUMNS=135 \
    microsoft/powershell:latest \
    pwsh -F /Scripts/$($tempScript)
'@

$vcsaName = 'vcsa.local.lab'
$credVCSA = Get-VICredentialStoreItem -Host $vcsaName
$vcsaUser = $credVCSA.User
$vcsaPswd = $credVCSA.Password

$credVM = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credVM.User, (ConvertTo-SecureString $credVM.Password -AsPlainText -Force))

$tempScript = 'script.ps1'
$ExecutionContext.InvokeCommand.ExpandString($script) | Out-File -LiteralPath $tempScript
$sInvoke = @{
    VM = $vmName
    GuestCredential = $cred
    ScriptType = 'bash'
    ScriptText = $ExecutionContext.InvokeCommand.ExpandString($docker)
    NoIPinCert = $true
    InFile = $tempScript
    OutFile = 'report.html'
}
Invoke-VMScriptPlus @sInvoke

Remove-Item -LiteralPath $tempScript -Confirm:$false

Invoke-Item -Path .\report.html
</pre>



<h4 class="wp-block-heading">Annotations</h4>



<p><strong>Line 2</strong>: The name of our script we want to execute. Note that we had to change the <strong>Path</strong> of the resulting file. This to allow the mapping through the <strong>volume</strong> parameter on the <em>docker run</em> command.</p>



<p><strong>Line 3</strong>: A filename for the script we are going to run in the container. Ultimately this could be replaced by a randomly generated filename.</p>



<p><strong>Line 5-27</strong>: The full script we are sending to the container. It is comprised of the Prefix, our original script and the Suffix. The Prefix and Suffix are the same as we have shown in previous sections. The only difference is that all three parts now come together in one here-string.</p>



<p><strong>Line 23</strong>: We substitute this line with our original script later on with the call to <strong>ExpandString</strong>.</p>



<p><strong>Line 29-38</strong>: The actual bash script, with the docker commands, that we are running inside the Guest OS of the instance.</p>



<p><strong>Line 49</strong>: We expand the $script variable and save the resulting text to a file.</p>



<p><strong>Line 54</strong>: Another call to <strong>ExpandString</strong>. This time to substitute the variables in the <strong>$docker</strong> variable. </p>



<p><strong>Line 56</strong>: The script file we send along to the Guest OS.</p>



<p><strong>Line 57</strong>: The output file we retrieve from the Guest OS.</p>



<p>The script's output looks like this. Notice that it only contains the result from the Write-Host line in our original script.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="719" height="468" src="https://www.lucd.info/wp-content/uploads/2019/12/out5.png" alt="" class="wp-image-7057" srcset="https://www.lucd.info/wp-content/uploads/2019/12/out5.png 719w, https://www.lucd.info/wp-content/uploads/2019/12/out5-300x195.png 300w" sizes="auto, (max-width: 719px) 100vw, 719px" /></figure>



<p>And the result, report.html, is displayed in a web browser through the Invoke-Item cmdlet.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="338" height="203" src="https://www.lucd.info/wp-content/uploads/2019/12/out6.png" alt="" class="wp-image-7058" srcset="https://www.lucd.info/wp-content/uploads/2019/12/out6.png 338w, https://www.lucd.info/wp-content/uploads/2019/12/out6-300x180.png 300w" sizes="auto, (max-width: 338px) 100vw, 338px" /></figure>



<h2 class="wp-block-heading">Image vmware/powerclicore </h2>



<p>Shortly after the new <strong>tdnf</strong> package for <strong>PowerShell</strong> was made available, a new version of the <a href="https://hub.docker.com/r/vmware/powerclicore/" target="_blank" rel="noreferrer noopener" aria-label="vmware/powerclicore (opens in a new tab)">vmware/powerclicore</a> docker image was created.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="595" height="274" src="https://www.lucd.info/wp-content/uploads/2019/12/image.png" alt="" class="wp-image-7076" srcset="https://www.lucd.info/wp-content/uploads/2019/12/image.png 595w, https://www.lucd.info/wp-content/uploads/2019/12/image-300x138.png 300w" sizes="auto, (max-width: 595px) 100vw, 595px" /></figure>



<p>The difference with the <a rel="noreferrer noopener" aria-label="microsoft/powershell (opens in a new tab)" href="https://hub.docker.com/_/microsoft-powershell" target="_blank">microsoft/powershell</a> image is the underlying OS, Photon v3R2 instead of Ubuntu 18.04.</p>



<p>Let's start with a simple script, similar to the one we used earlier.</p>



<pre class="lang:ps decode:true  ">$code = @'
docker run --rm microsoft/powershell:latest pwsh -C 'whoami;Get-Process;pwd'
'@

$credItem = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credItem.User, (ConvertTo-SecureString $credItem.Password -AsPlainText -Force))

$sInvoke = @{
    VM = $vmName
    GuestCredential = $cred
    ScriptType = 'bash'
    ScriptText = $code
    NoIPinCert = $true
    GuestOSType = 'linux'
}
Invoke-VMScriptPlus @sInvoke | select -ExpandProperty ScriptOutput
</pre>



<p>With the <strong>microsoft/powershell image</strong> this produces the following. We see the result of all three commands.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="662" height="254" src="https://www.lucd.info/wp-content/uploads/2019/12/cmp1.png" alt="" class="wp-image-7080" srcset="https://www.lucd.info/wp-content/uploads/2019/12/cmp1.png 662w, https://www.lucd.info/wp-content/uploads/2019/12/cmp1-300x115.png 300w" sizes="auto, (max-width: 662px) 100vw, 662px" /></figure>



<p>On the <strong>vmware/powerclicore</strong> image, the result is a bit different. The <em>whoami</em> output is there, but the output from the other two commands is missing. </p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="530" height="122" src="https://www.lucd.info/wp-content/uploads/2019/12/cmp2-2.png" alt="" class="wp-image-7087" srcset="https://www.lucd.info/wp-content/uploads/2019/12/cmp2-2.png 530w, https://www.lucd.info/wp-content/uploads/2019/12/cmp2-2-300x69.png 300w" sizes="auto, (max-width: 530px) 100vw, 530px" /></figure>



<p>As it turns out, there is an issue when passing commands to pwsh via the Command parameter. When using the File parameter everything seems to work well.</p>



<p>One difference to note, compared with the micorosft/powershell image, the vmware/powerclicore:latest image, and the Photon OS in that image, have minimal terminal types present. As a result, you will need to set the TERM environment variable to <strong>linux</strong> instead of <strong>xterm</strong>.</p>



<p>A typical call would look something like this.</p>



<pre class="lang:sh decode:true  ">docker volume create PCLIModules &gt; /dev/null
docker run --rm \
    --tty \
    -v /PCLIModules:/root/.local/share/powershell/Modules \
    -v `$(pwd):/Scripts \
    -e TERM=linux -e LINES=25 -e COLUMNS=135 \
    vmware/powerclicore:latest \
    pwsh -F /Scripts/script.ps1</pre>



<h2 class="wp-block-heading">Image  mcr.microsoft.com/powershell:preview </h2>



<p>Note that there is also an image for the latest PS v7 Preview available. If you want to test/develop your scripts in this PS v7 Preview, this is a simple and easy method to do it.</p>



<p>All previous methods discussed in this post work against this Preview.</p>



<p>Enjoy!</p>



<p>p</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.lucd.info/2019/12/11/cloud-init-part-5-running-containers/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>Cloud-init &#8211; Part 4 &#8211; Running Scripts</title>
		<link>https://www.lucd.info/2019/12/09/cloud-init-part-4-running-scripts/</link>
					<comments>https://www.lucd.info/2019/12/09/cloud-init-part-4-running-scripts/#comments</comments>
		
		<dc:creator><![CDATA[LucD]]></dc:creator>
		<pubDate>Mon, 09 Dec 2019 14:08:44 +0000</pubDate>
				<category><![CDATA[Cloud-init]]></category>
		<category><![CDATA[Photon]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[Scripts]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[cloud-init]]></category>
		<category><![CDATA[scripts]]></category>
		<category><![CDATA[VMware PowerCLI]]></category>
		<guid isPermaLink="false">http://www.lucd.info/?p=6697</guid>

					<description><![CDATA[For now, the second to last part in this series. And although technically [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>For now, the second to last part in this series. And although technically not a &#8216;<em>real</em>&#8216; <a href="https://cloudinit.readthedocs.io/en/latest/" target="_blank" rel="noopener noreferrer">cloud-init</a> post, I consider it related to <a href="https://www.lucd.info/2019/12/06/cloud-init-part-1-the-basics/" target="_blank" rel="noopener noreferrer">Part 1</a>, <a href="https://www.lucd.info/2019/12/07/cloud-init-part-2-advanced-ubuntu/" target="_blank" rel="noopener noreferrer">Part 2</a> and <a href="https://www.lucd.info/2019/12/08/cloud-init-part-3-photon-os/" target="_blank" rel="noopener noreferrer">Part3</a> in this series. In this post I&#8217;ll show how you can run <strong>scripts</strong> on these &#8216;<em>cattle</em>&#8216; stations we just deployed.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="721" height="427" src="https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-4-2.png" alt="" class="wp-image-6907" srcset="https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-4-2.png 721w, https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-4-2-300x178.png 300w" sizes="auto, (max-width: 721px) 100vw, 721px" /></figure>



<span id="more-6697"></span>



<h2 class="wp-block-heading">Introduction</h2>



<p>The purpose of this post is quite straightforward. How can we use the &#8216;<em>cattle</em>&#8216; stations we created to run our PowerShell/PowerCLI scripts?</p>



<p>Since all the stations we deployed with cloud-init come with the <a rel="noreferrer noopener" aria-label="VMware Tools (opens in a new tab)" href="https://docs.vmware.com/en/VMware-Tools/index.html" target="_blank">VMware Tools</a> installed, we can use the <a rel="noreferrer noopener" aria-label="GuestOperations (opens in a new tab)" href="https://vdc-download.vmware.com/vmwb-repository/dcr-public/790263bc-bd30-48f1-af12-ed36055d718b/e5f17bfc-ecba-40bf-a04f-376bbb11e811/vim.vm.guest.GuestOperationsManager.html" target="_blank">GuestOperations</a>, and all cmdlets and functions based on these <strong>API</strong>, to communicate with the Guest OS.</p>



<p>In practice, this means that the <a rel="noreferrer noopener" aria-label="Invoke-VMScript (opens in a new tab)" href="https://vdc-repo.vmware.com/vmwb-repository/dcr-public/6fb85470-f6ca-4341-858d-12ffd94d975e/4bee17f3-579b-474e-b51c-898e38cc0abb/doc/Invoke-VMScript.html" target="_blank">Invoke-VMScript</a> cmdlet should be used. But unfortunately, the current implementation of that cmdlet has some issues with multi-line bash scripts (and a few other problems).</p>



<p>To avoid those issues I have written my <a rel="noreferrer noopener" aria-label="Invoke-VMScriptPlus (opens in a new tab)" href="https://www.lucd.info/2019/11/17/invoke-vmscriptplus-v3/" target="_blank">Invoke-VMScriptPlus</a> function. Besides fixing the above-mentioned issues, it also adds some extra features that come in handy, as you will notice later on in this post.</p>



<h2 class="wp-block-heading">Local PowerShell Script</h2>



<h3 class="wp-block-heading">Ubuntu</h3>



<p>We start simple. In this post, we will show how you can run your PowerShell scripts locally inside the guest OS of the stations we deployed with cloud-init.</p>



<p>Note that in the examples I use a <a rel="noreferrer noopener" aria-label="VICredentialStoreItem (opens in a new tab)" href="https://vdc-repo.vmware.com/vmwb-repository/dcr-public/6fb85470-f6ca-4341-858d-12ffd94d975e/4bee17f3-579b-474e-b51c-898e38cc0abb/doc/VICredentialStoreItem.html" target="_blank">VICredentialStoreItem</a> to fetch the credentials for the target guest OS. As you might know by now, the VICredentialStoreItem cmdlets are only supported on a <strong>Windows platform</strong> (since they use the <a href="https://en.wikipedia.org/wiki/Data_Protection_API" target="_blank" rel="noreferrer noopener" aria-label="DPAPI (opens in a new tab)">DPAPI</a> methods).</p>



<p>Any which way you get those credentials will of course work. But I consider that not to be part of the cloud-init series of posts.</p>



<p>Back to the task at hand, let&#8217;s use a simple example to start.</p>



<pre class="lang:ps decode:true ">$vmName = 'ubuntubionicps'
$code = @'
$PSVersionTable
'@

$credItem = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credItem.User, (ConvertTo-SecureString $credItem.Password -AsPlainText -Force))

$sInvoke = @{
    VM = $vmName
    GuestCredential = $cred
    ScriptType = 'powershellv6'
    ScriptText = $code
}
Invoke-VMScriptPlus @sInvoke
</pre>



<p>Nothing too complicated at first sight. We sent a PowerShell script to an Ubuntu box. But we do seem to get an error.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="111" src="https://www.lucd.info/wp-content/uploads/2019/12/error1-1024x111.png" alt="" class="wp-image-6858" srcset="https://www.lucd.info/wp-content/uploads/2019/12/error1-1024x111.png 1024w, https://www.lucd.info/wp-content/uploads/2019/12/error1-300x33.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/error1-768x83.png 768w, https://www.lucd.info/wp-content/uploads/2019/12/error1-720x78.png 720w, https://www.lucd.info/wp-content/uploads/2019/12/error1.png 1215w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>That doesn&#8217;t really give us a lot of useful information on what is going on. My first step in analysing such non-obvious errors is to add some <strong>verbosity</strong>.</p>



<pre class="lang:ps decode:true ">$vmName = 'ubuntubionicps'
$code = @'
$PSVersionTable
'@

$credItem = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credItem.User, (ConvertTo-SecureString $credItem.Password -AsPlainText -Force))

$sInvoke = @{
    VM = $vmName
    GuestCredential = $cred
    ScriptType = 'powershellv6'
    ScriptText = $code
    Verbose = $true
}
Invoke-VMScriptPlus @sInvoke
</pre>



<p>And now we get some more clues. Btw, I only listed part of the verbose output that helps in analysing the issue.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="220" src="https://www.lucd.info/wp-content/uploads/2019/12/error2-1024x220.png" alt="" class="wp-image-6863" srcset="https://www.lucd.info/wp-content/uploads/2019/12/error2-1024x220.png 1024w, https://www.lucd.info/wp-content/uploads/2019/12/error2-300x64.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/error2-768x165.png 768w, https://www.lucd.info/wp-content/uploads/2019/12/error2-720x154.png 720w, https://www.lucd.info/wp-content/uploads/2019/12/error2.png 1203w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>The error comes immediately after the PUT where we copy the script text to a file in the guest OS. Since SSL/TLS secure channel is mentioned, let&#8217;s have a look at the certificate that is used on that ESXi node.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="406" height="550" src="https://www.lucd.info/wp-content/uploads/2019/12/cert.png" alt="" class="wp-image-6866" srcset="https://www.lucd.info/wp-content/uploads/2019/12/cert.png 406w, https://www.lucd.info/wp-content/uploads/2019/12/cert-221x300.png 221w" sizes="auto, (max-width: 406px) 100vw, 406px" /></figure>



<p>And there is our answer. The certificate used on the ESXi node in question only contains a SAN for the FQDN, not the IP address.</p>



<p>Since this is something I watched happen at several sites, I added the <strong>NoIPinCert</strong> switch to the Invoke-VMScriptPlus function. When that switch is set, the function will take the IP address and replace it by the FQDN. This, of course, assumes that DNS resolution is working!</p>



<pre class="lang:ps decode:true ">$vmName = 'ubuntubionicps'
$code = @'
$PSVersionTable
'@

$credItem = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credItem.User, (ConvertTo-SecureString $credItem.Password -AsPlainText -Force))

$sInvoke = @{
    VM = $vmName
    GuestCredential = $cred
    ScriptType = 'powershellv6'
    ScriptText = $code
    NoIPinCert = $true
}
Invoke-VMScriptPlus @sInvoke
</pre>



<p>And now we get the result we were expecting.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="910" height="553" src="https://www.lucd.info/wp-content/uploads/2019/12/out1.png" alt="" class="wp-image-6867" srcset="https://www.lucd.info/wp-content/uploads/2019/12/out1.png 910w, https://www.lucd.info/wp-content/uploads/2019/12/out1-300x182.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/out1-768x467.png 768w, https://www.lucd.info/wp-content/uploads/2019/12/out1-720x438.png 720w" sizes="auto, (max-width: 910px) 100vw, 910px" /></figure>



<p>Don&#8217;t mind the properties beside ScriptOutput and the funny characters in ScriptOutput for now. We&#8217;ll get to that later.</p>



<h3 class="wp-block-heading">Photon</h3>



<p>And for good measure, let&#8217;s run the same on a <strong>Photon</strong> station.</p>



<pre class="lang:ps decode:true  ">$vmName = 'photonps'
$code = @'
$PSVersionTable
'@

$credItem = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credItem.User, (ConvertTo-SecureString $credItem.Password -AsPlainText -Force))

$sInvoke = @{
    VM = $vmName
    GuestCredential = $cred
    ScriptType = 'powershellv6'
    ScriptText = $code
    NoIPinCert = $true
}
Invoke-VMScriptPlus @sInvoke
</pre>



<p>Confirmed, also a success with a Photon guest OS.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="928" height="566" src="https://www.lucd.info/wp-content/uploads/2019/12/out2.png" alt="" class="wp-image-6876" srcset="https://www.lucd.info/wp-content/uploads/2019/12/out2.png 928w, https://www.lucd.info/wp-content/uploads/2019/12/out2-300x183.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/out2-768x468.png 768w, https://www.lucd.info/wp-content/uploads/2019/12/out2-720x439.png 720w" sizes="auto, (max-width: 928px) 100vw, 928px" /></figure>



<h2 class="wp-block-heading">Local PowerCLI Script</h2>



<p>So we can run PowerShell script, but what about <a href="https://www.vmware.com/support/developer/PowerCLI/" target="_blank" rel="noreferrer noopener" aria-label="VMware PowerCLI (opens in a new tab)">VMware PowerCLI</a>? I want to be able to run my PowerCLI script on such a station.</p>



<p>Since we need to have the modules installed, before we can actually use any of the PowerCLI cmdlets, let&#8217;s include that in our test script.</p>



<pre class="lang:ps decode:true  ">$vmName = 'ubuntubionicps'

$code = @'
<code>$ProgressPreference = "SilentlyContinue"
if(-not (Get-Module -Name 'VMware.PowerCLI' -ListAvailable)){
    Install-Module -Name VMware.PowerCLI -Scope CurrentUser -AllowClobber -Force | Out-Null
}
</code>$WarningPreference = 'SilentlyContinue'
<code>$sConf = @{
    InvalidCertificateAction = 'Ignore'
    ParticipateInCeip = </code>$false
    DisplayDeprecationWarnings = <code>$false
    Scope = 'User'
    Confirm = </code>$false
}
Set-PowerCLIConfiguration @sConf | Out-Null
Connect-VIServer -Server $vcsaName -User $user -Password $pswd | Out-Null
Get-VMHost | Select Name,Version,PowerState,NumCpu,MemoryTotalGB | ConvertTo-Csv -NotypeInformation -UseCulture
Disconnect-VIServer -Server $vcsaName -Confirm:<code>$false
'@

$vcsaName = 'vcsa.local.lab'
$credVCSA = Get-VICredentialStoreItem -Host $vcsaName
$user = $credVCSA.User
$pswd = $credVCSA.Password

$credVM = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credVM.User, (ConvertTo-SecureString $credVM.Password -AsPlainText -Force))

$sInvoke = @{
    VM = Get-VM -Name $vmName
    GuestCredential = $cred
    ScriptType = 'powershellv6'
    ScriptText = $ExecutionContext.InvokeCommand.ExpandString($code)
    NoIPinCert = $true
}
$result = Invoke-VMScriptPlus @sInvoke
$result.ScriptOutput | ConvertFrom-Csv -UseCulture | Format-Table -AutoSize
</pre>



<h4 class="wp-block-heading">Annotations</h4>



<p><strong>Line 3-20</strong>: The script we are executing inside the target guest OS.</p>



<p><strong>Line 4</strong>: Since we are using variable substitution on the script text later on (see <strong>line 34</strong>), we need to escape the dollar signs in the variables we do NOT want to be substituted. This is done through a backtick before the dollar sign.</p>



<p><strong>Line 4</strong>: We mute the progress information. This to avoid unnecessary output during the installation of the PowerCLI modules.</p>



<p><strong>Line 5-7</strong>: A simple test to check if the PowerCLI modules are installed or not. If they aren't, the script will install them. Note that since we are running our scripts locally in the guest OS, once the modules are installed they will stay installed for any future script runs. If you want to start fresh for each script, you will have to deploy a new instance. The 'cattle' principle!</p>



<p><strong>Line 9-16</strong>: We want our script to be as 'silent' as possible, that is why we change some PowerCLI configuration settings. Note that disabling the <strong>CEIP</strong> participation in this example script is a personal choice and you can leave it on without any impact on your script(s).</p>



<p><strong>Line 18</strong>: This sample code will output results in CSV format (<a rel="noreferrer noopener" aria-label="ConvertTo-Csv (opens in a new tab)" href="https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/convertto-csv?view=powershell-6" target="_blank">ConvertTo-Csv</a>) to the console. This will make it easy for the calling script to handle the returned data.</p>



<p><strong>Line 34</strong>: Instead of hard-coding specific values in the script we send to the target station, we use variable substitution to fill in the values at the moment we send the script to the target station. This is done through the <a href="https://docs.microsoft.com/en-us/dotnet/api/system.management.automation.commandinvocationintrinsics.expandstring?view=pscore-6.2.0" target="_blank" rel="noreferrer noopener" aria-label="ExpandString (opens in a new tab)">ExpandString</a> method.</p>



<p><strong>Line 38</strong>: This show how easy it is to receive the result when we have the data converted into a well-known format (CSV in this case).</p>



<p>The script results in the following. Instead of <strong>Format-Table</strong> we could have used any other cmdlet that sends the result to a file, or as the body of an email...</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="671" height="154" src="https://www.lucd.info/wp-content/uploads/2019/12/out3.png" alt="" class="wp-image-6890" srcset="https://www.lucd.info/wp-content/uploads/2019/12/out3.png 671w, https://www.lucd.info/wp-content/uploads/2019/12/out3-300x69.png 300w" sizes="auto, (max-width: 671px) 100vw, 671px" /></figure>



<p>The same script runs in the same way on Ubuntu and Photon. And in both cases produces the same result.</p>



<h2 class="wp-block-heading">Your Existing Scripts</h2>



<p>A question I heard several times when I presented the concept of 'cattle' stations at conferences, was "<em>What will happen with my existing scripts?</em>".</p>



<p>As it turns out, you will not have to do a lot. In most cases, you will able to just take your script and with a bit of PowerShell magic, you can use it in this setup.</p>



<p>Let's take this simple script as an example. And yes, this is exactly the same code as ran before. But we're demonstrating the concept.</p>



<p>We save the following lines as <strong>Sample-PCLI-Script.ps1</strong>, and it is representative for all your existing scripts.</p>



<pre class="lang:ps decode:true ">Get-VMHost | Select Name, Version, PowerState, NumCpu, MemoryTotalGB |<br>ConvertTo-Csv -NotypeInformation -UseCulture
</pre>



<p>We now need to use a slightly different script to submit this to our target station.</p>



<pre class="lang:ps decode:true ">$vmName = 'ubuntubionicps'

$prefix = @'
</code>$ProgressPreference = "SilentlyContinue"
if(-not (Get-Module -Name 'VMware.PowerCLI' -ListAvailable)){
    Install-Module -Name VMware.PowerCLI -Scope CurrentUser -AllowClobber -Force | Out-Null
}
<code>$WarningPreference = 'SilentlyContinue'
</code>$sConf = @{
    InvalidCertificateAction = 'Ignore'
    ParticipateInCeip = <code>$false
    DisplayDeprecationWarnings = </code>$false
    Scope = 'User'
    Confirm = <code>$false
}
Set-PowerCLIConfiguration @sConf | Out-Null
Connect-VIServer -Server $vcsaName -User $user -Password $pswd | Out-Null
'@

$code = Get-Content -Path .\Sample-PCLI-Script.ps1

$suffix = @'
Disconnect-VIServer -Server $vcsaName -Confirm:</code>$false
'@

$vcsaName = 'vcsa.local.lab'
$credVCSA = Get-VICredentialStoreItem -Host $vcsaName
$user = $credVCSA.User
$pswd = $credVCSA.Password

$credVM = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credVM.User, (ConvertTo-SecureString $credVM.Password -AsPlainText -Force))

$sInvoke = @{
    VM = Get-VM -Name $vmName
    GuestCredential = $cred
    ScriptType = 'powershellv6'
    ScriptText = ($ExecutionContext.InvokeCommand.ExpandString($prefix) + "<code>n$code</code>n" + $ExecutionContext.InvokeCommand.ExpandString($suffix))
    NoIPinCert = $true
}
$result = Invoke-VMScriptPlus @sInvoke
$result.ScriptOutput
$result.ScriptOutput | ConvertFrom-Csv -UseCulture | Format-Table -AutoSize
</pre>



<h4 class="wp-block-heading">Annotations</h4>



<p><strong>Line 3-18</strong>: Under the $prefix variable we store the 'fixed' code that prepares the environment. Just as the same part in the previous example.</p>



<p><strong>Line 20</strong>: This is where we read our existing script into a variable.</p>



<p><strong>Line 22-24</strong>: The $suffix variable contains the 'fixed' closing part of the code.</p>



<p><strong>Line 38</strong>: We compose the ScriptText from the three variables $preifx, $code and $suffix. Note that for the $code variable we don't need to do any variable substitution.</p>



<p>Will this work for all your existing scripts?</p>



<p>Probably not, you will need to verify if there are for example any Connect- and/or DisConnect-VIServer lines in your script.</p>



<h2 class="wp-block-heading">Infile and Outfile </h2>



<p>In the previous examples, we always send the result back over the standard output. This is not a feasible solution in all cases. What for example when your script produces multiples outputs? And what when you need to read data as input for your script?</p>



<p>For all those reasons I added the <strong>InFile</strong> and <strong>OutFile</strong> parameters to Invoke-VMScriptPlus. For more details see my <a rel="noreferrer noopener" aria-label="Invoke-VMScriptPLus V3 (opens in a new tab)" href="https://www.lucd.info/2019/11/17/invoke-vmscriptplus-v3/" target="_blank">Invoke-VMScriptPLus V3</a> post.</p>



<p>The following demonstrates, in a simple way, how you can use those InFile and OutFile parameters to run your scripts on a 'cattle' station.</p>



<p>This is our original script, saved as <strong>Sample-PCLI-Script4.ps1</strong>.</p>



<pre class="lang:ps decode:true">$esxName = Import-Csv -Path .\esxnames.csv | Select -ExpandProperty Name
Get-VMHost -Name $esxName | Select Name, NumCpu, Version |
Export-Csv -Path .\report.csv -UseCulture -NoTypeInformation

Write-Host "Script completed"
</pre>



<p>The <strong>esxnames.csv</strong> file contains the following.</p>



<table style="border-collapse: collapse; width: 100%;">
<tbody>
<tr>
<td style="width: 100%;">
<div>Name</div>
</td>
</tr>
<tr>
<td style="width: 100%;">esx1.local.lab</td>
</tr>
<tr>
<td style="width: 100%;">esx3.local.lab</td>
</tr>
</tbody>
</table>



<p>Our calling script. Notice the use of the <strong>InFile</strong> and <strong>OutFile</strong> parameters on <strong>lines 40 and 41</strong>.</p>



<pre class="lang:ps decode:true ">$vmName = 'ubuntubionicps'

$prefix = @'
<code>$ProgressPreference = "SilentlyContinue"
if(-not (Get-Module -Name 'VMware.PowerCLI' -ListAvailable)){
    Install-Module -Name VMware.PowerCLI -Scope CurrentUser -AllowClobber -Force | Out-Null
}
</code>$WarningPreference = 'SilentlyContinue'
<code>$sConf = @{
    InvalidCertificateAction = 'Ignore'
    ParticipateInCeip = </code>$false
    DisplayDeprecationWarnings = <code>$false
    Scope = 'User'
    Confirm = </code>$false
}
Set-PowerCLIConfiguration @sConf | Out-Null
Connect-VIServer -Server $vcsaName -User $user -Password $pswd | Out-Null
'@

$code = Get-Content -Path .\Sample-PCLI-Script4.ps1

$suffix = @'
Disconnect-VIServer -Server $vcsaName -Confirm:<code>$false
'@

$vcsaName = 'vcsa.local.lab'
$credVCSA = Get-VICredentialStoreItem -Host $vcsaName
$user = $credVCSA.User
$pswd = $credVCSA.Password

$credVM = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credVM.User, (ConvertTo-SecureString $credVM.Password -AsPlainText -Force))

$sInvoke = @{
    VM = Get-VM -Name $vmName
    GuestCredential = $cred
    ScriptType = 'powershellv6'
    ScriptText = ($ExecutionContext.InvokeCommand.ExpandString($prefix) + "</code>n$($code -join "<code>n")</code>n" + $ExecutionContext.InvokeCommand.ExpandString($suffix))
    GuestOSType = 'Linux'
    InFile = '.\esxnames.csv'
    OutFile = 'report.csv'
    NoIPinCert = $true
}
$result = Invoke-VMScriptPlus @sInvoke
$result.ScriptOutput

</pre>



<p>The output from our script is now just a message we wrote in the original script.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="513" height="58" src="https://www.lucd.info/wp-content/uploads/2019/12/out4.png" alt="" class="wp-image-6906" srcset="https://www.lucd.info/wp-content/uploads/2019/12/out4.png 513w, https://www.lucd.info/wp-content/uploads/2019/12/out4-300x34.png 300w" sizes="auto, (max-width: 513px) 100vw, 513px" /></figure>



<p>But the result can now be found in the file <strong>report.csv</strong>.</p>



<table style="border-collapse: collapse; width: 100%;">
<tbody>
<tr>
<td style="width: 25%;">
<div>
<div>Name</div>
</div>
</td>
<td style="width: 25%;">NumCpu</td>
<td style="width: 25%;">Version</td>
<td style="width: 25%;">&nbsp;</td>
</tr>
<tr>
<td style="width: 25%;">esx1.local.lab</td>
<td style="width: 25%;">4</td>
<td style="width: 25%;">6.7.0</td>
<td style="width: 25%;">&nbsp;</td>
</tr>
<tr>
<td style="width: 25%;">esx3.local.lab</td>
<td style="width: 25%;">4</td>
<td style="width: 25%;">6.7.0</td>
<td style="width: 25%;">&nbsp;</td>
</tr>
</tbody>
</table>



<h2 class="wp-block-heading">Conclusion</h2>



<p>I hope the previous sections have shown that it doesn't require rocket science to run your scripts on instances deployed with the help of cloud-init.</p>



<p>Will the above methods always work for your scripts?</p>



<p>Probably not, but it shouldn't be too difficult to make them work.</p>



<p>I'm always interested to hear about cases that at first can't be run on 'cattle' stations. If you think you have such a case, let me know through the comments.</p>



<p>Enjoy!</p>


]]></content:encoded>
					
					<wfw:commentRss>https://www.lucd.info/2019/12/09/cloud-init-part-4-running-scripts/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>Cloud-init &#8211; Part 3 &#8211; Photon OS</title>
		<link>https://www.lucd.info/2019/12/08/cloud-init-part-3-photon-os/</link>
					<comments>https://www.lucd.info/2019/12/08/cloud-init-part-3-photon-os/#comments</comments>
		
		<dc:creator><![CDATA[LucD]]></dc:creator>
		<pubDate>Sun, 08 Dec 2019 21:52:03 +0000</pubDate>
				<category><![CDATA[Cloud-init]]></category>
		<category><![CDATA[Deploy]]></category>
		<category><![CDATA[OVA]]></category>
		<category><![CDATA[Photon]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[powershellv6]]></category>
		<category><![CDATA[YAML]]></category>
		<category><![CDATA[cloud-init]]></category>
		<category><![CDATA[seedISO]]></category>
		<guid isPermaLink="false">http://www.lucd.info/?p=6695</guid>

					<description><![CDATA[In Part 1 and Part 2 of this series we used Ubuntu as [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>In <a rel="noreferrer noopener" aria-label="Part 1 (opens in a new tab)" href="https://www.lucd.info/2019/12/06/cloud-init-part-1-the-basics/" target="_blank">Part 1</a> and <a href="https://www.lucd.info/2019/12/07/cloud-init-part-2-advanced-ubuntu/">Part 2</a> of this series we used Ubuntu as the guest OS of our target instances. In Part 3 we will show how to use VMware&#8217;s <a rel="noreferrer noopener" aria-label="Photon OS (opens in a new tab)" href="https://vmware.github.io/photon/" target="_blank">Photon OS</a> as our guest OS.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="693" height="295" src="https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-3.png" alt="" class="wp-image-6784" srcset="https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-3.png 693w, https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-3-300x128.png 300w" sizes="auto, (max-width: 693px) 100vw, 693px" /></figure>



<p>The main reason to use Photon OS is that it is <strong>open-sourced</strong>, it has a <strong>small footprint</strong> and it is <strong>optimised for VMware vSphere</strong>.</p>



<span id="more-6695"></span>



<h2 class="wp-block-heading">Some Background</h2>



<p>Does this mean that <strong>Photon OS</strong> is the ideal guest OS for our &#8216;<em>cattle</em>&#8216; stations where we want to run our PowerShell scripts?</p>



<p>Not really, the major flaw with Photon OS, imho, is that the available packages are <strong>limited</strong> and rather <strong>infrequently maintained</strong>. </p>



<p>In fact, until recently the available <strong>PowerShell package</strong> was an outdated, <strong>not supported </strong>anymore version. But the worst part of the deal, it didn&#8217;t even allow to perform an <a rel="noreferrer noopener" aria-label="Install-Module (opens in a new tab)" href="https://docs.microsoft.com/en-us/powershell/module/powershellget/install-module?view=powershell-6" target="_blank">Install-Module</a>. Which, again imho, is a basic concept in PowerShell. And which didn&#8217;t allow me to install, for example, <a rel="noreferrer noopener" aria-label="VMware PowerCLI (opens in a new tab)" href="https://www.vmware.com/support/developer/PowerCLI/" target="_blank">VMware PowerCLI</a> on a Photon station.</p>



<p>But then recently this happened!</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="521" height="363" src="https://www.lucd.info/wp-content/uploads/2019/12/ps6-photon.png" alt="" class="wp-image-6788" srcset="https://www.lucd.info/wp-content/uploads/2019/12/ps6-photon.png 521w, https://www.lucd.info/wp-content/uploads/2019/12/ps6-photon-300x209.png 300w" sizes="auto, (max-width: 521px) 100vw, 521px" /></figure>



<p>The available PowerShell v6 <a href="https://vmware.github.io/photon/assets/files/html/3.0/photon_admin/tdnf.html" target="_blank" rel="noopener noreferrer">TDNF</a> package was upgraded to a currently supported version, and more importantly, the <strong>Import-Module</strong> cmdlet&nbsp; worked!&nbsp;</p>
<p>Allowing us to install VMware PowerCLI on a Photon instance.</p>



<p>Is this the end of all the PowerShell woes on Photon OS?</p>



<p>Not really! The PowerShell version in this TDNF package will also run out of support in a not so far future and a PowerShell TDNF package upgrade is (still) not automated and somewhat of a black art. </p>



<p>But for now, let&#8217;s be happy with what we have and &#8216;cloud-init&#8217; our Photon &#8216;cattle&#8217; station!</p>



<h2 class="wp-block-heading">Issues and some Solutions</h2>



<h3 class="wp-block-heading">No UserData Property</h3>



<p>While the Ubuntu cloud image, as we saw in <a rel="noreferrer noopener" aria-label="Part 1 (opens in a new tab)" href="https://www.lucd.info/2019/12/06/cloud-init-part-1-the-basics/" target="_blank">Part 1</a> of the series, has the option to pass the <strong>user-data</strong> to cloud-init via an <strong>OVF property</strong>, this feature is unfortunately not available in the Photon OVA.</p>



<p>There are some solutions available.</p>



<ul class="wp-block-list"><li>Roll your own OVA image</li><li>Use the <a rel="noreferrer noopener" aria-label="NoCloud (opens in a new tab)" href="https://cloudinit.readthedocs.io/en/latest/topics/datasources/nocloud.html" target="_blank">NoCloud</a> option, which means creating an ISO containing the user-data and attaching this ISO to the instance before cloud-init runs.</li></ul>



<p>Since I prefer to use the &#8216;standard&#8217; OVA, I opt for creating such a seed ISO.</p>



<h3 class="wp-block-heading">The Root Password</h3>



<p>This is somewhat of a <a rel="noreferrer noopener" aria-label="chicken-and-the-egg (opens in a new tab)" href="https://en.wikipedia.org/wiki/Chicken_or_the_egg" target="_blank">chicken-and-the-egg</a> dilemma.</p>



<p>In the Photon OVA image, the root account is set to force a password change on first use. But unfortunately, this also disallows setting the root password via the user-data. See <a href="https://github.com/vmware/photon/issues/931">Issue#931</a> for more details.</p>



<p>Again, there are two options available.</p>



<ul class="wp-block-list"><li>Roll your own OVA image</li><li>Use a two-stage process, wherein the first stage we use <a rel="noreferrer noopener" aria-label="William Lam (opens in a new tab)" href="https://twitter.com/lamw" target="_blank">William Lam</a>&#8216;s  <a rel="noreferrer noopener" aria-label="Set-VMKeystrokes (opens in a new tab)" href="https://www.virtuallyghetto.com/2017/09/automating-vm-keystrokes-using-the-vsphere-api-powercli.html" target="_blank">Set-VMKeystrokes</a> function. Then in the second stage, we attach the seed ISO and start the instance. And thus kicking off the cloud-init process.</li></ul>



<p>And again, since I prefer to use the &#8216;standard&#8217; OVA, I opt for the Set-VMKeystrokes solution.</p>



<h2 class="wp-block-heading">Install-CloudInitVM Revisited</h2>



<pre class="lang:ps decode:true  ">function Install-CloudInitVM
{
  &lt;#
.SYNOPSIS
  Deploy a VM from an OVA file and use cloud-init for the configuration
  .DESCRIPTION
  This function will deploy an OVA file.
  The function transfer the user-data to the cloud-init process on the VM with
  one of the OVF properties.
.NOTES
  Author:  Luc Dekens
  Version:
  1.0 05/12/19  Initial release
  1.1 07/12/19  Added Photon deployment
                Added seed ISO option
.PARAMETER OvaFile
  Specifies the path to the OVA file
.PARAMETER VmName
  The displayname of the VM
.PARAMETER ClusterName
  The cluster onto which the VM shall be deployed
.PARAMETER DsName
  The datastore on which the VM shall be deployed
.PARAMETER PgName
  The portgroupname to which the VM shall be connected
.PARAMETER CloudConfig
  The path to the YAML file containing the user-data
.PARAMETER CloudConfigISO
  The ISO seed file, containing the meta-data and user-data
.PARAMETER Credential
  The credentials for a user in the VM's guest OS
.PARAMETER Photon
  Switch to indicate a Photon is deployed. This will add an
  additional step to reset the root password before starting
  the actual cloud-init based deployment
.EXAMPLE
  $sCloudInitVM = @{
    OvaFile = '.\bionic-server-cloudimg-amd64.ova'
    VmName = $vmName
    ClusterName = $clusterName
    DsName = $dsName
    PgName = $pgName
    CloudConfig = '.\user-data.yaml'
    Credential = $cred
  }
  Install-CloudInitVM @sCloudInitVM
.EXAMPLE
  $sCloudInitVM = @{
    OvaFile = '.\photon-hw13_uefi-3.0-9355405.ova'
    VmName = $vmName
    ClusterName = $clusterName
    DsName = $dsName
    PgName = $pgName
    CloudConfigISO = '.\seed.iso'
    Credential = $cred
    Photon = $true
  }
  Install-CloudInitVM @sCloudInitVM
#&gt;

  [cmdletbinding()]
  param(
    [string]$OvaFile,
    [string]$VmName,
    [string]$ClusterName,
    [string]$DsName,
    [string]$PgName,
    [Parameter(ParameterSetName = 'YAML')]
    [string]$CloudConfig,
    [Parameter(ParameterSetName = 'SeedISO')]
    [string]$CloudConfigISO,
    [PSCredential[]]$Credential,
    [Switch]$Photon
  )

  $waitJob = (Get-Command -Name .\Wait-Job.ps1 ).ScriptBlock
  if ($PSCmdlet.ParameterSetName -eq 'YAML')
  {
    $userData = Get-Content -Path $CloudConfig -Raw
  }

  Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Starting deployment of $vmName"
  if ($Photon)
  {
    Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - This is a Photon deployment"
  }

  $start = Get-Date

  $vm = Get-VM -Name $vmName -ErrorAction SilentlyContinue -Verbose:$false
  if ($vm)
  {
    Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Cleaning up"
    if ($vm.PowerState -eq 'PoweredOn')
    {
      Stop-VM -VM $vm -Confirm:$false -Verbose:$false | Out-Null
    }
    Remove-VM -VM $vm -DeletePermanently -Confirm:$false -Verbose:$false
  }

  $ovfProp = Get-OvfConfiguration -Ovf $ovaFile -Verbose:$false
  if (-not $Photon)
  {
    $ovfProp.Common.user_data.Value = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($userData))
    $ovfProp.NetworkMapping.VM_Network.Value = $pgName
  }
  else
  {
    $ovfProp.NetworkMapping.None.Value = $pgName
  }
  $sApp = @{
    Source = $ovaFile
    Name = $vmName
    Datastore = Get-Datastore -Name $dsName -Verbose:$false
    DiskStorageFormat = 'Thin'
    VMHost = Get-Cluster -Name $clusterName -Verbose:$false | Get-VMHost  -Verbose:$false | Get-Random
    OvfConfiguration = $ovfProp
    Verbose = $false
  }
  Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Importing OVA"
  $vm = Import-VApp @sApp

  Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Starting the VM"
  Start-VM -VM $vm -Confirm:$false -RunAsync -Verbose:$false | Out-Null

  if ($Photon)
  {
    while (-not $vm.ExtensionData.Guest.GuestOperationsReady)
    {
      Start-Sleep 2
      $vm.ExtensionData.UpdateViewData('Guest')
    }

    Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Resetting password"

    $PswdIsReset = $false
    while (-not $PswdIsReset)
    {
      Set-VMKeystrokes -VMName $VM.Name -StringInput 'root' -ReturnCarriage $true | Out-Null
      Start-Sleep 1
      Set-VMKeystrokes -VMName $VM.Name -StringInput 'changeme' -ReturnCarriage $true | Out-Null
      Start-Sleep 1
      Set-VMKeystrokes -VMName $VM.Name -StringInput 'changeme' -ReturnCarriage $true | Out-Null
      Start-Sleep 1
      Set-VMKeystrokes -VMName $VM.Name -StringInput 'Welcome2019!' -ReturnCarriage $true | Out-Null
      Start-Sleep 1
      Set-VMKeystrokes -VMName $VM.Name -StringInput 'Welcome2019!' -ReturnCarriage $true | Out-Null
      Start-Sleep 1
      Set-VMKeystrokes -VMName $VM.Name -StringInput "exit" -ReturnCarriage $true | Out-Null

      Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Testing new password"

      $sInvoke = @{
        VM = $vm
        ScriptType = 'bash'
        ScriptText = 'whoami'
        GuestUser = $Credential.GetNetworkCredential().UserName
        GuestPassword = $Credential.GetNetworkCredential().Password
        Verbose = $false
      }
      try
      {
        Invoke-VMScript @sInvoke -ErrorAction Stop | Out-Null
        $PswdIsReset = $true
      }
      catch [VMware.VimAutomation.ViCore.Types.V1.ErrorHandling.InvalidGuestLogin]
      {
        Start-Sleep -Seconds 1
      }
      catch
      {
        Write-Error "Exception $($error[0].Exception.GetType().FullName)"
        return
      }
    }

    Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Shutdown VM"

    Shutdown-VMGuest -VM $vm -Confirm:$false -Verbose:$false | Out-Null
    while ($vm.ExtensionData.Runtime.PowerState -ne 'poweredOff')
    {
      Start-Sleep 2
      $vm.ExtensionData.UpdateViewData('Runtime.PowerState')
    }

    Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Attaching seed ISO"

    $isoName = Split-Path -Path $CloudConfigISO -Leaf
    $isoFile = "DS:\$($vm.ExtensionData.Summary.Config.VmPathName.Split(' ')[1].Split('/')[0])\$($isoName)"
    Get-PSDrive -Name DS -ErrorAction SilentlyContinue -Verbose:$false | Remove-PSDrive -Confirm:$false -Verbose:$false
    New-PSDrive -Location (Get-Datastore -VM $vm) -Name DS -PSProvider VimDatastore -Root '\' -Verbose:$false | Out-Null
    Get-Item -Path $CloudConfigISO |
    Copy-DatastoreItem -Destination $isoFile -Force -Confirm:$false -Verbose:$false
    $isoDSPath = $vm.ExtensionData.Summary.Config.VmPathName.Replace("$vmName.vmx", $isoName)
    Get-CDDrive -VM $vm -Verbose:$false | Set-CDDrive -IsoPath $isoDSPath -StartConnected:$true -Confirm:$false -Verbose:$false | Out-Null

    Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Starting the VM"
    Start-VM -VM $vm -Confirm:$false -Verbose:$false | Out-Null
  }

  Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Waiting for cloud-init to finish"

  $User = $Credential.GetNetworkCredential().UserName
  $Password = $Credential.GetNetworkCredential().Password

  $sJob = @{
    Name = 'WaitForCloudInit'
    ScriptBlock = $waitJob
    ArgumentList = $vm.Name, $User, $Password, $global:DefaultVIServer.Name, $global:DefaultVIServer.SessionId
  }
  Start-Job @sJob | Receive-Job -Wait

  if ($Photon)
  {
    Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Restart OS"
    $vmGuest = Stop-VMGuest -VM $vm -Confirm:$false -Verbose:$false
    while ($vmGuest.VM.ExtensionData.Runtime.PowerState -ne 'poweredOff')
    {
      Start-Sleep 2
      $vmGuest.VM.ExtensionData.UpdateViewData('Runtime.PowerState')
    }
    $vm = Start-VM -VM $vmName -Confirm:$false -Verbose:$false
    while ($vm.PowerState -ne 'PoweredOn')
    {
      Start-Sleep 2
      $vm = Get-VM -Name $VmName -Verbose:$false
    }
    while (-not $vm.ExtensionData.Guest.GuestOperationsReady)
    {
      Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Waiting for GuestOperations ready"
      Start-Sleep 2
      $vm.ExtensionData.UpdateViewData('Guest')
    }
    Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Eject CDROM"
    $sInvoke = @{
      VM = $vm
      ScriptType = 'bash'
      ScriptText = 'eject cdrom'
      GuestUser = $Credential.GetNetworkCredential().UserName
      GuestPassword = $Credential.GetNetworkCredential().Password
      Verbose = $false
    }
    Invoke-VMScript @sInvoke | Out-Null
    Get-CDDrive -VM $vm -Verbose:$false | Set-CDDrive -NoMedia -Confirm:$false -Verbose:$false | Out-Null
  }

  Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Deployment complete"
  Write-Verbose "`nDeployment took $([math]::Round((New-TimeSpan -Start $start -End (Get-Date)).TotalSeconds,0)) seconds"
}</pre>
<p>&nbsp;</p>



<h3 class="wp-block-heading">Annotations</h3>



<p>The v1.0 version of this function has already been annotated in <a href="https://www.lucd.info/2019/12/06/cloud-init-part-1-the-basics/" target="_blank" rel="noreferrer noopener" aria-label="Part 1 (opens in a new tab)">Part 1</a> of this series. Those annotations will not be repeated here.</p>



<p><strong>Line 68-71</strong>: To distinguish between the way the user-data is passed, the function now has two mutually exclusive parameters, CloudConfig and CloudConfigIso.</p>



<p><strong>Line 73</strong>: Since Photon requires some special actions, the function now has a switch, obviously named Photon, that specifies if the cloud-init deployment is intended for a Photon instance or not.</p>



<p><strong>Line 77-80</strong>: When the user-data is provided as a YAML file, the function reads the file.</p>



<p><strong>Line 83-86</strong>: A small reminder in the verbose output that the function will handle this as a Photon deployment.</p>



<p><strong>Line 102-110</strong>: The Ubuntu and the Photon OVA files have different OVF properties. This If-Then-Else handles the differences.</p>



<p><strong>Line 126-199</strong>: This part of the function handles most of the peculiarities of a Photon deployment.</p>



<p><strong>Line 136-175</strong>: This part handles the reset of the root password. As mentioned earlier, it uses William&#8217;s <a rel="noreferrer noopener" aria-label="Set-VMKeystrokes (opens in a new tab)" href="https://www.virtuallyghetto.com/2017/09/automating-vm-keystrokes-using-the-vsphere-api-powercli.html" target="_blank">Set-VMKeystrokes</a> function. I experienced intermittent race conditions in sending the keystrokes. Hence the loop with the retries. As a test, to check if the root password was reset, the command &#8216;<em>whoami</em>&#8216; is executed trough <a rel="noreferrer noopener" aria-label="Invoke-VMScript (opens in a new tab)" href="https://vdc-repo.vmware.com/vmwb-repository/dcr-public/6fb85470-f6ca-4341-858d-12ffd94d975e/4bee17f3-579b-474e-b51c-898e38cc0abb/doc/Invoke-VMScript.html" target="_blank">Invoke-VMScript</a> on the instance.</p>



<p><strong>Line 179-184</strong>: Before attaching the seed ISO, the Photon OS is shutdown gracefully. In a While-loop the script test until the instance is actually powered off.</p>



<p><strong>Line 188-195</strong>: The seed ISO is copied to the VM&#8217;s folder, and then attached.</p>



<p><strong>Line 198</strong>: The VM is started with the seed ISO attached. As a result, cloud-init will now use the YAML files it finds on the CD drive as datasource.</p>



<p><strong>Line 216-221</strong>: Since the cloud-init implementation on Photon is <a rel="noreferrer noopener" aria-label="not configured (opens in a new tab)" href="https://github.com/vmware/photon/issues/950" target="_blank">not configured</a> to use the power-state module, the test we used on the Ubuntu instance, can not be used for Photon. As a bypass, the function stops/starts the guest OS on the instance.</p>



<p><strong>Line 235-244</strong>: The function uses the stop/start sequence to disconnect the seed ISO. To avoid having a question on the VM, the function first runs the &#8216;<em>eject cdrom</em>&#8216; command inside the instance.</p>



<h2 class="wp-block-heading">The Seed ISO</h2>



<p>As mentioned earlier, I prefer to use a seed ISO file to solve the issue of not having an OVF property for the user-data.</p>



<p>To create this seed ISO file, we need a Linux station that has the &#8216;<a rel="noreferrer noopener" aria-label="genisoimage (opens in a new tab)" href="https://linux.die.net/man/1/genisoimage" target="_blank">genisoimage</a>&#8216; command available. In the following sample script, I use an  Ubuntu station just for that purpose.</p>



<pre class="lang:ps decode:true  ">$workVM = 'UbuntuPS'
$isoName = 'genisoimage-ps.iso'
$isoVolId = 'cidata'
$isoFiles = @{
    'meta-data' = 'meta-data.yaml'
    'user-data' = 'user-data.yaml'
}

$code1 = @'
sudo apt-get install genisoimage -y &gt; null
genisoimage -output $isoName -volid $isoVolId -joliet -rock $($isoFiles.Keys -join ' ')
'@

$workCred = Get-VICredentialStoreItem -Host $workVM

$isoFiles.GetEnumerator() | ForEach-Object -Process {
    Copy-Item -Path $_.Value -Destination $_.Name
}

$sInvoke = @{
    VM = $workVM
    ScriptType = 'Bash'
    ScriptText = $ExecutionContext.InvokeCommand.ExpandString($code1)
    GuestUser = $workCred.User
    GuestPassword = ConvertTo-SecureString -String $workCred.Password -AsPlainText -Force
    GuestOSType = 'Linux'
    Verbose = $true
    NoIPinCert = $true
    InFile = $isoFiles.Keys
    OutFile = $isoName
}
Invoke-VMScriptPlus @sInvoke

Remove-Item -Path @($isoFiles.Keys) -Confirm:$false
Move-Item -Path $isoName -Destination "..\$isoName" -Force -Confirm:$false</pre>



<h3 class="wp-block-heading">Annotations</h3>



<p><strong>Line 4-7, 16-18</strong>: The script converts (copies) the provided YAML files to standard named YAML files. This allows the script to be used with different versions of the data and only needs a change in the <strong>$isoFiles</strong> hash table.</p>



<p><strong>Line 32</strong>: This script uses the latest version of my <a rel="noreferrer noopener" aria-label="Invoke-VMScriptPlus (opens in a new tab)" href="https://www.lucd.info/2019/11/17/invoke-vmscriptplus-v3/" target="_blank">Invoke-VMScriptPlus</a> function. The reason is twofold: the bash script is multi-line and the script used the InFile/OutFile parameters to copy the YAML files to the Linux box and the ISO file back to the calling station.</p>



<h2 class="wp-block-heading">User-Data</h2>



<p>The user-data that is used for a Photon based instance is, for now, rather simple and straightforward. </p>



<p>Note the installation of the <strong>tdnf</strong> <strong>PowerShell</strong> package (as mentioned earlier in this post) in the <strong>runcmd</strong> section.</p>



<pre class="lang:yaml decode:true ">#cloud-config
fqdn: PhotonPS.local.lab
timezone: Europe/Brussels
hostname: PhotonPS
write_files:
    - path: /etc/systemd/network/10-static.network
      permissions: 0644
      content: |
        [Match]
        Name=eth0

        [Network]
        Address=192.168.10.81/24
        Gateway=192.168.10.1
        DNS=192.168.10.2 192.168.10.3
        DHCP=no
        Domains=local.lab
        NTP=192.168.10.2 192.168.10.3
        LinkLocalAddressing=no
        LLDP=true
    - path: /etc/systemd/network/99-dhcp-en.network
      permissions: 0644
      content: |
        [Match]
        Name=e*

        [Network]
        DHCP=no
    - path: /etc/gnutls/default-priorities
      content: |
        SYSTEM=NONE:!VERS-SSL3.0:!VERS-TLS1.0:+VERS-TLS1.1:+VERS-TLS1.2:+AES-128-CBC:+RSA:+SHA1:+COMP-NULL
runcmd:
- tdnf distro-sync
- tdnf update -y
- tdnf install bindutils -y
- tdnf install powershell -y
- tdnf install gnuTLS -y
- touch /etc/gnutls/default-priorities
package_upgrade: true
package_reboot_if_required: true
</pre>



<h2 class="wp-block-heading">Meta-data</h2>



<p>If you plan on deploying multiple instance, just make sure that the <strong>instance-id</strong> has a unique value.</p>



<pre class="lang:yaml decode:true ">instance-id: iid-local01
local-hostname: photonps
</pre>



<h2 class="wp-block-heading">Sample Run</h2>



<p>For a Photon instance the function is called like this.</p>



<pre class="lang:ps decode:true  ">$vmName = 'PhotonPS'

# Get credential for logging on to the guest OS
$viCred = Get-VICredentialStoreItem -Host $vmName
$secPassword = ConvertTo-SecureString -String $viCred.Password -AsPlainText -Force
$cred = [Management.Automation.PSCredential]::new($viCred.User, $secPassword)

$sCloudInitVM = @{
  OvaFile = 'D:\Repository\VMware\Photon\photon-hw13_uefi-3.0-9355405.ova'
  VmName = $vmName
  ClusterName = 'cluster'
  DsName = 'vsanDatastore'
  PgName = 'vdPg1'
  CloudConfigISO = '..\genisoimage-ps.iso'
  Photon = $true
  Credential = $cred
  Verbose = $true
}
Install-CloudInitVM @sCloudInitVM</pre>



<p>The result of this, since we used the Verbose switch, looks like this.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="687" height="533" src="https://www.lucd.info/wp-content/uploads/2019/12/photon-deploy-1.png" alt="" class="wp-image-6832" srcset="https://www.lucd.info/wp-content/uploads/2019/12/photon-deploy-1.png 687w, https://www.lucd.info/wp-content/uploads/2019/12/photon-deploy-1-300x233.png 300w" sizes="auto, (max-width: 687px) 100vw, 687px" /></figure>



<p>Again, don&#8217;t give too much importance to the total execution time. This was a run in my lab environment, which has limited resources.</p>



<p>On a side-note, notice how the PowerCLI cmdlets used in the function all show this, imho, annoying &#8216;<em><strong>Finished execution</strong></em>&#8216; message.<br>I would love to be able to suppress these verbose messages from the PowerCLI cmdlets. That is why I launched <a rel="noreferrer noopener" aria-label="PowerCLI Idea #225 (opens in a new tab)" href="https://powercli.ideas.aha.io/ideas/PCLI-I-225?utm_source=idea_mailer&amp;utm_medium=email&amp;utm_campaign=submitted_idea" target="_blank">PowerCLI Idea #225</a> &#8220;<em>Get rid of the over-eager verbosity on PowerCLI cmdlets</em>&#8220;.</p>



<p>Enjoy!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.lucd.info/2019/12/08/cloud-init-part-3-photon-os/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title>Cloud-init &#8211; Part 2 &#8211; Advanced Ubuntu</title>
		<link>https://www.lucd.info/2019/12/07/cloud-init-part-2-advanced-ubuntu/</link>
					<comments>https://www.lucd.info/2019/12/07/cloud-init-part-2-advanced-ubuntu/#comments</comments>
		
		<dc:creator><![CDATA[LucD]]></dc:creator>
		<pubDate>Sat, 07 Dec 2019 07:43:24 +0000</pubDate>
				<category><![CDATA[Cloud-init]]></category>
		<category><![CDATA[Deploy]]></category>
		<category><![CDATA[GUI]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[powershellv6]]></category>
		<category><![CDATA[powershellv7]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[Visual Studio Code]]></category>
		<category><![CDATA[YAML]]></category>
		<category><![CDATA[cloud-init]]></category>
		<category><![CDATA[Code]]></category>
		<category><![CDATA[Git]]></category>
		<category><![CDATA[repository]]></category>
		<category><![CDATA[v6]]></category>
		<category><![CDATA[v7]]></category>
		<guid isPermaLink="false">http://www.lucd.info/?p=6693</guid>

					<description><![CDATA[In Cloud-init – Part 1 – The Basics, we laid the groundwork for [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>In <a href="https://www.lucd.info/2019/12/06/cloud-init-part-1-the-basics/" target="_blank" rel="noopener noreferrer">Cloud-init – Part 1 – The Basics</a>, we laid the groundwork for using <strong>cloud-init</strong> in a <strong>vSphere</strong> environment. In this post we will go into more <strong>advanced Ubuntu</strong> setups. This includes deploying PowerShell, v6 and v7, using repositories and if needed, a GUI with Visual Studio Code.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="679" height="296" src="https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-2-2.png" alt="" class="wp-image-6708" srcset="https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-2-2.png 679w, https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-2-2-300x131.png 300w" sizes="auto, (max-width: 679px) 100vw, 679px" /></figure>



<span id="more-6693"></span>



<p>For the installation of software on the instance we use the <strong>user-data</strong> YAML file. In the following sections, we discuss some of the more obvious candidates you want to have on your station. Remember that the basic idea of the series is to have a &#8220;<em>cattle</em>&#8221; station to test/run (and optionally develop) your scripts. </p>



<h2 class="wp-block-heading">Scripting Packages</h2>



<h3 class="wp-block-heading">PowerShell</h3>



<p>Let&#8217;s start by installing <strong>PowerShell</strong> on the instance. This comes down to adding a couple of instructions to the YAML file we used in Part 1.</p>



<pre class="lang:yaml decode:true ">#cloud-config
hostname: ubuntubionicps
fqdn: ubuntubionicps.local.lab
write_files:
- path: /etc/netplan/50-cloud-init.yaml
  content: |
    network:
     version: 2
     ethernets:
      ens192:
       addresses: [192.168.10.82/24]
       gateway4: 192.168.10.1
       dhcp6: false
       nameservers:
         addresses:
           - 192.168.10.2
           - 192.168.10.3
         search:
           - local.lab
       dhcp4: false
       optional: true
- path: /etc/sysctl.d/60-disable-ipv6.conf
  owner: root
  content: |
    net.ipv6.conf.all.disable_ipv6=1
    net.ipv6.conf.default.disable_ipv6=1
runcmd:
- netplan --debug apply
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
- sysctl -w net.ipv6.conf.default.disable_ipv6=1
- apt-get -y update
- add-apt-repository universe
# PowerShell
- wget https://raw.githubusercontent.com/PowerShell/PowerShell/master/tools/install-powershell.sh
- chmod 755 install-powershell.sh
# PowerShell v6
- ./install-powershell.sh
# PowerShell v7
- ./install-powershell.sh -preview
- apt-get -y clean
- apt-get -y autoremove --purge
timezone: Europe/Brussels
system_info:
  default_user:
    name: default-user
    lock_passwd: false
    sudo: ["ALL=(ALL) NOPASSWD:ALL"]
disable_root: false
ssh_pwauth: yes
users:
  - default
  - name: luc
    gecos: LucD
    lock_passwd: false
    groups: sudo, users, admin
    shell: /bin/bash
    sudo: ['ALL=(ALL) NOPASSWD:ALL']
chpasswd:
  list: |
    default-user:$6$aPp//e2ueP$ETEXcAhAyQuJ4qNCbqxmmSYGZbg2wFwpP/YITvoXdgxwZBnf32drePKi2OIn5fLqtH5pHO03yRdPXK3ToLG6b0
    luc:$6$kW1WwJ2K$M6415du1BZd.qt92SvR6X.RuyDhEZmgR4hz4NcKH9XHn2850Vc6zHpubXM6uUeqMUaJQ740ogROB74gfBEhn9.
    root:$6$Js9CVr06$br9qf0VxuBsdY7Vtg/0pk9jLlycYBDLVsvbKwLDleCK7dSDheOxWaFOWdjkiqSPRrWG./N8V5RgCVwugZGnTc1
  expire: false
package_upgrade: true
package_reboot_if_required: true
power_state:
  delay: now
  mode: reboot
  message: Rebooting the OS
  condition: if [ -e /var/run/reboot-required ]; then exit 0; else exit 1; fi</pre>



<h4 class="wp-block-heading">Annotations</h4>



<p><strong>Line 33-39</strong>: The instructions to install PowerShell are added to the <strong>runcmd</strong> section.</p>



<p><strong>Line 33</strong>: You can use comments in your YAML file. I highly recommend doing that. It will help others, and probably yourself later on.</p>



<p><strong>Line 34-35</strong>: The method uses the <a rel="noreferrer noopener" aria-label="install.sh (opens in a new tab)" href="https://github.com/PowerShell/PowerShell/blob/master/tools/install-powershell.sh" target="_blank">install-powershell.sh</a> bash script to install the latest PowerShell v6 and the latest PowerShell v7 Preview. I personally prefer this method vs the apt-get method, since it is globally usable on multiple Linux distributions. The <strong>install-powershell.sh</strong> script determines on which distro it is running, and launches the appropriate command(s).</p>



<h4 class="wp-block-heading">Result</h4>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="550" height="288" src="https://www.lucd.info/wp-content/uploads/2019/12/ubuntubionicps.png" alt="" class="wp-image-6744" srcset="https://www.lucd.info/wp-content/uploads/2019/12/ubuntubionicps.png 550w, https://www.lucd.info/wp-content/uploads/2019/12/ubuntubionicps-300x157.png 300w" sizes="auto, (max-width: 550px) 100vw, 550px" /></figure>



<h3 class="wp-block-heading">Git &amp; repo cloning</h3>



<p>For your scripts, you, of course, use a repository. That means you will need to configure <a rel="noreferrer noopener" aria-label="git (opens in a new tab)" href="https://www.linux.com/tutorials/introduction-using-git/" target="_blank">git</a>, and a way to clone repositories to your instance.</p>



<p>The following extract of the YAML file only shows the lines in the <strong>runcmd</strong> section.</p>



<pre class="lang:yaml decode:true  ">runcmd:
- netplan --debug apply
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
- sysctl -w net.ipv6.conf.default.disable_ipv6=1
- apt-get -y update
- add-apt-repository universe
# PowerShell
- wget https://raw.githubusercontent.com/PowerShell/PowerShell/master/tools/install-powershell.sh
- chmod 755 install-powershell.sh
# PowerShell v6
- ./install-powershell.sh
# PowerShell v7
- ./install-powershell.sh -preview
# Git
- git config --global user.name "LucD"
- git config --global user.email "lucd@lucd.info"
- mkdir /home/luc/vCheck-vSphere
- chown luc:luc /home/luc/vCheck-vSphere
- git clone https://github.com/alanrenouf/vCheck-vSphere /home/luc/vCheck-vSphere
- apt-get -y clean
- apt-get -y autoremove --purge</pre>



<h4 class="wp-block-heading">Annotations</h4>



<p><strong>Line 14-16</strong>: Since git is by default installed in the Ubuntu 18.04 LTS Cloud Image OVA file, we only need to configure some settings. In this we update some configuration settings, primarily to be able to correct sign off commits.</p>



<p><strong>Line 17-19</strong>: As part of the deployment process through cloud-init, you can clone specific repositories to the instance. In these lines I used <a rel="noreferrer noopener" aria-label="Alan Renouf (opens in a new tab)" href="https://twitter.com/alanrenouf" target="_blank">Alan Renouf</a>&#8216;s <a href="https://github.com/alanrenouf/vCheck-vSphere" target="_blank" rel="noreferrer noopener" aria-label="vCheck-vSphere (opens in a new tab)">vCheck-vSphere</a> repository as an example.</p>



<h4 class="wp-block-heading">Result</h4>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="517" height="293" src="https://www.lucd.info/wp-content/uploads/2019/12/ubuntubionicvcheck.png" alt="" class="wp-image-6747" srcset="https://www.lucd.info/wp-content/uploads/2019/12/ubuntubionicvcheck.png 517w, https://www.lucd.info/wp-content/uploads/2019/12/ubuntubionicvcheck-300x170.png 300w" sizes="auto, (max-width: 517px) 100vw, 517px" /></figure>



<h2 class="wp-block-heading">Going GUI</h2>



<p>There might be occasions when you need to provide a temporary station with a GUI. That can also be done with cloud-init&#8217;s user-data YAML file.</p>



<h3 class="wp-block-heading">The Desktop</h3>



<p>The choice of which desktop and which desktop manager is completely up to you. Just be aware that some of these might require more resources to be assigned to the instance.</p>



<p>The following sample is intended for an Ubuntu 18.04 LTS instance. Again this extract only shows the <strong>runcmd</strong> section of the YAML file.</p>



<pre class="lang:yaml decode:true   "># Install GUI
- apt-get install -y tasksel
- tasksel install lubuntu-core
# ==&gt; know issue
- apt-get --purge remove -y light-locker
- service lightdm start
# Install RDP
- apt-get install -y apt-transport-https
- apt-get install -y xrdp
- systemctl enable xrdp</pre>



<h4 class="wp-block-heading">Annotations</h4>



<p><strong>Line 1-10</strong>: In this example, I use <a rel="noreferrer noopener" aria-label="lubuntu (opens in a new tab)" href="https://lubuntu.net/" target="_blank">lubuntu</a> as the desktop. Primarily because it is fast and resource-friendly.</p>



<p><strong>Line </strong>2: We use <a rel="noreferrer noopener" aria-label="tasksel (opens in a new tab)" href="https://manpages.ubuntu.com/manpages/xenial/man8/tasksel.8.html" target="_blank">tasksel</a> to install the desktop, so we have to make sure that it is available.</p>



<p><strong>Line 5</strong>: There is a known issue with the <a rel="noreferrer noopener" aria-label="light-locker (opens in a new tab)" href="https://launchpad.net/ubuntu/+source/light-locker" target="_blank">light-locker</a> package. Until a final fix is available, the easiest solution is to just uninstall it. If you absolutely need a screen-saver, install one of the alternatives. Like for example <a rel="noreferrer noopener" aria-label="xscreensaver (opens in a new tab)" href="https://packages.ubuntu.com/search?keywords=xscreensaver" target="_blank">xscreensaver</a>.</p>



<p><strong>Line 6</strong>: Next we start the display manager <a rel="noreferrer noopener" aria-label="lightdm (opens in a new tab)" href="https://wiki.ubuntu.com/LightDM" target="_blank">lightdm</a>.</p>



<p><strong>Line 7-</strong>10: We need a way to connect to the station we are deploying. In this example, I opted for RDP as my protocol.</p>



<p><strong>Line 8</strong>: The <a rel="noreferrer noopener" aria-label="apt-transport-https (opens in a new tab)" href="https://manpages.ubuntu.com/manpages/bionic/man1/apt-transport-https.1.html" target="_blank">apt-transport-https</a> package is a pre-requisite.</p>



<p><strong>Line 10</strong>: Configure the <a rel="noreferrer noopener" aria-label="xrdp (opens in a new tab)" href="https://help.ubuntu.com/community/xrdp" target="_blank">xrdp</a> service to automatically start at the boot.</p>



<h3 class="wp-block-heading">Visual Studio Code</h3>



<p>Now that we have a GUI, we can use several GUI based applications. For working with PowerShell, the <a rel="noreferrer noopener" aria-label="Visual Studio Code (opens in a new tab)" href="https://code.visualstudio.com/" target="_blank">Visual Studio Code</a> editor with the <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.PowerShell" target="_blank" rel="noreferrer noopener" aria-label="PowerShell extension (opens in a new tab)">PowerShell extension</a> is an obvious choice.</p>



<pre class="lang:yaml decode:true "># Install Visual Studio Code
- add-apt-repository "deb [arch=amd64] https://packages.microsoft.com/repos/vscode stable main"
- apt-get update
- apt-get install -y code
- mkdir /home/luc/.vscode
- chown luc:luc /home/luc/.vscode
- code --install-extension ms-vscode.powershell --user-data-dir /home/luc/.vscode/ --extensions-dir /home/luc/.vscode/extensions/
- code --install-extension eamodio.gitlens --user-data-dir /home/luc/.vscode --extensions-dir /home/luc/.vscode/extensions/</pre>



<h4 class="wp-block-heading">Annotations</h4>



<p><strong>Line 2-4</strong>: The standard <a href="https://code.visualstudio.com/docs/setup/linux" target="_blank" rel="noopener noreferrer">installation procedure</a> for Code on a Linux platform.</p>
<p><strong>Line 5-8</strong>: This installs the extensions for <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.PowerShell" target="_blank" rel="noopener noreferrer">PowerShell</a> and <a href="https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens" target="_blank" rel="noopener noreferrer">GitLens</a>. Remember that the cloud-init stages run under the root account, if you need to install packages for a user, make sure to change the owner.</p>



<h4 class="wp-block-heading">Result</h4>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="561" src="https://www.lucd.info/wp-content/uploads/2019/12/ubuntubionicpsdevGUI-1024x561.png" alt="" class="wp-image-6773" srcset="https://www.lucd.info/wp-content/uploads/2019/12/ubuntubionicpsdevGUI-1024x561.png 1024w, https://www.lucd.info/wp-content/uploads/2019/12/ubuntubionicpsdevGUI-300x164.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/ubuntubionicpsdevGUI-768x421.png 768w, https://www.lucd.info/wp-content/uploads/2019/12/ubuntubionicpsdevGUI-1536x842.png 1536w, https://www.lucd.info/wp-content/uploads/2019/12/ubuntubionicpsdevGUI-720x395.png 720w, https://www.lucd.info/wp-content/uploads/2019/12/ubuntubionicpsdevGUI.png 1633w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading">Conclusion</h2>



<p>We have now set the stage to actually start using these instances. In one of the next posts in this cloud-init series, we will show how to run scripts on these stations.</p>



<p>If you have suggestions or questions about specific additions on these instances, feel free to use the comments.</p>



<p>Enjoy!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.lucd.info/2019/12/07/cloud-init-part-2-advanced-ubuntu/feed/</wfw:commentRss>
			<slash:comments>5</slash:comments>
		
		
			</item>
		<item>
		<title>Cloud-init &#8211; Part 1 &#8211; The Basics</title>
		<link>https://www.lucd.info/2019/12/06/cloud-init-part-1-the-basics/</link>
					<comments>https://www.lucd.info/2019/12/06/cloud-init-part-1-the-basics/#comments</comments>
		
		<dc:creator><![CDATA[LucD]]></dc:creator>
		<pubDate>Fri, 06 Dec 2019 12:16:08 +0000</pubDate>
				<category><![CDATA[Cloud-init]]></category>
		<category><![CDATA[Deploy]]></category>
		<category><![CDATA[OVA]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[YAML]]></category>
		<category><![CDATA[cloud-init]]></category>
		<category><![CDATA[PowerCLI]]></category>
		<category><![CDATA[ubuntu]]></category>
		<category><![CDATA[VM]]></category>
		<guid isPermaLink="false">http://www.lucd.info/?p=6586</guid>

					<description><![CDATA[In this first part, we will introduce cloud-init and how you can use it from your PowerShell/PowerCLI scripts. Since the Ubuntu distribution is very popular...]]></description>
										<content:encoded><![CDATA[
<p>One of the important <a href="https://en.wikipedia.org/wiki/DevOps">DevOps</a> adagios in my book is “<em>Treat your servers as cattle, not as pets</em>”. Meaning that you roll out your stations when you need them, use them and throw them away after you used them. This series of posts will document one such way of deploying such &#8216;cattle&#8217; stations. The method is named <a href="https://cloud-init.io/" target="_blank" rel="noreferrer noopener" aria-label="cloud-init (opens in a new tab)">cloud-init</a>.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="678" height="295" src="https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-1.png" alt="Post logo" class="wp-image-6589" srcset="https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-1.png 678w, https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-part-1-300x131.png 300w" sizes="auto, (max-width: 678px) 100vw, 678px" /></figure>



<p>In this first part, we will introduce <strong>cloud-init</strong> and how you can use it from your <strong>PowerShell/PowerCLI</strong> scripts. Since the <a rel="noreferrer noopener" aria-label="Ubuntu (opens in a new tab)" href="https://en.wikipedia.org/wiki/Ubuntu" target="_blank">Ubuntu</a> distribution is very popular, on-premises and in the cloud, this introduction will focus on that distro to demonstrate the concept. In the following parts, we will tackle <a rel="noreferrer noopener" aria-label="Photon (opens in a new tab)" href="https://vmware.github.io/photon/" target="_blank">Photon</a>, containers and how to run your scripts on these stations.</p>



<span id="more-6586"></span>



<p>Before diving into the technical stuff, first a reminder why we should treat our <strong>servers as cattle</strong>.</p>



<p>A list with some of the major arguments (at least for me).</p>



<ul class="wp-block-list"><li>No maintenance required when using the latest distributions. Meaning no long-running updates and security patches before usage.</li><li>No “<em>less than 5% used</em>” machines anymore in your environment.</li><li>No risk of system tattooing from any previous usage.</li><li>An easy way to test new versions of your scripts, the OS, PowerShell and PowerCLI.</li></ul>



<h2 class="wp-block-heading">Ingredients</h2>



<p>To use cloud-init, the requirements, in the cases we will handle here, are minimal.</p>



<ul class="wp-block-list"><li>An <strong>OVA image</strong> that is configured to use cloud-init<ul><li>Several Linux distributions nowadays have such a &#8216;cloud&#8217; image: <a rel="noreferrer noopener" aria-label="Ubuntu (opens in a new tab)" href="https://cloud-images.ubuntu.com/" target="_blank">Ubuntu</a>, <a rel="noreferrer noopener" aria-label="Photon (opens in a new tab)" href="https://github.com/vmware/photon/wiki/Downloading-Photon-OS" target="_blank">Photon</a>&#8230;</li></ul></li><li>A <a rel="noreferrer noopener" aria-label="datasource (opens in a new tab)" href="https://cloudinit.readthedocs.io/en/latest/topics/datasources.html" target="_blank">datasource</a>, which is the configuration data provided by the user to the cloud-init process, and which defines how the resulting station will be configured.</li><li>A <strong>cloud</strong>, in it&#8217;s broadest sense, to run the VM. From the cloud-init documentation &#8220;&#8230; <em>all major public cloud providers, provisioning systems for private cloud infrastructure, and bare-metal installations</em>.&#8221;</li><li>A <strong>script</strong> to trigger and control it all. </li></ul>



<p><a rel="noreferrer noopener" aria-label="Canonical (opens in a new tab)" href="https://twitter.com/canonical" target="_blank">Canonical</a>, the company behind Ubuntu, maintains a document, named <a href="https://pages.ubuntu.com/rs/066-EOV-335/images/CloudInit_Whitepaper.pdf?utm_source=marketo&amp;utm_medium=landingpage&amp;utm_campaign=CY19_DC_Server_Whitepaper_CloudInit" target="_blank" rel="noreferrer noopener" aria-label="Cloud Instance Initialisation with cloud-init (opens in a new tab)">Cloud Instance Initialisation with cloud-init</a>, that has a schematic that captures the cloud-init process perfectly.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="923" height="487" src="https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-schema.png" alt="Cloud-init schematic" class="wp-image-6600" srcset="https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-schema.png 923w, https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-schema-300x158.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-schema-768x405.png 768w, https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-schema-720x380.png 720w" sizes="auto, (max-width: 923px) 100vw, 923px" /></figure>



<p>If we annotate and update this schema for the usage we have in mind, it becomes like this.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="860" height="487" src="https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-schema-vsphere.png" alt="Cloud-init for a vSphere environment schematic" class="wp-image-6601" srcset="https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-schema-vsphere.png 860w, https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-schema-vsphere-300x170.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-schema-vsphere-768x435.png 768w, https://www.lucd.info/wp-content/uploads/2019/12/cloud-init-schema-vsphere-720x408.png 720w" sizes="auto, (max-width: 860px) 100vw, 860px" /></figure>



<h2 class="wp-block-heading">The OVA File</h2>



<p>Since our target cloud platform in this series is a vSphere environment, we will use OVA files as the source for the VM(s) we are going to deploy.</p>



<p>The <a rel="noreferrer noopener" aria-label="Ubuntu OVA image (opens in a new tab)" href="https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.ova" target="_blank">Ubuntu OVA image</a> that we will be using in this post is for <strong>Ubuntu Server 18.04 LTS</strong>, aka <strong>Bionic Beaver</strong>.</p>



<p>This OVA image allows using the <a rel="noreferrer noopener" aria-label="OVFProperties (opens in a new tab)" href="https://blogs.vmware.com/PowerCLI/2014/09/powercli-5-8-new-feature-get-ovfconfiguration-part-1-2.html" target="_blank">OVFProperties</a> as the datasource. Note that the user-data we pass in this way needs to be <a rel="noreferrer noopener" aria-label="Base64 (opens in a new tab)" href="https://en.wikipedia.org/wiki/Base64" target="_blank">Base64</a> encoded.</p>



<h2 class="wp-block-heading">The User-data</h2>



<p>As mentioned in the previous section, in this Ubuntu image we will use the <strong>OVFProperties</strong> as the datasource for our user-data.</p>



<p>The content of the <strong>user-data</strong> is provided to the script as a <a rel="noreferrer noopener" aria-label="YAML (opens in a new tab)" href="https://en.wikipedia.org/wiki/YAML" target="_blank">YAML</a> file.  The syntax for such a file and the available is defined in the <a href="https://readthedocs.org/projects/cloudinit/downloads/pdf/latest/" target="_blank" rel="noreferrer noopener" aria-label="cloud-init Documentation (opens in a new tab)">cloud-init Documentation</a>.</p>



<p>The following is the sample YAML file we will use in the rest of this post.</p>



<pre class="theme:vs2012-black lang:yaml decode:true ">#cloud-config
hostname: ubuntubionic
fqdn: ubuntubionic.local.lab
write_files:
- path: /etc/netplan/50-cloud-init.yaml
  content: |
    network:
     version: 2
     ethernets:
      ens192:
       addresses: [192.168.10.79/24]
       gateway4: 192.168.10.1
       dhcp6: false
       nameservers:
         addresses:
           - 192.168.10.2
           - 192.168.10.3
         search:
           - local.lab
       dhcp4: false
       optional: true
- path: /etc/sysctl.d/60-disable-ipv6.conf
  owner: root
  content: |
    net.ipv6.conf.all.disable_ipv6=1
    net.ipv6.conf.default.disable_ipv6=1
runcmd:
- netplan --debug apply
- sysctl -w net.ipv6.conf.all.disable_ipv6=1
- sysctl -w net.ipv6.conf.default.disable_ipv6=1
- apt-get -y update
- add-apt-repository universe
- apt-get -y clean
- apt-get -y autoremove --purge
timezone: Europe/Brussels
system_info:
  default_user:
    name: default-user
    lock_passwd: false
    sudo: ["ALL=(ALL) NOPASSWD:ALL"]
disable_root: false
ssh_pwauth: yes
users:
  - default
  - name: luc
    gecos: LucD
    lock_passwd: false
    groups: sudo, users, admin
    shell: /bin/bash
    sudo: ['ALL=(ALL) NOPASSWD:ALL']
chpasswd:
  list: |
    default-user:$6$aPp//e2ueP$ETEXcAhAyQuJ4qNCbqxmmSYGZbg2wFwpP/YITvoXdgxwZBnf32drePKi2OIn5fLqtH5pHO03yRdPXK3ToLG6b0
    luc:$6$kW1WwJ2K$M6415du1BZd.qt92SvR6X.RuyDhEZmgR4hz4NcKH9XHn2850Vc6zHpubXM6uUeqMUaJQ740ogROB74gfBEhn9.
    root:$6$Js9CVr06$br9qf0VxuBsdY7Vtg/0pk9jLlycYBDLVsvbKwLDleCK7dSDheOxWaFOWdjkiqSPRrWG./N8V5RgCVwugZGnTc1
  expire: false
package_upgrade: true
package_reboot_if_required: true
power_state:
  delay: now
  mode: reboot
  message: Rebooting the OS
  condition: if [ -e /var/run/reboot-required ]; then exit 0; else exit 1; fi
</pre>



<h3 class="wp-block-heading">Annotations</h3>



<p><strong>Line 1</strong>: the user-data file always has to start with the line &#8216;# cloud-config&#8217;</p>



<p><strong>Line 2</strong>-3: These lines follow the regular <a rel="noreferrer noopener" aria-label="YAML key-value syntax (opens in a new tab)" href="https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html" target="_blank">YAML syntax</a> of key-value entries.  The lines defines the <strong>hostname</strong> and the <strong>FQDN</strong> that the instance will get.</p>



<p><strong>Line 4-26</strong>: The <strong>write-files</strong> section instructs cloud-init to <strong>create files</strong>, with the specified properties and content on the instance. In this example file the files are used to define a static IPv4 address and to disable IPv6. Which files need to be used is dependent on the OS that runs in the VM. Note that this example will use <a href="https://netplan.io/" target="_blank" rel="noreferrer noopener" aria-label="netplan (opens in a new tab)">netplan</a> for the network configuration.</p>



<p><strong>Line 27-34</strong>: The <strong>runcmd</strong> section defines commands that will run on the instance during the <strong>first boot</strong> of the guest OS. In this example the commands are used to activate the network (it uses netplan), set some variables and set and update the application repositories.</p>



<p><strong>Line 35</strong>: This line defines which timezone needs to be configured.</p>



<p><strong>Line 36-40</strong>: An Ubuntu image comes configured with a <strong>default user. In the system_info section,</strong> the settings for this default user are specified.</p>



<p><strong>Line 43-50</strong>: The <strong>users</strong> section allows to define additional users on the instance. Note that the <strong>default</strong> user needs to come first in this list.</p>



<p><strong>Line 51-56</strong>: The <strong>chpasswd</strong> allows you to change and/or set the passwords for the users. The required format and how these entries are created will be discussed in one of the following sections.</p>



<p><strong>Line 57-58</strong>: These lines request an upgrade of the <strong>packages</strong> present on the instance. And perform a reboot when one of the package upgrades would require that.</p>



<p><strong>Line 59-63</strong>: The <strong>power-state</strong> section instructs cloud-init in which state the instance shall be left after cloud-init completes. In this example, the system is rebooted on condition that a specific file exists. This file is created by cloud-init when it has decided that a reboot of the instance is required.</p>



<h3 class="wp-block-heading">Password encoding</h3>



<p>In <strong>user-data,</strong> there are several places where a password can be provided. The value needs to be the hash of the password, not the password itself.  With the help of an existing Linux box (<strong>UbuntuWork</strong> in this case), I use the following script to create such SHA-512 encoded password hashes.  The script uses the <a rel="noreferrer noopener" aria-label="mkpasswd (opens in a new tab)" href="https://manpages.ubuntu.com/manpages/trusty/man1/mkpasswd.1.html" target="_blank">mkpasswd</a> command to generate the hash. </p>



<p>Automation rules!</p>



<pre class="lang:ps decode:true  ">function Get-PasswordHash
{
    param(
        [String]$VMName,
        [String]$GuestUser,
        [String]$GuestPassword,
        [String]$Password
    )

    $sInvoke = @{
        VM = $VMName
        ScriptType = 'bash'
        ScriptText = "mkpasswd -m SHA-512 $Password"
        GuestUser = $GuestUser
        GuestPassword = $GuestPassword
    }
    (Invoke-VMScript @sInvoke).ScriptOutput.Trim("<code>n")
}

$sHash = @{
    VM = 'UbuntuWork'
    GuestUser = 'root'
    GuestPassword = 'VMware1!'
    Password = 'VMware1!'
}
Get-PasswordHash @sHash
</pre>



<p>This returns something like this.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="944" height="81" src="https://www.lucd.info/wp-content/uploads/2019/12/hash.png" alt="Password hash" class="wp-image-6613" srcset="https://www.lucd.info/wp-content/uploads/2019/12/hash.png 944w, https://www.lucd.info/wp-content/uploads/2019/12/hash-300x26.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/hash-768x66.png 768w, https://www.lucd.info/wp-content/uploads/2019/12/hash-720x62.png 720w" sizes="auto, (max-width: 944px) 100vw, 944px" /></figure>



<p>A word of warning, such hashed password are not super-safe. A decent password cracker with sufficient compute power and a good wordlist can most probably crack this in less than one minute.</p>



<h2 class="wp-block-heading">The Script</h2>



<pre class="lang:ps decode:true   ">function Install-CloudInitVM
{
  &lt;#
.SYNOPSIS
  Deploy a VM from an OVA file and use cloud-init for the configuration
  .DESCRIPTION
  This function will deploy an OVA file.
  The function transfer the user-data to the cloud-init process on the VM with
  one of the OVF properties.
.NOTES
  Author:  Luc Dekens
  Version:
  1.0 05/12/19  Initial release
.PARAMETER OvaFile
  Specifies the path to the OVA file
.PARAMETER VmName
  The displayname of the VM
.PARAMETER ClusterName
  The cluster onto which the VM shall be deployed
.PARAMETER DsName
  The datastore on which the VM shall be deployed
.PARAMETER PgName
  The portgroupname to which the VM shall be connected
.PARAMETER CloudConfig
  The path to the YAML file containing the user-data
.PARAMETER Credential
  The credentials for a user in the VM's guest OS
.EXAMPLE
  $sCloudInitVM = @{
    OvaFile = '.\bionic-server-cloudimg-amd64.ova'
    VmName = $vmName
    ClusterName = $clusterName
    DsName = $dsName
    PgName = $pgName
    CloudConfig = '.\user-data.yaml'
    Credential = $cred
  }
  Install-CloudInitVM @sCloudInitVM
  #&gt;

  [cmdletbinding()]
  param(
    [string]$OvaFile,
    [string]$VmName,
    [string]$ClusterName,
    [string]$DsName,
    [string]$PgName,
    [string]$CloudConfig,
    [PSCredential[]]$Credential
  )

  $waitJob = (Get-Command -Name .\Wait-Job.ps1).ScriptBlock
  $userData = Get-Content -Path $CloudConfig -Raw

  Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Starting deployment of $vmName"

  $start = Get-Date

  $vm = Get-VM -Name $vmName -ErrorAction SilentlyContinue
  if ($vm)
  {
    Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Cleaning up"
    if ($vm.PowerState -eq 'PoweredOn')
    {
      Stop-VM -VM $vm -Confirm:$false | Out-Null
    }
    Remove-VM -VM $vm -DeletePermanently -Confirm:$false
  }

  $ovfProp = Get-OvfConfiguration -Ovf $ovaFile
  $ovfProp.Common.user_data.Value = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($userData))
  $ovfProp.NetworkMapping.VM_Network.Value = $pgName

  $sApp = @{
    Source = $ovaFile
    Name = $vmName
    Datastore = Get-Datastore -Name $dsName
    DiskStorageFormat = 'Thin'
    VMHost = Get-Cluster -Name $clusterName | Get-VMHost | Get-Random
    OvfConfiguration = $ovfProp
  }
  Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Importing OVA"
  $vm = Import-VApp @sApp

  Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Starting the VM"
  Start-VM -VM $vm -Confirm:$false -RunAsync | Out-Null

  Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Waiting for cloud-init to finish"

  $User = $Credential.GetNetworkCredential().UserName
  $Password = $Credential.GetNetworkCredential().Password

  $sJob = @{
    Name = 'WaitForCloudInit'
    ScriptBlock = $waitJob
    ArgumentList = $vm.Name, $User, $Password, $global:DefaultVIServer.Name, $global:DefaultVIServer.SessionId
  }
  Start-Job @sJob | Receive-Job -Wait

  Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') - Deployment complete"

  Write-Verbose "</code>nDeployment took $([math]::Round((New-TimeSpan -Start $start -End (Get-Date)).TotalSeconds,0)) seconds"
}
</pre>



<h3 class="wp-block-heading">Annotations</h3>



<p><strong>Line 52</strong>: To find out when cloud-init has finished it's run, the function uses a background job. This background job is stored in a separate .ps1 file. More on the content and purpose of that 'wait' script later.</p>



<p><strong>Line 53</strong>: The user-data is read from the YAML file. It is important to use the <strong>Raw</strong> switch, otherwise, we would lose the line separators.</p>



<p><strong>Line 60-68</strong>: If a VM with the same Displayname is already present, it will be stopped (when powered on) and removed.</p>



<p><strong>Line 70-71</strong>: The user-data is assigned to the <strong>Common.user_data</strong> property of the OVF properties. The user-data needs to be converted to <strong>base64</strong>.</p>



<p><strong>Line 72</strong>: The Ubuntu OVA allows to pass the Portgroup to which the VM shall be connected.</p>



<p><strong>Line 74-83</strong>: The VM is installed from the OVA with the <a rel="noreferrer noopener" aria-label="Import-VApp (opens in a new tab)" href="https://vdc-repo.vmware.com/vmwb-repository/dcr-public/6fb85470-f6ca-4341-858d-12ffd94d975e/4bee17f3-579b-474e-b51c-898e38cc0abb/doc/Import-VApp.html" target="_blank">Import-VApp</a> cmdlet.</p>



<p><strong>Line 86</strong>: The VM is powered on. During the boot process, the first phase of cloud-init will run.</p>



<p><strong>Line 90-98</strong>: The function uses a background job to check if the cloud-init process has completed. More on that later.</p>



<h3 class="wp-block-heading">The Wait job</h3>



<p>I decided to store the WaitJob script in a separate .ps1 file. Primarily because I can easily reuse it this way and it makes the code of scripts that use shorter.</p>



<p>The Wait Job is started as a background job. This allows the calling code to simply use the <a href="https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/wait-job?view=powershell-6" target="_blank" rel="noreferrer noopener" aria-label="Wait-Job (opens in a new tab)">Wait-Job</a> cmdlet.</p>



<pre class="lang:ps decode:true   ">[CmdletBinding()]
param(
    [string]$vmName,
    [string]$user,
    [string]$pswd,
    [string]$vcsaName,
    [string]$SessionId
)

$sleepTime = 5
Connect-VIServer -Server $vcsaName -Session $SessionId | Out-Null
$notFinished = $true
while ($notFinished)
{
    Try
    {
        $vm = Get-VM -Name $vmName -ErrorAction Stop
        while ($vm.PowerState -ne 'PoweredOn' -and -not $vm.ExtensionData.Guest.GuestOperationsReady)
        {
            Start-Sleep -Seconds $sleepTime
        }
        $fileExist = $false
        while (-not $fileExist)
        {
            $sInvoke = @{
                VM = $vm
                ScriptType = 'bash'
                ScriptText = '[ -f /var/lib/cloud/instance/boot-finished ] &amp;&amp; echo "File exist"'
                GuestUser = $user
                GuestPassword = $pswd
                ErrorAction = 'Stop'
            }
            try
            {
                $result = Invoke-VMScript @sInvoke
                $fileExist = [Boolean]$result.ScriptOutput
            }
            catch
            {
                Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') Exception:<code>tId: $($error[0].Exception.ErrorId)  Category: $($error[0].Exception.ErrorCategory)"
                Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff')</code>t<code>tLine: $($error[0].InvocationInfo.Line)"
            }
        }
        $sInvoke = @{
            VM = $vm
            ScriptType = 'bash'
            ScriptText = 'cat /var/lib/cloud/instance/boot-finished'
            GuestUser = $user
            GuestPassword = $pswd
        }
        $result = Invoke-VMScript @sInvoke
        $result.ScriptOutput
        $notFinished = $false
    }
    catch
    {
        Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff') Exception:</code>tId: $($error[0].Exception.ErrorId)  Category: $($error[0].Exception.ErrorCategory)"
        Write-Verbose "$(Get-Date -Format 'HH:mm:ss.fff')<code>t</code>tLine: $($error[0].InvocationInfo.Line)"
    }
    Start-Sleep -Seconds $sleepTime
}</pre>
<p>&nbsp;</p>



<h4 class="wp-block-heading">Annotations</h4>



<p><strong>Line 7 + 11</strong>: In a background job we do not inherit any open connections to a vSphere Server. A script can use the SessionId of an open connection in the calling script, to open a connection, without having to provide credentials.</p>



<p><strong>Line 18</strong>: Before using the call to <a rel="noreferrer noopener" aria-label="Invoke-VMScript (opens in a new tab)" href="https://vdc-repo.vmware.com/vmwb-repository/dcr-public/6fb85470-f6ca-4341-858d-12ffd94d975e/4bee17f3-579b-474e-b51c-898e38cc0abb/doc/Invoke-VMScript.html" target="_blank">Invoke-VMScript</a>, the Wait Job makes sure the VM and the VMware Tools in there, are ready to receive such a call.</p>



<p><strong>Line 28</strong>: Inside the guest OS, the function uses the simple bash expression to test for the presence of a file. The file <strong>/var/lib/cloud/instance/boot-finished</strong> is created by cloud-init when the process completes. </p>



<p><strong>Line 44-52</strong>: The Wait Job function returns the content of the file to the caller.</p>



<h2 class="wp-block-heading">Sample Run</h2>



<p>With the above code, we have everything in place to deploy a <strong>VM</strong> from an <strong>Ubuntu OVA</strong>, with the guest OS configuration is done through cloud-init, based on a <strong>YAML</strong> file.</p>



<p>A typical call could look something like this.</p>



<pre class="lang:ps decode:true  ">$vmName = 'UbuntuBionic'

# Get credential for logging on to the guest OS

$viCred = Get-VICredentialStoreItem -Host $vmName
$secPassword = ConvertTo-SecureString -String $viCred.Password -AsPlainText -Force
$cred = [Management.Automation.PSCredential]::new($viCred.User, $secPassword)

$sCloudInitVM = @{
  OvaFile = '.\bionic-server-cloudimg-amd64.ova'
  VmName = $vmName
  ClusterName = 'cluster'
  DsName = 'vsanDatastore'
  PgName = 'vdPg1'
  CloudConfig = '.\user-data-bionic.yaml'
  Credential = $cred
  Verbose = $true
}
Install-CloudInitVM @sCloudInitVM</pre>


<p>With the <strong>Verbose</strong> switch set to $true, the output of this call would look like this.</p>


<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="763" height="390" src="https://www.lucd.info/wp-content/uploads/2019/12/bionicVM.png" alt="Deployment - Verbose output" class="wp-image-6662" srcset="https://www.lucd.info/wp-content/uploads/2019/12/bionicVM.png 763w, https://www.lucd.info/wp-content/uploads/2019/12/bionicVM-300x153.png 300w, https://www.lucd.info/wp-content/uploads/2019/12/bionicVM-720x368.png 720w" sizes="auto, (max-width: 763px) 100vw, 763px" /></figure>



<p>I highlighted with <mark><span style="background-color:#f78da7" class="tadv-background-color">red</span></mark> boxes the verbose messages from the function. The text in the <span style="background-color:#7bdcb5" class="tadv-background-color">green</span> box is the content of the  <strong>/var/lib/cloud/instance/boot-finished file</strong>.</p>



<p>Do not look too much at the timings. This run happened in my lab environment, which has limited resources. The main reason for showing this is to demonstrate how easy such a cloud-init based deployment can be incorporated in your pipelines.</p>



<p>This concludes Part 1 in the cloud-init series. It provides some functions and scripts to deploy a VM, starting from an OVA file, with the configuration of the guest OS done through cloud-init.</p>



<p>In the upcoming parts in this series, I will show some more advanced Ubuntu deployments, show how cloud-init can be used with Photon and show how you can use these deployed VMs as your '<em>cattle</em>'.</p>



<p>Enjoy!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.lucd.info/2019/12/06/cloud-init-part-1-the-basics/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title>Invoke-VMScriptPlus v3</title>
		<link>https://www.lucd.info/2019/11/17/invoke-vmscriptplus-v3/</link>
					<comments>https://www.lucd.info/2019/11/17/invoke-vmscriptplus-v3/#comments</comments>
		
		<dc:creator><![CDATA[LucD]]></dc:creator>
		<pubDate>Sun, 17 Nov 2019 16:10:55 +0000</pubDate>
				<category><![CDATA[Invoke-VMScriptPlus]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[powershellv6]]></category>
		<category><![CDATA[powershellv7]]></category>
		<category><![CDATA[sudo]]></category>
		<category><![CDATA[infile]]></category>
		<category><![CDATA[outfile]]></category>
		<guid isPermaLink="false">http://www.lucd.info/?p=6437</guid>

					<description><![CDATA[My InvokeVMScriptPlus function serves me well while interacting with the guest OS on [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>My <strong>InvokeVMScriptPlus</strong> function serves me well while interacting with the guest OS on a VM. And I&#8217;m apparently not the only one that uses the function. This post introduces <strong>Invoke-VMScriptPlus v3</strong>.</p>



<p>The original <a rel="noreferrer noopener" aria-label="Invoke-VMScriptPlus (opens in a new tab)" href="https://www.lucd.info/2017/09/14/invoke-vmscriptplus/" target="_blank">Invoke-VMScriptPlus</a> post, and the addition of PS Core support, described in the <a rel="noreferrer noopener" aria-label="Invoke-VMScriptPlus v2 (opens in a new tab)" href="https://www.lucd.info/2018/08/05/invoke-vmscriptplus-v2/" target="_blank">Invoke-VMScriptPlus v2</a> post, keep being some of my most read posts. Time for another update.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="575" height="516" src="https://www.lucd.info/wp-content/uploads/2019/11/invoke-plus-v3-1.png" alt="" class="wp-image-6551" srcset="https://www.lucd.info/wp-content/uploads/2019/11/invoke-plus-v3-1.png 575w, https://www.lucd.info/wp-content/uploads/2019/11/invoke-plus-v3-1-300x269.png 300w" sizes="auto, (max-width: 575px) 100vw, 575px" /></figure>



<p>In this <strong>v3</strong> version, I introduce some new features to the function.</p>



<ul class="wp-block-list"><li><strong>PSv6</strong> and <strong>PSv7</strong>  support</li><li>Use <strong>files</strong> (input and output) from within your scripts</li><li>improved <strong>sudo</strong> support</li></ul>



<p><span style="background-color: #fae100;"><strong>Update July 2nd 2021</strong></span></p>
<ul>
<li>Fixed incorrect variable <strong>NameHost</strong></li>
<li>Added tests to detect <strong>IP</strong> or <strong>FQDN</strong> in URI</li>
<li>Updated test to check if type <strong>TrustAllCertsPolicy</strong> exists or not</li>
</ul>



<p><span style="background-color: #fae100;"><strong>Update April 15th 2020</strong></span></p>
<ul>
<li>Added <strong>SkipCertificateCheck</strong> switch</li>
</ul>



<p><span style="background-color: #fae100;"><strong>Update January 16th 2020</strong></span></p>
<ul>
<li>Bug fix which occured when connected to an ESXi node</li>
</ul>



<p></p>



<p><span style="background-color: #fae100;"><strong>Update November 18th 2019</strong></span></p>
<ul>
<li>Added <strong>NoIPinCert</strong> switch</li>
</ul>



<span id="more-6437"></span>



<h2 class="wp-block-heading">The Code</h2>



<pre class="lang:ps decode:true ">class MyOBN:System.Management.Automation.ArgumentTransformationAttribute {
    [ValidateSet(
      'Cluster', 'Datacenter', 'Datastore', 'DatastoreCluster', 'Folder',
      'VirtualMachine', 'VirtualSwitch', 'VMHost', 'VIServer'
    )]
    [String]$Type
    MyOBN([string]$Type) {
      $this.Type = $Type
    }
    [object] Transform([System.Management.Automation.EngineIntrinsics]$engineIntrinsics, [object]$inputData) {
      if ($inputData -is [string]) {
        if (-NOT [string]::IsNullOrWhiteSpace( $inputData )) {
          $cmdParam = "-$(if($this.Type -eq 'VIServer'){'Server'}else{'Name'}) $($inputData)"
          $sCmd = @{
            Command = "Get-$($this.Type.Replace('VirtualMachine','VM')) $($cmdParam)"
          }
          return (Invoke-Expression @sCmd)
        }
      } elseif ($inputData.GetType().Name -match "$($this.Type)Impl") {
        return $inputData
      } elseif ($inputData.GetType().Name -eq 'Object[]') {
        return ($inputData | ForEach-Object {
            if ($_ -is [String]) {
              return (Invoke-Expression -Command "Get-$($this.Type.Replace('VirtualMachine','VM')) -Name <code>$_")
            } elseif ($_.GetType().Name -match "$($this.Type)Impl") {
              $_
            }
          })
      }
      throw [System.IO.FileNotFoundException]::New()
    }
  }
  function Invoke-VMScriptPlus {
    &lt;#
  .SYNOPSIS
  Runs a script in a Linux guest OS.
  The script can use the SheBang to indicate which interpreter to use.
  .DESCRIPTION
  This function will launch a script in a Linux guest OS.
  The script supports the SheBang line for a limited set of interpreters.
  .NOTES
  Author:  Luc Dekens
  Version:
  1.0 14/09/17  Initial release
  1.1 14/10/17  Support bash here-document
  2.0 01/08/18  Support Windows guest OS, bat &amp; powershell
  2.1 03/08/18  PowerShell she-bang for Linux
  2.2 17/08/18  Added ScriptEnvironment
  2.3 11/03/19  Resolve IP to FQDN to support certificate for ESXi node
  2.4 22/04/19  Switch to provide password inline to 'sudo' lines
  2.5 07/06/19  Switch WaitForToolsVersionChange to wait for a version change
  3.0 17/11/19  Added powershellv7 support, added InFile &amp; OutFile
  3.1 18/11/19  Added switch NoIPinCert
  3.2 15/04/20  Added switch SkipCertificateCheck
  3.3 02/07/21  Fixed issue with NameHost, added test for IP or FQDN, changed  type test TrustAllCertsPolicy
  .PARAMETER VM
  Specifies the virtual machines on whose guest operating systems
  you want to run the script.
  .PARAMETER GuestUser
  Specifies the user name you want to use for authenticating with the
  virtual machine guest OS.
  .PARAMETER GuestPassword
  Specifies the password you want to use for authenticating with the
  virtual machine guest OS.
  .PARAMETER GuestCredential
  Specifies a PSCredential object containing the credentials you want
  to use for authenticating with the virtual machine guest OS.
  .PARAMETER ScriptText
  Provides the text of the script you want to run. You can also pass
  to this parameter a string variable containing the path to the script.
  Note that the function will add a SheBang line, based on the ScriptType,
  if none is provided in the script text.
  .PARAMETER ScriptType
  The supported Linux interpreters.
  Currently these are bash,perl,python3,nodejs,php,lua,powershell,powershellv6,powershellv7
  .PARAMETER ScriptEnvironment
  A string array with environment variables.
  These environment variables are available to the script from ScriptText
  .PARAMETER GuestOSType
  Indicates which type of guest OS the VM is using.
  The parameter accepts Windows or Linux. This parameter is a fallback for
  when the function cannot determine which OS Family the Guest OS
  belongs to
  .PARAMETER CRLF
  Switch to indicate of the NL that is returned by Linux, shall be
  converted to a CRLF
  .PARAMETER Sudo
  Switch to convert all 'sudo' lines to an inline password 'sudo' line.
  Only taken into account when the GuestOSType is 'Linux'
  .PARAMETER KeepFiles
  Switch to indicate that the temporary files, the script and the output files,
  shall not be deleted.
  Only to be used for debugging purposes.
  .PARAMETER InFile
  One or more files that will be copied to the guest OS.
  These files will be copied to the directory from where the script will run
  and can be used from within the script.
  .PARAMETER InFile
  One or more files that will be copied from the guest OS after the script has ran.
  These files will be copied from the directory from where the script runs.
  .PARAMETER Server
  Specifies the vCenter Server systems on which you want to run the
  cmdlet. If no value is passed to this parameter, the command runs
  on the default servers. For more information about default servers,
  see the description of Connect-VIServer.
  .PARAMETER WaitForToolsVersionChange
  When the invoked code changes the version of the VMware Tools, this switch
  tells the function to wait till this version change is visible in the script
  .PARAMETER NoIPinCert
  When certificates are used that do not contain the IP address of the ESXi node
  as a Subject Alternative Name (SAN), this switch tells the function to convert
  the IP address in all URI used for file transfers, to a FQDN.
  .PARAMETER NoIPinCert
  When certificates are used that do not contain the IP address of the ESXi node
  as a Subject Alternative Name (SAN), this switch tells the function to convert
  the IP address in all URI used for file transfers, to a FQDN.
  .PARAMETER SkipCertificateCheck
  When a non-trusted certificate is used on the ESXi node that hosts the targetted
  VM, the transfer of files to and from the VM's Guest OS will fail.
  This switch tells the function to ignore invalid certificates on the ESXi node.
  .EXAMPLE
  $pScript = @'
  #!/usr/bin/env perl
  use strict;
  use warnings;
  print "Hello world\n";
  '@
  $sCode = @{
  VM = $VM
  GuestCredential = $cred
  ScriptType = 'perl'
  ScriptText = $pScript
  }
  Invoke-VMScriptPlus @sCode
  .EXAMPLE
  $pScript = @'
  print("Happy 10th Birthday PowerCLI!")
  '@
  $sCode = @{
  VM = $VM
  GuestCredential = $cred
  ScriptType = 'python3'
  ScriptText = $pScript
  }
  Invoke-VMScriptPlus @sCode
  .EXAMPLE
  $pScript = @'
  Get-Content -Path .\MyInput.txt | Set-Content -Path .\MyOutput.txt
  '@
  $sCode = @{
  VM = $VM
  GuestCredential = $cred
  ScriptType = 'powershellv7'
  ScriptText = $pScript
  InFile = 'C:\Test\MyInput.txt'
  OutFile = 'C:\Report\MyOutput.txt'
  }
  Invoke-VMScriptPlus @sCode
  #&gt;
    [cmdletbinding()]
    param(
      [parameter(Mandatory = $true, ValueFromPipeline = $true)]
      [MyOBN('VirtualMachine')]
      [VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine[]]$VM,
      [Parameter(Mandatory = $true, ParameterSetName = 'TextScript')]
      [Parameter(Mandatory = $true, ParameterSetName = 'TextExe')]
      [String]$GuestUser,
      [Parameter(Mandatory = $true, ParameterSetName = 'TextScript')]
      [Parameter(Mandatory = $true, ParameterSetName = 'TextExe')]
      [SecureString]$GuestPassword,
      [Parameter(Mandatory = $true, ParameterSetName = 'CredScript')]
      [Parameter(Mandatory = $true, ParameterSetName = 'CredExe')]
      [PSCredential[]]$GuestCredential,
      [Parameter(Mandatory = $true, ParameterSetName = 'TextScript')]
      [Parameter(Mandatory = $true, ParameterSetName = 'CredScript')]
      [String]$ScriptText,
      [Parameter(Mandatory = $true, ParameterSetName = 'TextScript')]
      [Parameter(Mandatory = $true, ParameterSetName = 'CredScript')]
      [ValidateSet('bash', 'perl', 'python3', 'nodejs', 'php', 'lua', 'powershell',
        'powershellv6', 'powershellv7', 'bat', 'exe')]
      [String]$ScriptType,
      [Parameter(Mandatory = $true, ParameterSetName = 'TextExe')]
      [Parameter(Mandatory = $true, ParameterSetName = 'CredExe')]
      [string]$ExeName,
      [String[]]$ScriptEnvironment,
      [ValidateSet('Windows', 'Linux')]
      [String]$GuestOSType,
      [Switch]$CRLF,
      [Switch]$Sudo,
      [Switch]$KeepFiles,
      [MyOBN('VIServer')]
      [VMware.VimAutomation.ViCore.Types.V1.VIServer]$Server = $global:DefaultVIServer,
      [Switch]$WaitForToolsVersionChange,
      [String[]]$InFile,
      [String[]]$OutFile,
      [Switch]$NoIPinCert,
      [Switch]$SkipCertificateCheck
    )
    Begin {
      #region Helper functions
      function Send-GuestFile {
        [cmdletbinding()]
        param(
          [Parameter(Mandatory = $true)]
          [String]$File,
          [Parameter(Mandatory = $true, ParameterSetName = 'File')]
          [String]$Source,
          [Parameter(Mandatory = $true, ParameterSetName = 'Data')]
          [String]$Data
        )
        if ($PSCmdlet.ParameterSetName -eq 'File') {
          $Data = Get-Content -Path $Source -Raw
        }
        $attr = New-Object VMware.Vim.GuestFileAttributes
        $clobber = $true
        $fileInfo = $gFileMgr.InitiateFileTransferToGuest($moref, $auth, $File, $attr, $Data.Length, $clobber)
        if ($Server.ProductLine -eq 'embeddedEsx') {
          $fileInfo = $fileInfo.Replace('*', ([System.Uri]$server.ServiceUri).Host)
        }
        if ($NoIPinCert.IsPresent) {
          $ip = $fileInfo.split('/')[2].Split(':')[0]
          if ($ip -as [IPAddress]) {
            $hostName = Resolve-DnsName -Name $ip | Select-Object -ExpandProperty NameHost
            $fileInfo = $fileInfo.replace($ip, $hostName)
          }
        }
        $sWeb = @{
          Uri = $fileInfo
          Method = 'Put'
          Body = $Data
        }
        if ($SkipCertificateCheck.IsPresent) {
          if ($PSVersionTable.PSVersion.Major -lt 6) {
            [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
          } else {
            $sWeb.Add('SkipCertificateCheck', $true)
          }
        }
        Write-Verbose -Message "Copying $($PSCmdlet.ParameterSetName) to $File"
        $copyResult = Invoke-WebRequest @sWeb
        if ($copyResult.StatusCode -ne 200) {
          Throw "ScripText copy failed!</code>rStatus $($copyResult.StatusCode)<code>r$(($copyResult.Content | ForEach-Object{[char]$_}) -join '')"
        }
      }
      function Receive-GuestFile {
        [cmdletbinding()]
        param(
          [String]$Source,
          [String]$File
        )
        $fileInfo = $gFileMgr.InitiateFileTransferFromGuest($moref, $auth, $Source)
        if ($Server.ProductLine -eq 'embeddedEsx') {
          $fileInfo.Url = $fileInfo.Url.Replace('*', ([System.Uri]$server.ServiceUri).Host)
        }
        if ($NoIPinCert.IsPresent) {
          $ip = $fileInfo.Url.split('/')[2].Split(':')[0]
          if ($ip -as [IPAddress]) {
            hostName = Resolve-DnsName -Name $ip | Select-Object -ExpandProperty NameHost
            $fileInfo.Url = $fileInfo.Url.replace($ip, $hostName)
          }
        }
        $sWeb = @{
          Uri = $fileInfo.Url
          Method = 'Get'
        }
        if ($SkipCertificateCheck.IsPresent) {
          if ($PSVersionTable.PSVersion.Major -lt 6) {
            [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
          } else {
            $sWeb.Add('SkipCertificateCheck', $true)
          }
        }
        $fileContent = Invoke-WebRequest @sWeb
        if ($fileContent.StatusCode -ne 200) {
          Throw "Retrieve of script output failed!</code>rStatus $($fileContent.Status)<code>r$(($fileContent.Content | ForEach-Object{[char]$_}) -join '')"
        }
        if ($File) {
          $fileContent.Content | Set-Content -Path $File -Encoding byte -Confirm:$false
        } else {
          $fileContent.Content
        }
      }
      #endregion
      #region Set up guest operations
      $si = Get-View ServiceInstance -Server $Server
      $guestMgr = Get-View -Id $si.Content.GuestOperationsManager
      $gFileMgr = Get-View -Id $guestMgr.FileManager
      $gProcMgr = Get-View -Id $guestMgr.ProcessManager
      #endregion
      #region Set up shebang table
      $shebangTab = @{
        'bash' = '#!/usr/bin/env bash'
        'perl' = '#!/usr/bin/env perl'
        'python3' = '#!/usr/bin/env python3'
        'nodejs' = '#!/usr/bin/env nodejs'
        'php' = '#!/usr/bin/env php'
        'lua' = '#!/usr/bin/env lua'
        'powershellv6' = '#!/usr/bin/env pwsh'
        'powershellv7' = '#!/usr/bin/env pwsh-preview'
      }
      #endregion
      #region Handle SkipCertificateCheck (if used)
      if ($SkipCertificateCheck.IsPresent -and
        $PSVersionTable.PSVersion.Major -lt 6 -and
        -not ('TrustAllCertsPolicy' -as [Type])) {
        Add-Type @'
    using System.Net;
    using System.Security.Cryptography.X509Certificates;
    public class TrustAllCertsPolicy : ICertificatePolicy {
    public bool CheckValidationResult(
        ServicePoint srvPoint, X509Certificate certificate,
        WebRequest request, int certificateProblem) {
            return true;
        }
    }
'@
      }
      #endregion
    }
    Process {
      foreach ($vmInstance in $VM) {
        #region Test conditions for running script in guest OS
        if ($vmInstance.PowerState -ne 'PoweredOn') {
          Write-Error "VM $($vmInstance.Name) is not powered on"
          continue
        }
        $vmInstance.ExtensionData.UpdateViewData('Guest')
        if ($vmInstance.ExtensionData.Guest.ToolsRunningStatus -ne 'guestToolsRunning') {
          Write-Error "VMware Tools are not running on VM $($vmInstance.Name)"
          continue
        }
        if (-not $vmInstance.ExtensionData.Guest.GuestOperationsReady) {
          Write-Error "VM $($vmInstance.Name) is not ready to use Guest Operations"
          continue
        }
        $moref = $vmInstance.ExtensionData.MoRef
        #endregion
        #region Create Authentication Object (User + Password)
        if ('CredScript', 'CredExe' -contains $PSCmdlet.ParameterSetName) {
          $GuestUser = $GuestCredential.GetNetworkCredential().username
          $plainGuestPassword = $GuestCredential.GetNetworkCredential().password
        }
        if ('TextScript', 'TextExe' -contains $PSCmdlet.ParameterSetName) {
          $bStr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($GuestPassword)
          $plainGuestPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bStr)
        }
        $auth = New-Object VMware.Vim.NamePasswordAuthentication
        $auth.InteractiveSession = $false
        $auth.Username = $GuestUser
        $auth.Password = $plainGuestPassword
        #endregion
        #region Determine GuestOSType
        if (-not $GuestOSType) {
          Write-Verbose "No GuestOSType value provided. Trying to determine now."
          switch -Regex ($vmInstance.Guest.OSFullName) {
            'Windows' {
              Write-Verbose "It's a Windows guest OS"
              $GuestOSType = 'Windows'
              if (-not $ExeName -and 'bat', 'powershell', 'powershellv6', 'powershellv7' -notcontains $ScriptType) {
                Write-Verbose "Invalid scripttype provided"
                Write-Error "For a Windows guest OS the ScriptType can be Bat, PowerShell, PowerShellv6 or PowerShellv7"
                continue
              }
            }
            'Linux' {
              Write-Verbose "It's a Linux guest OS"
              $GuestOSType = 'Linux'
              if (-not $ExeName -and 'bat', 'powershell' -contains $ScriptType) {
                Write-Verbose "Invalid scripttype provided"
                Write-Error "For a Linux guest OS the ScriptType cannot be Bat"
                continue
              }
            }
            Default {
              Write-Verbose "Can't determine guest OS type."
              Write-Error "Unable to determine the guest OS type on VM $($vmInstance.Name)"
              Write-Error "Try using the GuestOSType parameter."
              continue
            }
          }
        }
        if ($GuestOSType -eq 'Linux') {
          Write-Verbose "Seems to be a Linux guest OS"
          # Test if code contains a SheBang, otherwise add it
          $targetCode = $shebangTab[$ScriptType]
          if ($ScriptText -notmatch "^$($targetCode)") {
            Write-Verbose "Add SheBang $targetCode"
            $ScriptText = "$($targetCode)</code>n<code>r$($ScriptText)"
          }
          # Take care of the 'sudo' switch
          if ($Sudo) {
            Write-Verbose "Setting up sudo usage"
            $ScriptText = ($ScriptText | ForEach-Object -Process {
                $_ -replace 'sudo', "echo $plainGuestPassword | sudo -S"
              })
          }
        }
        #endregion
        #region Create a temp directory
        $tempFolder = $gFileMgr.CreateTemporaryDirectoryInGuest($moref, $auth, "$($env:USERNAME)_$($PID)", $null, $null)
        Write-Verbose "Created temp folder in guest OS $tempFolder"
        #endregion
        #region Create temp file for script
        $suffix = ''
        if ('bat', 'exe' -contains $ScriptType) {
          $suffix = ".cmd"
        }
        if ('powershell', 'powershellv6', 'powershellv7' -contains $ScriptType) {
          $suffix = ".ps1"
        }
        Try {
          $tempFile = $gFileMgr.CreateTemporaryFileInGuest($moref, $auth, "$($env:USERNAME)_$($PID)", $suffix, $tempFolder)
          Write-Verbose "Created temp script file in guest OS $tempFile"
        } Catch {
          Write-Verbose "Encountered a problem creating the script file in the guest OS"
          Throw "$error[0].Exception.Message"
        }
        #endregion
        #region Create temp file for output
        Try {
          $tempOutput = $gFileMgr.CreateTemporaryFileInGuest($moref, $auth, "$($env:USERNAME)_$($PID)_output", $null, $tempFolder)
          Write-Verbose "Created temp output file in guest OS $tempOutput"
        } Catch {
          Write-Verbose "Encountered a problem creating the output file in the guest OS"
          Throw "$error[0].Exception.Message"
        }
        #endregion
        #region Copy script to temp file
        if ($ExeName) {
          Send-GuestFile -Data $ExeName -File $tempFile
          Write-Verbose "Copied ExeName to temp script file"
        } else {
          if ($GuestOSType -eq 'Linux') {
            $ScriptText = $ScriptText.Split("</code>r") -join ''
          }
          Send-GuestFile -Data $ScriptText -File $tempFile
          Write-Verbose "Copied scripttext to temp script file"
        }
        #endregion
        #region Get current environment variables
        $SystemEnvironment = $gProcMgr.ReadEnvironmentVariableInGuest($moref, $auth, $null)
        #endregion
        #region Copy InFiles to to guest OS
        if ($InFile) {
          $InFile | ForEach-Object -Process {
            $destinationFilePath = "$tempFolder/$(Split-Path -Path $_ -Leaf)"
            Write-Verbose "Upload InFile $_"
            Send-GuestFile -Source $_ -File $destinationFilePath
          }
        }
        #endregion
        #region Run script
        if ($WaitForToolsVersionChange) {
          $toolsVersion = $vmInstance.ExtensionData.Guest.ToolsVersion
        }
        switch ($GuestOSType) {
          'Linux' {
            # Make temp file executable
            $spec = New-Object VMware.Vim.GuestProgramSpec
            $spec.Arguments = "751 $tempFile"
            $spec.ProgramPath = '/bin/chmod'
            Try {
              $procId = $gProcMgr.StartProgramInGuest($moref, $auth, $spec)
              Write-Verbose "Make script file executable"
            } Catch {
              Write-Verbose "Encountered a problem making the script file executable in the guest OS"
              Throw "$error[0].Exception.Message"
            }
            # Run temp file
            $spec = New-Object VMware.Vim.GuestProgramSpec
            if ($ScriptEnvironment) {
              $spec.EnvVariables = $SystemEnvironment + $ScriptEnvironment
            }
            $spec.Arguments = " &gt; $($tempOutput) 2&gt;&amp;1"
            $spec.ProgramPath = "$($tempFile)"
            $spec.WorkingDirectory = $tempFolder
            Try {
              $procId = $gProcMgr.StartProgramInGuest($moref, $auth, $spec)
              Write-Verbose "Run script with '$($tempFile) &gt; $($tempOutput)'"
            } Catch {
              Write-Verbose "Encountered a problem running the script file in the guest OS"
              Throw "$error[0].Exception.Message"
            }
          }
          'Windows' {
            # Run temp file
            $spec = New-Object VMware.Vim.GuestProgramSpec
            $spec.WorkingDirectory = $tempFolder
            if ($ScriptEnvironment) {
              $spec.EnvVariables = $SystemEnvironment + $ScriptEnvironment
            }
            if ($ExeName) {
              $spec.ProgramPath = "cmd.exe"
              $spec.Arguments = " /s /c start """" ""$ExeName"""
            } else {
              switch ($ScriptType) {
                'PowerShell' {
                  $spec.Arguments = " /C powershell -NonInteractive -File $($tempFile) &gt; $($tempOutput)"
                  $spec.ProgramPath = "cmd.exe"
                }
                { 'PowerShellv6', 'PowerShellv7' -contains $_ } {
                  $psCmd = 'pwsh.exe'
                  if ($ScriptType -eq 'PowerShellv7') {
                    $psCmd = 'pwsh-preview.exe'
                  }
                  $spec.Arguments = " /C ""$psCmd"" -NonInteractive -File $($tempFile) &gt; $($tempOutput)"
                  $spec.ProgramPath = "cmd.exe"
                }
                'Bat' {
                  $spec.Arguments = " /s /c cmd &gt; $($tempOutput) 2&gt;&amp;1 /s /c $($tempFile)"
                  $spec.ProgramPath = "cmd.exe"
                }
              }
            }
            Try {
              $procId = $gProcMgr.StartProgramInGuest($moref, $auth, $spec)
              Write-Verbose "Run script with '$($spec.ProgramPath) $($spec.Arguments)'"
            } Catch {
              Write-Verbose "Encountered a problem running the script file in the guest OS"
              Throw "$error[0].Exception.Message"
            }
          }
        }
        if ($WaitForToolsVersionChange) {
          Write-Verbose "Waiting for VMware Tools version to change"
          while ($toolsVersion -eq $vmInstance.ExtensionData.Guest.ToolsVersion) {
            Start-Sleep -Seconds 1
            $vmInstance.ExtensionData.UpdateViewData('Guest')
          }
          Write-Verbose "VMware Tools version changed from $toolsVersion to $($vmInstance.ExtensionData.Guest.ToolsVersion)"
        }
        #endregion
        #region Wait for script to finish
        Try {
          $pInfo = $gProcMgr.ListProcessesInGuest($moref, $auth, @($procId))
          Write-Verbose "Wait for process to end"
          while ($pInfo -and $null -eq $pInfo.EndTime) {
            Start-Sleep 1
            $pInfo = $gProcMgr.ListProcessesInGuest($moref, $auth, @($procId))
          }
        } Catch {
          Write-Verbose "Encountered a problem waiting for the script to end in the guest OS"
          Throw "$error[0].Exception.Message"
        }
        #endregion
        #region Retrieve output from script
        Write-Verbose "Get output from $tempOutput"
        $scriptOutput = Receive-GuestFile -Source $tempOutput
        #endregion
        #region Copy OutFiles from guest OS
        if ($OutFile) {
          $OutFile | ForEach-Object -Process {
            $sourceFilePath = "$tempFolder/$_"
            Write-Verbose "Download OutFile $_"
            Receive-GuestFile -Source $sourceFilePath -File $_
          }
        }
        #endregion
        #region Clean up
        # Remove temporary folder
        if (-not $KeepFiles) {
          $gFileMgr.DeleteDirectoryInGuest($moref, $auth, $tempFolder, $true)
          Write-Verbose "Removed folder $tempFolder"
        }
        #endregion
        #region Package result in object
        New-Object PSObject -Property @{
          VM = $vmInstance
          ScriptOutput = &amp; {
            $out = ($scriptOutput | ForEach-Object { [char]$_ }) -join ''
            if ($CRLF) {
              $out.Replace("<code>n", "</code>n`r")
            } else {
              $out
            }
          }
          Pid = $procId
          PidOwner = $pInfo.Owner
          Start = $pInfo.StartTime
          Finish = $pInfo.EndTime
          ExitCode = $pInfo.ExitCode
          ScriptType = $ScriptType
          ScriptSize = $ScriptText.Length
          ScriptText = $ScriptText
          OutFiles = $OutFile
          GuestOS = $GuestOSType
        }
        #endregion
      }
    }
  }
</pre>



<h2 class="wp-block-heading">Annotations</h2>



<p> The following annotations will only document the additions to the v2 version of the function. Refer to the blog posts mentioned in the introduction to see the annotations of the previous versions of the function. </p>



<p><strong data-rich-text-format-boundary="true">Line 191-192</strong>: Addition/change of the script types. Now includes <strong>powershellv6</strong> and <strong>powershellv7</strong>.</p>
<p><strong>Line 201</strong>: A switch that allows the caller to ask for <strong>sudo</strong> support. In practice the function will pipe, via an echo command, the guest credential&#8217;s password to each sudo command in the script.</p>
<p><strong>Line 206-207</strong>: New parameters <strong>InFile</strong> and <strong>OutFile</strong>. They permit files to be copied to/from the script environment.</p>
<p><strong>Line 208</strong>: The NoIPforCert switch</p>
<p><strong>Line 209</strong>: The SkipCertificateCheck switch</p>
<p><strong>Line 214-262</strong>: Internal helper function to send files from the caller&#8217;s environment to the guest OS.</p>
<p><strong>Line 248-255,286-293</strong>: The <strong>SkipCertificateCheck</strong> switch calls Invoke-WebRequest with the option to not check the certificate. In pre-V6 that is done through the CertificatePolicy class, in PS v6 and higher, the SkipCertificateCheck switch on Invoke-WebRequest is used.</p>
<p><strong>Line 264-307</strong>: Internal helper function to receive files from the guest OS into the caller&#8217;s environment.</p>
<p><strong>Line 237-242,276-281</strong>: If the <strong>NoIPforCert</strong> switch is $true, the IP address in the URI returned by the ESXi node (where the VM is running) will be converted to the FQDN of the ESXi node.</p>
<p><strong>Line 248-255,286-293</strong>: If the <strong>SkipCertificateCheck</strong> switch is used, the following Invoke-WebRequest is called with the setting to check checking certificates.</p>
<p><strong>Line 325-326</strong>: New she-bang entries for <strong>PowerShell v6</strong> and <strong>v7</strong>. Note that at the time this post was published, that v7 was still in preview.</p>
<p><strong>Line 331-345</strong>: When the <strong>SkipCertificateCheck</strong> is used and the PS version is pre-V6 and the type hasn&#8217;t been declared yet, the <strong>TrustAllCertsPolicy</strong> class is defined.</p>
<p><strong>Line 365-369</strong>: Additional &#8216;ready&#8217; test. If this property is not $true, the VMware Tools inside the guest OS are not ready to accept any <a href="https://vdc-download.vmware.com/vmwb-repository/dcr-public/790263bc-bd30-48f1-af12-ed36055d718b/e5f17bfc-ecba-40bf-a04f-376bbb11e811/vim.vm.guest.GuestOperationsManager.html" target="_blank" rel="noopener noreferrer">GuestOperations</a> related calls.</p>
<p><strong>Line 400</strong>: For any Windows guest OS, the function currently only accepts BAT, PS, PSv6 and PSv7 scripts.</p>
<p><strong>Line 411</strong>: For any Linux (this includes MacOS) guest OS, the function accepts all scripttypes, except BAT and PowerShell (meaning PS pre-v6).</p>
<p><strong>Line 438-444</strong>: <strong>sudo</strong> support. Each line of the user&#8217;s script containg the sudo command, will be prefixed with an echo command, which will provide the password to the sudo prompt.</p>
<p><strong>Line 465</strong>: In this version of the function, all files will be stored in a temporary directory in the guest OS.</p>
<p><strong>Line 510-517</strong>: The function allows the caller to copy one or more files, from the caller&#8217;s environment to the temporary folder in the guest OS. Since these files will be in the same folder as the actual script, the script can reference these files.</p>
<p><strong>Line 581-595</strong>: The function supports for a Windows guest OS, the use of PSv5.*, PSV6 and PSv7.</p>
<p><strong>Line 651-658</strong>: The function allows the caller to copy one or more files, from the folder, where the script ran in the guest OS, to the caller&#8217;s environment. This allows an alternative method to send data back to caller besides the stdin channel.</p>
<p><strong>Line 663-667</strong>: When the <strong>KeepFiles</strong> switch is not used, the function will remove the temporary directory in the guest OS, where all the script related files are stored.</p>



<h2 class="wp-block-heading">Usage</h2>



<p>The following section will show some examples of how to use the new features introduced in the <strong>v3</strong> of the <strong>Invoke-VMScriptPlus</strong> function.</p>



<h3 class="wp-block-heading">Sudo</h3>



<p>One of security measure on many Linux systems is that you need to use  <a rel="noreferrer noopener" aria-label="sudo (opens in a new tab)" href="https://linuxacademy.com/blog/linux/linux-commands-for-beginners-sudo/" target="_blank">sudo</a> to run commands with elevated privileges. </p>



<p>This is a typical example of such a case.</p>


<pre class="urvanov-syntax-highlighter-plain-tag">$vmName = 'ubuntuVM'
$credVM = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credVM.User, (ConvertTo-SecureString $credVM.Password -AsPlainText -Force))


$sInvoke = @{
&nbsp;&nbsp;&nbsp;&nbsp;VM = $vmName
&nbsp;&nbsp;&nbsp;&nbsp;GuestCredential = $cred
&nbsp;&nbsp;&nbsp;&nbsp;ScriptText = 'apt install gawk'
&nbsp;&nbsp;&nbsp;&nbsp;ScriptType = 'bash'
}
Invoke-VMScriptPlus @sInvoke</pre>


<p>The OS returns the following error.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="840" height="64" src="https://www.lucd.info/wp-content/uploads/2019/11/sudo-1.png" alt="" class="wp-image-6477" srcset="https://www.lucd.info/wp-content/uploads/2019/11/sudo-1.png 840w, https://www.lucd.info/wp-content/uploads/2019/11/sudo-1-300x23.png 300w, https://www.lucd.info/wp-content/uploads/2019/11/sudo-1-768x59.png 768w, https://www.lucd.info/wp-content/uploads/2019/11/sudo-1-720x55.png 720w" sizes="auto, (max-width: 840px) 100vw, 840px" /><figcaption>Elevated privileges required</figcaption></figure>



<p>Ok, let&#8217;s place <strong>sudo</strong> in front of that.</p>


<pre class="urvanov-syntax-highlighter-plain-tag">$vmName = 'ubuntuVM'
$credVM = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credVM.User, (ConvertTo-SecureString $credVM.Password -AsPlainText -Force))


$sInvoke = @{
&nbsp;&nbsp;&nbsp;&nbsp;VM = $vmName
&nbsp;&nbsp;&nbsp;&nbsp;GuestCredential = $cred
&nbsp;&nbsp;&nbsp;&nbsp;ScriptText = 'sudo apt install gawk'
&nbsp;&nbsp;&nbsp;&nbsp;ScriptType = 'bash'
}
Invoke-VMScriptPlus @sInvoke</pre>


<p>But that just makes the next issue obvious. How to answer to a prompt in a script. The method to solve this quite simple. Echo the password and then pipe it to the sudo command.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="519" height="35" src="https://www.lucd.info/wp-content/uploads/2019/11/sudo-2.png" alt="" class="wp-image-6478" srcset="https://www.lucd.info/wp-content/uploads/2019/11/sudo-2.png 519w, https://www.lucd.info/wp-content/uploads/2019/11/sudo-2-300x20.png 300w" sizes="auto, (max-width: 519px) 100vw, 519px" /></figure>



<p>But this not always a practical solution, and probably not very safe either. To avoid having to handle this in the code you sent to the guest OS, the Invoke-VMScriptPlus function has the <strong>Sudo</strong> switch.</p>


<pre class="urvanov-syntax-highlighter-plain-tag">$vmName = 'ubuntuVM'
$credVM = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credVM.User, (ConvertTo-SecureString $credVM.Password -AsPlainText -Force))


$sInvoke = @{
&nbsp;&nbsp;&nbsp;&nbsp;VM = $vmName
&nbsp;&nbsp;&nbsp;&nbsp;GuestCredential = $cred
&nbsp;&nbsp;&nbsp;&nbsp;ScriptText = 'sudo apt install gawk'
&nbsp;&nbsp;&nbsp;&nbsp;ScriptType = 'bash'
    Sudo = $true
}
Invoke-VMScriptPlus @sInvoke</pre>


<p>It will extract the password from the <strong>GuestCredential</strong> parameter value, and insert the <strong>echo with the password</strong> on each line in your code that starts with sudo.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="590" height="65" src="https://www.lucd.info/wp-content/uploads/2019/11/sudo-3.png" alt="" class="wp-image-6484" srcset="https://www.lucd.info/wp-content/uploads/2019/11/sudo-3.png 590w, https://www.lucd.info/wp-content/uploads/2019/11/sudo-3-300x33.png 300w" sizes="auto, (max-width: 590px) 100vw, 590px" /></figure>



<h2 class="wp-block-heading">PowerShell v7</h2>



<p>At the moment, of this writing, PowerShell v7 is still a preview.<br>Since you can install PSv6 and PSv7 side-by-side, you can run your scripts in either version.</p>



<p>This is a handy way to test your scripts for readiness for PSv7.</p>



<p>With the <strong>ScriptType</strong> parameter, you easily define against which PowerShell version your script should run. First we use <strong>powershellv6</strong>.</p>



<pre class="lang:ps decode:true ">$vmName = 'ubuntuVM'
$credVM&nbsp;=&nbsp;Get-VICredentialStoreItem&nbsp;-Host&nbsp;$vmName
$cred&nbsp;=&nbsp;New-Object&nbsp;System.Management.Automation.PSCredential&nbsp;($credVM.User,&nbsp;(ConvertTo-SecureString&nbsp;$credVM.Password&nbsp;-AsPlainText&nbsp;-Force))


$sInvoke&nbsp;=&nbsp;@{
&nbsp;&nbsp;&nbsp;&nbsp;VM&nbsp;=&nbsp;$vmName
&nbsp;&nbsp;&nbsp;&nbsp;GuestCredential&nbsp;=&nbsp;$cred
&nbsp;&nbsp;&nbsp;&nbsp;ScriptText&nbsp;=&nbsp;'$PSVersionTable'
&nbsp;&nbsp;&nbsp;&nbsp;ScriptType&nbsp;=&nbsp;'powershellv6'
}
Invoke-VMScriptPlus&nbsp;@sInvoke</pre>



<p>This results in.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="754" height="232" src="https://www.lucd.info/wp-content/uploads/2019/11/psv6.png" alt="" class="wp-image-6485" srcset="https://www.lucd.info/wp-content/uploads/2019/11/psv6.png 754w, https://www.lucd.info/wp-content/uploads/2019/11/psv6-300x92.png 300w, https://www.lucd.info/wp-content/uploads/2019/11/psv6-750x232.png 750w, https://www.lucd.info/wp-content/uploads/2019/11/psv6-720x222.png 720w" sizes="auto, (max-width: 754px) 100vw, 754px" /><figcaption>PSv6</figcaption></figure>



<p>And then the same, but with <strong>powershellv7</strong>.</p>



<pre class="lang:ps decode:true  ">$vmName = 'ubuntuVM'
$credVM = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credVM.User, (ConvertTo-SecureString $credVM.Password -AsPlainText -Force))


$sInvoke = @{
&nbsp;&nbsp;&nbsp;&nbsp;VM = $vmName
&nbsp;&nbsp;&nbsp;&nbsp;GuestCredential = $cred
&nbsp;&nbsp;&nbsp;&nbsp;ScriptText = '$PSVersionTable'
&nbsp;&nbsp;&nbsp;&nbsp;ScriptType = 'powershellv7'
}
Invoke-VMScriptPlus @sInvoke</pre>



<p>And now we get.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="760" height="232" src="https://www.lucd.info/wp-content/uploads/2019/11/psv7.png" alt="" class="wp-image-6486" srcset="https://www.lucd.info/wp-content/uploads/2019/11/psv7.png 760w, https://www.lucd.info/wp-content/uploads/2019/11/psv7-300x92.png 300w, https://www.lucd.info/wp-content/uploads/2019/11/psv7-720x220.png 720w" sizes="auto, (max-width: 760px) 100vw, 760px" /></figure>



<h3 class="wp-block-heading">Infile/OutFile</h3>



<p>The Invoke-VMScriptPlus function captures the output of your script on the stdin stream. But sometimes you would like to produce multiple outputs, or output in a specific format.</p>



<p>The same goes for your input to your script. You can pass input along to your script, for example with a here-string, but this at least requires extra steps to run your script.</p>



<p>For that reason, this v3 version of Invoke-VMScriptPlus, added two new parameters, <strong>Infile</strong> and <strong>OutFile</strong>.</p>



<p>Each of these parameters allow you to specify <strong>one or more files</strong> in your local environment that will be passed and/or retrieved to the environment where your script runs in the guest OS of the target VM.</p>



<p>A somewhat contrived example on what this allows you to do.</p>



<div>
<pre class="">$vmName = 'ubuntuVM'

$credVM = Get-VICredentialStoreItem -Host $vmName
$cred = New-Object System.Management.Automation.PSCredential ($credVM.User, (ConvertTo-SecureString $credVM.Password -AsPlainText -Force))

$code = @'
Get-Content -Path .\in.txt |
ForEach-Object -Process {
   $_ | Set-Content -Path ".\$_.txt"
}
'@

# Create input file
1..3 | Set-Content -Path .\in.txt
Get-ChildItem -Path . -Filter *.txt

$sInvoke = @{
   VM = $vmName
   GuestCredential = $cred
   ScriptText = $code
   ScriptType = 'powershellv7'
   InFile = 'in.txt'
   OutFile = '1.txt', '2.txt', '3.txt'
}
Invoke-VMScriptPlus @sInvoke

Get-ChildItem -Path . -Filter *.txt</pre>
</div>



<p>In short, the script creates an input file, then runs a script on the target VM, passing along the input file.<br>The script on the target VM creates a number of files, which are returned to the caller when the script completes.</p>



<p>This is the output from the above code.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="571" height="477" src="https://www.lucd.info/wp-content/uploads/2019/11/in-out-file-1.png" alt="" class="wp-image-6540" srcset="https://www.lucd.info/wp-content/uploads/2019/11/in-out-file-1.png 571w, https://www.lucd.info/wp-content/uploads/2019/11/in-out-file-1-300x251.png 300w" sizes="auto, (max-width: 571px) 100vw, 571px" /></figure>



<p>This is a handy feature when your script requires an existing file as input and produces multiple output files.</p>



<h3 class="wp-block-heading">NoIPinCert</h3>



<p>When the function needs to retrieve files from the guest OS, the ESXi node on which the VM is running, provides a URI. This URI normally contains the IP address of the ESXi node.</p>



<p>This can cause an issue when using certificates, and not bypassing the certificate check. The reason is sometimes that the IP address of the ESXi node is not included as a <strong>Subject Alternate Address</strong> (<a href="https://www.digicert.com/subject-alternative-name.htm" target="_blank" rel="noreferrer noopener" aria-label="SAN (opens in a new tab)">SAN</a>) in the certificate. This is visible through the following error message. Note that the following output also shows some verbose output to demonstrate when this is happening, and also to show that the error occurs when doing a GET with an URI that contains the IP address.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="74" src="https://www.lucd.info/wp-content/uploads/2019/11/san-1024x74.png" alt="" class="wp-image-6558" srcset="https://www.lucd.info/wp-content/uploads/2019/11/san-1024x74.png 1024w, https://www.lucd.info/wp-content/uploads/2019/11/san-300x22.png 300w, https://www.lucd.info/wp-content/uploads/2019/11/san-768x56.png 768w, https://www.lucd.info/wp-content/uploads/2019/11/san-720x52.png 720w, https://www.lucd.info/wp-content/uploads/2019/11/san.png 1196w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption>No IP SAN in certificate</figcaption></figure>



<p>To avoid this error, the Invoke-VMScriptPlus function, by default, translates this IP address into the FQDN of the ESXi node. The following verbose output shows the adapted URI for the GET</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="81" src="https://www.lucd.info/wp-content/uploads/2019/11/san-2-1024x81.png" alt="" class="wp-image-6559" srcset="https://www.lucd.info/wp-content/uploads/2019/11/san-2-1024x81.png 1024w, https://www.lucd.info/wp-content/uploads/2019/11/san-2-300x24.png 300w, https://www.lucd.info/wp-content/uploads/2019/11/san-2-768x61.png 768w, https://www.lucd.info/wp-content/uploads/2019/11/san-2-720x57.png 720w, https://www.lucd.info/wp-content/uploads/2019/11/san-2.png 1085w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>In some environments these automatic conversion of the IP address to the FQDN might cause an issue. One reason for this to happen might be that the ESXi node is added with it&#8217;s IP address. Another might be that the DNS query to get the FQDN of the ESXi node fails.<br>Especially for those occasion, the function now has this NoIPforCert switch. This switch, when set, instructs the function to NOT translate the IP address in the URI to the FQDN.</p>



<p>A sample call that uses this switch can look like this.</p>



<pre class="">$sInvoke = @{
    VM = $vmName
    ScriptType = 'bash'
    ScriptText = 'whoami'
    GuestCredential = $cred
    Verbose = $true
    NoIPinCert = $true
}
Invoke-VMScriptPlus @sInvoke</pre>



<p>Enjoy!</p>


]]></content:encoded>
					
					<wfw:commentRss>https://www.lucd.info/2019/11/17/invoke-vmscriptplus-v3/feed/</wfw:commentRss>
			<slash:comments>58</slash:comments>
		
		
			</item>
		<item>
		<title>At Your Fingertips</title>
		<link>https://www.lucd.info/2019/09/05/at-your-fingertips/</link>
					<comments>https://www.lucd.info/2019/09/05/at-your-fingertips/#comments</comments>
		
		<dc:creator><![CDATA[LucD]]></dc:creator>
		<pubDate>Thu, 05 Sep 2019 10:41:47 +0000</pubDate>
				<category><![CDATA[2019]]></category>
		<category><![CDATA[HBI1729BU]]></category>
		<category><![CDATA[PowerCLI]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[Profile]]></category>
		<category><![CDATA[VMworld]]></category>
		<category><![CDATA[Git]]></category>
		<guid isPermaLink="false">http://www.lucd.info/?p=6384</guid>

					<description><![CDATA[How often have you been finding out the PowerShell version you were using, [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>How often have you been finding out the PowerShell version you were using, or to which vSphere Server you were connected, or in which git repo/branch your code was being stored, or&#8230; Despair no more, it can now be available at your fingertips.</p>



<figure class="wp-block-gallery columns-1 is-cropped wp-block-gallery-1 is-layout-flex wp-block-gallery-is-layout-flex"><ul class="blocks-gallery-grid"><li class="blocks-gallery-item"><figure><img loading="lazy" decoding="async" width="1024" height="420" src="https://www.lucd.info/wp-content/uploads/2019/09/fingers-PCLI-1-1024x420.png" alt="" data-id="6393" data-link="https://www.lucd.info/?attachment_id=6393" class="wp-image-6393" srcset="https://www.lucd.info/wp-content/uploads/2019/09/fingers-PCLI-1.png 1024w, https://www.lucd.info/wp-content/uploads/2019/09/fingers-PCLI-1-300x123.png 300w, https://www.lucd.info/wp-content/uploads/2019/09/fingers-PCLI-1-768x315.png 768w, https://www.lucd.info/wp-content/uploads/2019/09/fingers-PCLI-1-720x295.png 720w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure></li></ul></figure>



<p>The following is a write up of a part of session <a rel="noreferrer noopener" href="https://my.vmworld.com/widget/vmware/vmworld19us/us19catalog?src=so_5cd1af8d1a916&amp;cid=7012H000001OK2o&amp;search=HBI1729BU" target="_blank">HBI1729BU</a> ,that was presented at <strong>VMworld US 2019</strong>. </p>



<p>The code shown in this post is also available in the <a rel="noreferrer noopener" aria-label="PowerCLI Community Repository (opens in a new tab)" href="https://github.com/vmware/PowerCLI-Example-Scripts/tree/master/Scripts/At_Your_Fingertips" target="_blank">PowerCLI Community Repository</a>.</p>



<span id="more-6384"></span>



<h2>Concept</h2>



<p>The idea behind this snippet came when I had checked what was in <strong>$global:DefaultVIServers</strong> for the n-th time in a short PowerCLI session. </p>



<p>And it&#8217;s probably not only that specific variable that must be all too familiar to most of us. Any entry in the following list of variables and cmdlets must have been executed by most of us multiple times.</p>



<pre class="wp-block-code"><code>$Global:DefaultVIServer
$Global:DefaultVIServers
$PSVersionTable
Get-Location
Get-Module -Name VMware.PowerCLI</code></pre>



<p>It would be an important timesaver, when the information returned by those instructions, would be permanently in view. <strong>At your fingertips</strong>, so to say.</p>
<p>The use of the <a href="https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_profiles?view=powershell-6" target="_blank" rel="noopener noreferrer">PowerShell profile(s)</a> and the <a href="https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_prompts?view=powershell-6" target="_blank" rel="noopener noreferrer">Prompt function</a>, immediately came to mind.</p>
<p>Not to overload your PS prompt, I decided to show part of the information in the console&#8217;s Window Title bar. That can be done by assigning a value to <strong>$host.ui.RawUI.WindowTitle</strong>.&nbsp;</p>



<h2 class="wp-block-heading">Implementation</h2>



<h3>The Code</h3>



<pre class="wp-block-code"><code>#
# Provide environment information in the PS Console
#
# History:
# 1.0 - August 4th 2019 - LucD
#       Initial version (for session HBI1729BU VMworld US 2019)
#
# 1) PS prompt
# - current (local) time
# - execution time of the previous command
# - shortened PWD
# 2) Window title
# - User/Admin
# - PS-32/54-Edition-Version
# - PCLI version
# - git repo/branch
# - VC/ESXi:defaultServer-User &#91;# connections]

function prompt
{
    # Current time
    $date = (Get-Date).ToString('HH:mm:ss')
    Write-Host -Object '&#91;' -NoNewline
    Write-Host -Object $date -ForegroundColor Cyan -BackgroundColor DarkBlue -NoNewline
    Write-Host -Object ']' -NoNewline

    # Execution time previous command
    $history = Get-History -ErrorAction Ignore -Count 1
    if ($history)
    {
        $time = (&#91;DateTime](New-TimeSpan -Start $history.StartExecutionTime -End $history.EndExecutionTime).Ticks).ToString('HH:mm:ss.ffff')
        Write-host -Object '&#91;' -NoNewLine
        Write-Host -Object "$time" -ForegroundColor Yellow -BackgroundColor DarkBlue -NoNewline
        Write-host -Object '] ' -NoNewLine
    }

    # Shorted PWD
    $path = $pwd.Path.Split('\')
    if ($path.Count -gt 3)
    {
        $path = $path&#91;0], '..', $path&#91;-2], $path&#91;-1]
    }
    Write-Host -Object "$($path -join '\')" -NoNewline

    # Prompt function needs to return something,
    # otherwise the default 'PS>' will be added
    "> "

    # Refresh the window's title
    Set-Title
}

function Set-Title
{
    # Running as Administrator or a regular user
    $userInfo = &#91;Security.Principal.WindowsIdentity]::GetCurrent()
    if ((New-Object Security.Principal.WindowsPrincipal $userInfo).IsInRole(&#91;Security.Principal.WindowsBuiltinRole]::Administrator))
    {
        $role = 'Admin'
    }
    else
    {
        $role = 'User'
    }

    # Usertype user@hostname
    $user = "$role $($userInfo.Name)@$($env:computername)"

    # PowerShell environment/PS version
    $bits = 32
    if (&#91;Environment]::Is64BitProcess)
    {
        $bits = 64
    }
    $ps = " - PS-$($bits): $PSEdition/$($PSVersionTable.PSVersion.ToString())"

    # PowerCLI version (derived from module VMware.PowerCLI)
    $pcliModule = Get-Module -Name VMware.PowerCLI -ListAvailable |
    Sort-Object -Property Version -Descending |
    Select-Object -First 1
    $pcli = " - PCLI: $(if($pcliModule){$pcliModule.Version.ToString()}else{'na'})"

    # If git is present and if in a git controlled folder, display repositoryname/current_branch
    $gitStr = ''
    if ((Get-Command -Name 'git' -CommandType Application -ErrorAction SilentlyContinue).Count -gt 0)
    {
        $gitTopLevel = git rev-parse --show-toplevel 2> $null
        if ($gitTopLevel.Length -ne 0)
        {
            $gitRepo = Split-Path -Path $gitTopLevel -Leaf
            $gitBranch = (git branch | Where-Object { $_ -match "\*" }).Trimstart('* ')
            $gitStr = " - git: $gitRepo/$gitBranch"
        }
    }

    # If there is an open vSphere Server connection
    # display &#91;VC|ESXi] last_connected_server-connected_user &#91;number of open server connections]
    if ($global:defaultviserver)
    {
        $vcObj = (Get-Variable -Scope global -Name 'DefaultVIServer').Value
        if ($vcObj.ProductLine -eq 'vpx')
        {
            $vcSrv = 'VC'
        }
        else
        {
            $vcSrv = 'ESXi'
        }
        $vc = " - $($vcSrv): $($vcObj.Name)-$($vcObj.User) &#91;$($global:DefaultVIServers.Count)]"
    }

    # Update the Window's title
    $host.ui.RawUI.WindowTitle = "$user$ps$pcli$vc$gitStr"
}

# Set title after starting session
Set-Title</code></pre>



<h3 class="wp-block-heading">Annotations</h3>



<p><strong>Line 19-51</strong>: Replacement function for the PS prompt.</p>



<p><strong>Line 22-25</strong>: Current time in the format &#8216;HH:mm:ss&#8217;. Note that &#8216;HH&#8217; asks for a 24-hour value.</p>



<p><strong>Line 28-35</strong>: The execution time of the previous command, in the format &#8216;HH:mm:ss:ffff&#8217;.</p>



<p><strong>Line 28</strong>: The execution time is based on the content of <a href="https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_history?view=powershell-6">PS History</a>. With <a href="https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/get-history?view=powershell-6">Get-History</a> we obtain the start time of the command, and with <a rel="noreferrer noopener" aria-label="New-TimeSpan (opens in a new tab)" href="https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/new-timespan?view=powershell-6" target="_blank">New-TimeSpan</a> we obtain the duration for running that command.</p>



<p><strong>Line 38-43</strong>: These lines take the path to the current folder, and shorten that path to only show the start of the path and the last two directories. When the path is shortened, it is  indicated by two dots.</p>



<p><strong>Line 53-114</strong>: Function to populate the Windows Title bar</p>



<p><strong>Line 56-64</strong>: These lines determine if the PS session is started &#8220;As Administrator&#8221; or as a regular user.</p>



<p><strong>Line 67</strong>: The user and station information.</p>



<p><strong>Line 70-75</strong>: The PS edition (Desktop or Core) and the PS version.</p>



<p><strong>Line 78-81</strong>: If installed, the version of VMware PowerCLI. Note that this looks at the VMware.PowerCLI module to obtain that version number.</p>



<p><strong>Line 84-94</strong>: If the git command is present on the station, these lines will check if the current directory is inside a git repository. It will then display the name of the git repository and the current branch.</p>



<p><strong>Line 98-110</strong>: These lines check if there is an open connection to a vSphere Server. It will show if the connected vSphere Server is a vCenter or an ESXi node. </p>



<p><strong>Line 109</strong>: If multiple connections to a vSphere Servers are open, this will display the number of these connections. The vSphere Server that is shown is the last connected one. This corresponds with what is kept in the<strong> $global:DefaultVIServer</strong> variable.</p>



<p><strong>Line 117</strong>: Initial call to the <strong>Set-Title </strong>function. This will populate the Windows Title bar when the PS console is opened.</p>



<h3>Installation</h3>



<p>You can just copy and rename the code to one of the <a rel="noreferrer noopener" aria-label="profile files (opens in a new tab)" href="https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_profiles?view=powershell-6" target="_blank">profile files</a>. To make that process easier, I also provide the following installation script.</p>



<pre class="wp-block-code"><code>[cmdletbinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
param(
    #        [Parameter(Mandatory = $true)]
    [ValidateSet('CurrentUserCurrentHost', 'CurrentUserAllHosts',
        'AllUsersCurrentHost', 'AllUsersAllHosts')]
    [string]$Scope,
    [switch]$NoClobber,
    [switch]$Backup,
    [string]$NewProfile = '.\NewProfile.ps1'
)

if ($PSCmdlet.ShouldProcess("$($Profile.$Scope)", "Create $Scope profile"))
{
    $profilePath = $Profile."$Scope"
    Write-Verbose -Message "Target is $profilePath"
    $createProfile = $true
    if (Test-Path -Path $profilePath)
    {
        Write-Verbose -Message "Target exists"
        if ($NoClobber)
        {
            Write-Verbose -Message "Cannot overwrite target due to NoClobber"
            $createProfile = $false
        }
        elseif ($Backup)
        {
            Write-Verbose -Message "Create a backup as $profilePath.bak"
            Copy-Item -Path $profilePath -Destination "$profilePath.bak" -Confirm:$false -Force
        }
        elseif (-not $NoClobber)
        {
            Write-Verbose -Message "Target will be overwritten"
        }
        else
        {
            Write-Verbose -Message "Use -NoClobber:$false or -Backup"
        }
    }
    if ($createProfile)
    {
        if (-not $NewProfile)
        {
            $script:MyInvocation.MyCommand | select *
            $folder = Split-Path -Parent -Path $script:MyInvocation.MyCommand.Path
            $folder = Get-Location
            $NewProfile = "$folder\NewProfile.ps1"
        }
        Write-Verbose -Message "New profile expected at $NewProfile"
        if (Test-Path -Path $NewProfile)
        {
            Write-Verbose -Message "Copy $NewProfile to $profilePath"
            Copy-Item -Path $NewProfile -Destination $profilePath -Confirm:$false
        }
        else
        {
            Write-Warning -Message "Could not find the new profile file!"
            Write-Warning -Message "Use the NewProfile parameter or store a NewProfile.ps1 file in folder $folder."
        }
    }
}</code></pre>



<p>Save the above code to a .ps1 file, let&#8217;s say <strong>install-profile.ps1</strong>. Also copy the profile code to a .ps1 file, let&#8217;s say <strong>newprofile.ps1</strong>.</p>



<p>You can now install this new profile with a simple call.</p>



<pre class="wp-block-code"><code>.\install-profile.ps1 -NewProfile .\NewProfile.ps1 -Scope CurrentUserCurrentHost -Backup </code></pre>



<p>The installation script has a number of parameters:</p>



<ul class="wp-block-list"><li><strong>NewProfile</strong> : the file that contains the profile you want to install</li><li><strong>Scope</strong> : for which Scope you want to install the new profile.</li><li><strong>Backup</strong> : backup the current profile before overwriting it.</li><li><strong>NoClobber</strong> : avoids overwriting an existing profile file.</li></ul>



<h3 class="wp-block-heading">Usage</h3>



<p>When the new profile is installed, and when you restart your console session, you will see the new profile in action.</p>



<p>The following screenshot shows the features of the new profile.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1024" height="346" src="https://www.lucd.info/wp-content/uploads/2019/09/demo-1024x346.png" alt="" class="wp-image-6413" srcset="https://www.lucd.info/wp-content/uploads/2019/09/demo-1024x346.png 1024w, https://www.lucd.info/wp-content/uploads/2019/09/demo-300x101.png 300w, https://www.lucd.info/wp-content/uploads/2019/09/demo-768x259.png 768w, https://www.lucd.info/wp-content/uploads/2019/09/demo-720x243.png 720w, https://www.lucd.info/wp-content/uploads/2019/09/demo.png 1223w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>And the following short video shows the installation process and the features the profile offers.</p>



<figure class="wp-block-video"><video height="1080" style="aspect-ratio: 1764 / 1080;" width="1764" controls src="https://www.lucd.info/wp-content/uploads/2019/09/VMworld19-Profile.mp4"></video><figcaption>At Your Fingertips</figcaption></figure>



<p>Enjoy!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.lucd.info/2019/09/05/at-your-fingertips/feed/</wfw:commentRss>
			<slash:comments>5</slash:comments>
		
		<enclosure url="https://www.lucd.info/wp-content/uploads/2019/09/VMworld19-Profile.mp4" length="3867096" type="video/mp4" />

			</item>
	</channel>
</rss>
