<?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>powershellv6 Archives - LucD notes</title>
	<atom:link href="https://www.lucd.info/category/powershell/powershellv6/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.lucd.info/category/powershell/powershellv6/</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</generator>

<image>
	<url>https://www.lucd.info/wp-content/uploads/2018/12/cropped-120px-Tibetan_Dharmacakra-32x32.png</url>
	<title>powershellv6 Archives - LucD notes</title>
	<link>https://www.lucd.info/category/powershell/powershellv6/</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>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 fetchpriority="high" 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="(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 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="(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 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="(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>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>
	</channel>
</rss>
