<?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>PowerCLI Archives - LucD notes</title>
	<atom:link href="https://www.lucd.info/category/powershell/powercli/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.lucd.info/category/powershell/powercli/</link>
	<description>My PowerShell ramblings</description>
	<lastBuildDate>Thu, 24 Dec 2020 06:58:45 +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>PowerCLI Archives - LucD notes</title>
	<link>https://www.lucd.info/category/powershell/powercli/</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>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>
		<item>
		<title>Optimize-VMwarePKS</title>
		<link>https://www.lucd.info/2019/04/03/optimize-vmwarepks/</link>
					<comments>https://www.lucd.info/2019/04/03/optimize-vmwarepks/#comments</comments>
		
		<dc:creator><![CDATA[LucD]]></dc:creator>
		<pubDate>Wed, 03 Apr 2019 19:29:51 +0000</pubDate>
				<category><![CDATA[DRS]]></category>
		<category><![CDATA[Folder]]></category>
		<category><![CDATA[PKS]]></category>
		<category><![CDATA[PowerCLI]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[Cluster]]></category>
		<category><![CDATA[Optimise]]></category>
		<guid isPermaLink="false">http://www.lucd.info/?p=6336</guid>

					<description><![CDATA[A guest post Over the last couple of weeks, it has been my [&#8230;]]]></description>
										<content:encoded><![CDATA[
<h3 class="wp-block-heading">A guest post</h3>



<p>Over the last couple of weeks, it has been my great pleasure to assist  <a rel="noreferrer noopener" aria-label=" (opens in a new tab)" href="https://twitter.com/chipzoller" target="_blank">Chip Zoller</a> in writing a <a rel="noreferrer noopener" aria-label="VMware PowerCLI (opens in a new tab)" href="https://communities.vmware.com/community/vmtn/automationtools/powercli" target="_blank">VMware PowerCLI</a> script, named <a rel="noreferrer noopener" aria-label="Optimize-VMwarePKS (opens in a new tab)" href="https://github.com/chipzoller/Optimize-VMwarePKS" target="_blank">Optimize-VMwarePKS</a>. This script helps organise your PKS deployment at three levels: folders, tags and DRS rules, including functionality to run a clean up.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="858" height="546" src="https://www.lucd.info/wp-content/uploads/2019/03/pks-pcli.png" alt="" class="wp-image-6337" srcset="https://www.lucd.info/wp-content/uploads/2019/03/pks-pcli.png 858w, https://www.lucd.info/wp-content/uploads/2019/03/pks-pcli-300x191.png 300w, https://www.lucd.info/wp-content/uploads/2019/03/pks-pcli-768x489.png 768w, https://www.lucd.info/wp-content/uploads/2019/03/pks-pcli-720x458.png 720w" sizes="auto, (max-width: 858px) 100vw, 858px" /><figcaption>VMware PKS</figcaption></figure>



<p>The following post by Chip describes the function in greater detail, and shows how you can use it. </p>



<p>This post also appears on the <strong>Sovereign Systems</strong> website as <a rel="noreferrer noopener" aria-label="Optimize-VMwarePKS: A PowerShell Script for All Your VMware PKS Deployment Needs (opens in a new tab)" href="https://www.sovsystems.com/optimize-vmwarepks-a-powershell-script-for-all-your-vmware-pks-deployment-needs/" target="_blank">Optimize-VMwarePKS: A PowerShell Script for All Your VMware PKS Deployment Needs</a>.</p>



<p>Take it away Chip.</p>



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



<h2 class="wp-block-heading">Optimize-VMwarePKS: A
PowerShell script for all your VMware PKS deployment needs</h2>



<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Ever since
VMware PKS (now called Enterprise PKS) came onto the market over a year ago,
it’s been a big hit. With it, you get upstream Kubernetes, NSX-T, an enterprise-class
container registry, automation of the entire K8s cluster creation process, and
lots more all on top of the de facto private cloud platform of vSphere. It’s
truly becoming the way organizations are standardizing on K8s cluster
instantiation, upgrade, and management on-premises. The deployment nodes
themselves are regular virtual machines. And, like all virtual machines, you
and your company probably have an established way of organizing those VMs.
While Enterprise PKS takes care of a lot of tasks automatically, one thing that
we still need to consider is how those VMs get organized. So, as a joint effort
between myself and <a href="https://twitter.com/LucD22">Luc Dekens</a>, we’re
glad to announce today a new script called <strong>Optimize-VMwarePKS</strong>
which is designed to bring that level of optimization to your vSphere and PKS
clusters. Specifically, this script is designed to optimize your PKS
deployments in three ways: vSphere folders, vSphere tags, and DRS rules–with
clean-up for three of these. Read on to see this in action and learn how you
can greatly simplify deployment of PKS clusters in your own environment.</p>



<h3 class="wp-block-heading"><strong>FOLDERS</strong>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </h3>



<p>As mentioned, PKS deploys K8s nodes
as simple VMs into your vSphere environment. After a single deployment, you may
see something like this in your vSphere Client.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="317" height="165" src="https://www.lucd.info/wp-content/uploads/2019/03/2019-03-24_16-35-40.png" alt="" class="wp-image-6338" srcset="https://www.lucd.info/wp-content/uploads/2019/03/2019-03-24_16-35-40.png 317w, https://www.lucd.info/wp-content/uploads/2019/03/2019-03-24_16-35-40-300x156.png 300w" sizes="auto, (max-width: 317px) 100vw, 317px" /></figure>



<p>All VMs are, as you can see, named with IDs that are doled
out by PKS and BOSH, and tracked internally. They probably look very different
from the internal naming standard you’ve developed, right? Although we shouldn’t
change the names of the VMs, we can do other things. The first feature of <strong>Optimize-VMwarePKS</strong> is the ability to place
all VMs from a given deployment automatically into a folder based on that name.
In so doing, the above screenshot now looks like this after optimization:</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="367" height="311" src="https://www.lucd.info/wp-content/uploads/2019/03/2019-03-24_16-59-11.png" alt="" class="wp-image-6339" srcset="https://www.lucd.info/wp-content/uploads/2019/03/2019-03-24_16-59-11.png 367w, https://www.lucd.info/wp-content/uploads/2019/03/2019-03-24_16-59-11-300x254.png 300w" sizes="auto, (max-width: 367px) 100vw, 367px" /></figure>



<p>As you can see, these folder names align with the PKS
cluster names perfectly if we examine them with a pks clusters
command. In fact, <strong>Optimize-VMwarePKS</strong>
is designed to take this information directly from the PKS CLI tool, which is a
requirement for the script (see later).</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="658" height="86" src="https://www.lucd.info/wp-content/uploads/2019/03/2019-03-24_17-00-30.png" alt="" class="wp-image-6340" srcset="https://www.lucd.info/wp-content/uploads/2019/03/2019-03-24_17-00-30.png 658w, https://www.lucd.info/wp-content/uploads/2019/03/2019-03-24_17-00-30-300x39.png 300w" sizes="auto, (max-width: 658px) 100vw, 658px" /></figure>



<p>The nice thing about this script is </p>



<ol class="wp-block-list"><li>it’s very easy to operate (simply add the –ProcessFolders switch)</li><li>it’s idempotent so will keep this organization
no matter how many times you run it.</li></ol>



<h3 class="wp-block-heading"><strong>TAGS</strong></h3>



<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; The second
feature of <strong>Optimize-VMwarePKS</strong> to highlight
is the ability to assign vSphere Tags to all of the VMs that form a given PKS
deployment. Tags are often used to provide a form of organization that may
exist alongside or independent of vSphere folders. These tags are, again by
default, created based on the name of the PKS cluster and automatically created
and assigned to all VMs part of a cluster. With a tag category of your choice
(or by accepting the default category of ‘PKS’), the script will detect your
clusters, create a new appropriate tag in the tag category, and assign that tag
to all VMs.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="187" height="232" src="https://www.lucd.info/wp-content/uploads/2019/03/2019-03-24_17-11-01.png" alt="" class="wp-image-6341"/></figure>



<p>As you can see here, the script has created two tags for us
that match the names of our clusters. If we inspect the VMs within those
clusters, we can see it’s been correctly assigned as well.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="689" height="278" src="https://www.lucd.info/wp-content/uploads/2019/03/2019-03-24_17-12-03.png" alt="" class="wp-image-6342" srcset="https://www.lucd.info/wp-content/uploads/2019/03/2019-03-24_17-12-03.png 689w, https://www.lucd.info/wp-content/uploads/2019/03/2019-03-24_17-12-03-300x121.png 300w" sizes="auto, (max-width: 689px) 100vw, 689px" /></figure>



<p>This optimization is easily enabled with the -ProcessTags switch.</p>



<h3 class="wp-block-heading"><strong>DRS RULES</strong></h3>



<p>            Since PKS has the ability to deploy HA K8s clusters–which is a tremendous strength and huge time saver–it automatically provisions multiple masters and places them behind an NSX-T load balancer. These masters then serve as the API entry point for all K8s-related commands. Masters can be spread out across multiple Availability Zones (AZ). Where AZs map directly onto distinct vSphere clusters, this provides a sufficient level of fault isolation. But in the case where either multiple Azs aren’t in use or they do not align to separate compute clusters, Enterprise PKS will place all masters on the same cluster. Optimize-VMwarePKS can therefore be used to build DRS anti-affinity rules to provide such separation.</p>



<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; In my
previous screenshots, you’ve noticed I have a single small cluster (1 master; 3
workers) and a large cluster (3 masters; 3 workers). If we run the script with
the -ProcessDRSRules switch, it will
automatically find those masters and create a DRS anti-affinity rule for us.
And not to worry, it’ll only create the rule with the masters of each cluster,
so multiple HA clusters get individual DRS rules created. See below. Even
better, the script is written in a future-proof way in that when VMware/Pivotal
support larger multi-master clusters in the future, if you scaled out masters
the script will detect the increase and update the rule for you!</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="527" height="592" src="https://www.lucd.info/wp-content/uploads/2019/03/2019-03-24_17-19-04.png" alt="" class="wp-image-6343" srcset="https://www.lucd.info/wp-content/uploads/2019/03/2019-03-24_17-19-04.png 527w, https://www.lucd.info/wp-content/uploads/2019/03/2019-03-24_17-19-04-267x300.png 267w" sizes="auto, (max-width: 527px) 100vw, 527px" /></figure>



<p>These abilities are, as I hope you’ll agree, extremely
useful in order to not only fill some gaps but bring VMware PKS deployments
into alignment with your vSphere policies today. But there’s one thing missing
that we haven’t covered yet. Kubernetes clusters may come and go (another big
win for Enterprise PKS is the automatic roll-back of NSX-T objects!), and when
they do we want to ensure we aren’t leaving behind elements that are no longer
relevant. So the last portion of <strong>Optimize-VMwarePKS</strong>
is its ability to automatically maintain a tidy house.</p>



<h3 class="wp-block-heading"><strong>TIDYING UP</strong></h3>



<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Each of the
three optimizations shown previously (folders, tags, and DRS rules) can be
switched on independently. And with each one, except DRS rules, if a cluster
gets deleted we’d have some clean-up work to do. So the script has also been
written with switches that you can (optionally, of course) automate the
clean-up of these objects. We’re talking here about tidying up of vSphere
folders and vSphere tags. By adding the -TidyFolders
and/or -TidyTags switches, <strong>Optimize-VMwarePKS</strong> will search for these empty folders and tags and
remove them for you. But, not to fear, it will *only* do this if you’ve
provided these switches and also only consider the parent vSphere folder or tag
category for clean-up. That means it won’t scour your entire estate and remove
anything else.</p>



<h3 class="wp-block-heading"><strong>IDEMPOTENCE</strong></h3>



<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Lastly, and
before we look at some running examples, is to point out the idempotent nature
of this script. Because you may have many groups of people bring up and down
PKS deployments, you need something that can maintain a ready state of what you
define, similar to tools like Ansible, Puppet, and Chef. Being able to schedule
this script is one of the main reasons it was written, and in so doing it must
be made idempotent. No matter how many times you run the script, it always
checks if things exist before making a change. You can be confident that when
you ask it to run with whatever parameters that it’ll maintain that state
exactly. So no fear if someone drops a tag or moves a VM out of a folder. Next
time it runs it’ll put it right back where it was.</p>



<h3 class="wp-block-heading"><strong>RUNNING
OPTIMIZE-VMWAREPKS</strong></h3>



<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Now you’re
aware of all its capabilities, let’s quickly look at what’s needed to run it
and then an example on how to run it.</p>



<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; The script
needs PowerShell, <a href="https://www.powershellgallery.com/packages/VMware.PowerCLI/">VMware PowerCLI</a>,
and the <a href="https://network.pivotal.io/products/pivotal-container-service">PKS
CLI binary tool</a>, all of which are cross-platform at this point. In fact, it
has been tested running directly from the Pivotal Operations Manager appliance
running Ubuntu with <a href="https://github.com/PowerShell/PowerShell">PowerShell
Core</a> but runs perfectly fine from a Windows control machine as well. The
PKS CLI tool needs to be added to your PATH. You also must ensure you have
credentials and network connectivity to access both vCenter Server and PKS.</p>



<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Now you
have the prereqs, time to actually run this script. Grab it from the <a href="https://github.com/chipzoller/Optimize-VMwarePKS">GitHub repo here</a>. For
your convenience (and so you don’t have to either read a blog or inspect the
code), extensive help has been provided on how to operate the script, its
parameters, but some examples.</p>



<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; All the
switches can be run independently, so you can mix and match whatever
functionality you like and only it will run. If we want to let it process
folders, tags, and DRS rules for us, we could call it like so:</p>



<pre class="wp-block-code"><code>$secvCPass = ConvertTo-SecureString -String 'VMware1!' -asPlainText -Force
$credvC = New-Object System.Management.Automation.PSCredential('administrator@vsphere.local',$secvCPass)
$secPKSPass = ConvertTo-SecureString -String 'VMware1!' -asPlainText -Force
$credPKS = New-Object System.Management.Automation.PSCredential('myuser',$secPKSPass)

./Optimize-VMwarePKS.ps1 -ProcessFolders -ProcessTags -ProcessDRSRules -vCenter $vc -vCenterCredential $credvC -PKSSever $pks -PKSCredential $credPKS -Verbose</code></pre>



<p>We have to pass a PSCredential object to the script, and so
the lines before actually running it do that for us.</p>



<p>If all you wanted was to clean-up (tidy) those objects, run
it with only those flags set:</p>



<pre class="wp-block-code"><code>./Optimize-VMwarePKS.ps1 -TidyFolders -TidyTags -vCenter $vc -vCenterCredential $credvC -PKSSever $pks -PKSCredential $credPKS</code></pre>



<p>Or, combine all flags into a single command:</p>



<pre class="wp-block-code"><code>./Optimize-VMwarePKS.ps1 -ProcessFolders -ProcessTags -ProcessDRSRules -TidyFolders -TidyTags -vCenter $vc -vCenterCredential $credvC -PKSSever $pks -PKSCredential $credPKS</code></pre>



<p>As far as managing the parameters needed, one of the most
convenient ways is with <a href="https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_splatting?view=powershell-6">splatting</a>
in PowerShell. We can write a script, call it “wrapper.ps1” and put all
parameters inside of it as shown below. Note that with splatting, we must set
the values to “true” unlike when passing the switches to the script directly. The
final line simply calls <strong>Optimize-VMwarePKS</strong>
for us passing the entire splat as an argument.</p>



<pre class="wp-block-code"><code>$vCUser = 'myuser@mydomain.com'
$vCPass = 'VMware1!' | ConvertTo-SecureString -asPlainText -Force
$PKSUser = 'myuser'
$PKSPass = 'VMware1!' | ConvertTo-SecureString -asPlainText -Force

$params = @{
ProcessFolders = $true
ProcessTags = $true
ProcessDRSRules = $true
TidyFolders = $true
TidyTags = $true
vCenter = 'myvcenter.mydomain.com'
PKSServer = 'mypks.mydomain.com'
vCenterCredential = New-Object System.Management.Automation.PSCredential($vCUser,$vCPass)
PKSCredential = New-Object System.Management.Automation.PSCredential($PKSUser,$PKSPass)
}

/home/ubuntu/Optimize-VMwarePKS.ps1 @params</code></pre>



<p>And, as mentioned earlier, because this script is entirely
idempotent, it can easily be scheduled with either cron, Windows task
scheduler, or any other mechanism you want. This will ensure your environment
is kept optimized based on your rules and patterns with no manual clean-up
required.</p>



<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; I hope you find this script to be valuable in your environments. If so, we’d love to hear about it and you can drop either Luc or Chip a line on Twitter at <a href="https://twitter.com/LucD22" target="_blank" rel="noreferrer noopener" aria-label="@LucD22 (opens in a new tab)">@LucD22</a> or <a rel="noreferrer noopener" aria-label="@chipzoller (opens in a new tab)" href="https://twitter.com/chipzoller" target="_blank">@chipzoller</a> respectively.</p>



<hr class="wp-block-separator"/>



<p>Thanks Chip.<br></p>



<p>Enjoy!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.lucd.info/2019/04/03/optimize-vmwarepks/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
			</item>
		<item>
		<title>DSCR for VMware and you!</title>
		<link>https://www.lucd.info/2018/12/16/dscr-for-vmware-and-you/</link>
					<comments>https://www.lucd.info/2018/12/16/dscr-for-vmware-and-you/#comments</comments>
		
		<dc:creator><![CDATA[LucD]]></dc:creator>
		<pubDate>Sun, 16 Dec 2018 20:50:30 +0000</pubDate>
				<category><![CDATA[DSC]]></category>
		<category><![CDATA[PowerCLI]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[vSphere]]></category>
		<guid isPermaLink="false">http://www.lucd.info/?p=6147</guid>

					<description><![CDATA[On December 13th 2018 the PowerCLI Team provided us with an early end-of-year [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>On <strong>December 13th 2018</strong> the PowerCLI Team provided us with an early end-of-year present. The <strong>Desired State  Configuration Resources (DSCR) for VMware</strong> are published, and they are <strong><a rel="noreferrer noopener" aria-label="On December 13th 2018 the PowerCLI Team provided us with an early Christmas present. The Desired State  Configuration Resources for VMware were published, and they are open sourced!
If you missed the announcement, hold what you are doing and go read the VMware PowerCLI blog post&nbsp;Getting Started with Desired State Configuration Resources for VMware. (opens in a new tab)" href="https://github.com/vmware/dscr-for-vmware/releases" target="_blank">open sourced</a></strong>!<br>If you missed the announcement, hold what you are doing, and go read the <a rel="noreferrer noopener" aria-label="On December 13th 2018 the PowerCLI Team provided us with an early Christmas present. The Desired State  Configuration Resources for VMware were published, and they are open sourced!
If you missed the announcement, hold what you are doing and go read the VMware PowerCLI blog post&nbsp;Getting Started with Desired State Configuration Resources for VMware. (opens in a new tab)" href="https://communities.vmware.com/community/vmtn/automationtools/powercli" target="_blank">VMware PowerCLI</a> blog post&nbsp;<a rel="noreferrer noopener" aria-label="On December 13th 2018 the PowerCLI Team provided us with an early Christmas present. The Desired State  Configuration Resources for VMware were published, and they are open sourced!
If you missed the announcement, hold what you are doing and go read the VMware PowerCLI blog post&nbsp;Getting Started with Desired State Configuration Resources for VMware. (opens in a new tab)" href="https://blogs.vmware.com/PowerCLI/2018/12/getting-started-dsc-for-vmware.html" target="_blank">Getting Started with Desired State Configuration Resources for VMware</a> right now!</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="629" height="472" src="https://www.lucd.info/wp-content/uploads/2018/12/dscr.jpg" alt="" class="wp-image-6150" srcset="https://www.lucd.info/wp-content/uploads/2018/12/dscr.jpg 629w, https://www.lucd.info/wp-content/uploads/2018/12/dscr-300x225.jpg 300w" sizes="auto, (max-width: 629px) 100vw, 629px" /></figure>



<p>The next question on your mind is probably &#8220;<em>How can I contribute?</em>&#8220;. Well, with the correct tools and some <a rel="noreferrer noopener" aria-label="The next question on your mind is probably &quot;How can I contribute?&quot;. Well, with the correct tools and some VMware PowerCLI knowledge, it turns out this is not too difficult. (opens in a new tab)" href="https://communities.vmware.com/community/vmtn/automationtools/powercli" target="_blank">VMware PowerCLI</a> knowledge, it turns out that this is not too difficult. What follows is my first attempt at contributing to the&nbsp;<strong>Desired State Configuration Resources for VMware</strong>.</p>



<p><span id="more-6147"></span></p>



<h2 class="wp-block-heading">Repositories, tools and clones</h2>



<p>For most of you what follows might probably state the obvious, but I included all the steps for completeness.</p>



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



<p>The <a rel="noreferrer noopener" aria-label="The DSCR for VMware are published on a github repository. (opens in a new tab)" href="https://github.com/vmware/dscr-for-vmware" target="_blank">DSCR for VMware</a> are published in a GitHub repository. So you will need access to GitHub. Create yourself a GitHub account, see&nbsp;<a rel="noreferrer noopener" aria-label="The DSCR for VMware are published in a github repository. So you will need access to github. For that create yourself a github account, see&nbsp;Signing up for a new GitHub account. (opens in a new tab)" href="https://help.github.com/articles/signing-up-for-a-new-github-account/" target="_blank">Signing up for a new GitHub account</a>.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1024" height="816" src="https://www.lucd.info/wp-content/uploads/2018/12/dscr-vmware-1024x816.jpg" alt="" class="wp-image-6164" srcset="https://www.lucd.info/wp-content/uploads/2018/12/dscr-vmware-1024x816.jpg 1024w, https://www.lucd.info/wp-content/uploads/2018/12/dscr-vmware-300x239.jpg 300w, https://www.lucd.info/wp-content/uploads/2018/12/dscr-vmware-768x612.jpg 768w, https://www.lucd.info/wp-content/uploads/2018/12/dscr-vmware-720x574.jpg 720w, https://www.lucd.info/wp-content/uploads/2018/12/dscr-vmware.jpg 1106w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>Once you are on GitHub, go to the&nbsp;<a rel="noreferrer noopener" href="https://github.com/vmware/dscr-for-vmware" target="_blank">DSCR for VMware</a>&nbsp;repository. Since you don&#8217;t want to be messing with that code directly, create a <strong>fork</strong> of the repository. For instructions on that see <a rel="noreferrer noopener" aria-label="Once you are on GitHub, go to the&nbsp;DSCR for VMware&nbsp;repository. Since you don't to be messing with that code directly, create a fork of the repository. For instructions on that see Fork a Repo. (opens in a new tab)" href="https://help.github.com/articles/fork-a-repo/" target="_blank">Fork a Repo</a>.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1024" height="111" src="https://www.lucd.info/wp-content/uploads/2018/12/fork-1024x111.jpg" alt="" class="wp-image-6155" srcset="https://www.lucd.info/wp-content/uploads/2018/12/fork-1024x111.jpg 1024w, https://www.lucd.info/wp-content/uploads/2018/12/fork-300x32.jpg 300w, https://www.lucd.info/wp-content/uploads/2018/12/fork-768x83.jpg 768w, https://www.lucd.info/wp-content/uploads/2018/12/fork-720x78.jpg 720w, https://www.lucd.info/wp-content/uploads/2018/12/fork.jpg 1108w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>That completes the repository part. You now have a copy of the&nbsp;&nbsp;<a rel="noreferrer noopener" href="https://github.com/vmware/dscr-for-vmware" target="_blank">DSCR for VMware</a> repository under your own GitHub account.</p>



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



<p>I understand that you are eager to add your own code to the DSCR for VMware module, but there are a few other preparatory steps you will have to take before you can actually start coding.</p>



<p>Determine on which station you are going to do your coding. On this station you will have to install some tools and some PowerShell modules.</p>



<h4 class="wp-block-heading">Git commands</h4>



<p>Since you will be working with a GitHub repository, you will need the <strong>Git commands</strong>. You can download the installation file from the <a rel="noreferrer noopener" aria-label="Since you will be working with a GitHub repository, you will need the Git commands. You can download the installation file from the Git - Downloads page. (opens in a new tab)" href="https://git-scm.com/downloads" target="_blank">Git &#8211; Downloads page</a>.</p>



<h4 class="wp-block-heading">Git GUI (optional)</h4>



<p>This is not really needed, but it can make your life a lot easier. Instead of entering all the Git commands from a prompt, you can go for a Git GUI. There are several of these available, each with its own pro and cons.</p>



<p>Personally I prefer to use <a rel="noreferrer noopener" aria-label="Personally I use GitKraken.&nbsp; (opens in a new tab)" href="https://www.gitkraken.com/download" target="_blank">GitKraken</a>.&nbsp;</p>



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



<p>Your choice of editor can be whatever you prefer to work with PowerShell code. My editor of choice is currently <a rel="noreferrer noopener" aria-label="Your choice of editor can be whatever you prefer to work with PowerShell code. My editor of choice is currently Visual Studio Code with (opens in a new tab)" href="https://code.visualstudio.com/download" target="_blank">Visual Studio Code</a> with the <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.PowerShell" target="_blank" rel="noreferrer noopener" aria-label="Your choice of editor can be whatever you prefer to work with PowerShell code. My editor of choice is currently Visual Studio Code with the PowerShell Extension. (opens in a new tab)">PowerShell Extension</a>.</p>



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



<p>To contribute to the&nbsp;<a rel="noreferrer noopener" href="https://github.com/vmware/dscr-for-vmware" target="_blank">DSCR for VMware</a> module, and follow the&nbsp;<a href="https://github.com/vmware/dscr-for-vmware/blob/master/CODING_GUIDELINES.md" target="_blank" rel="noreferrer noopener" aria-label="To contribute to the&nbsp;DSCR for VMware module, and follow the&nbsp;Coding guidelines, you will need to install the following PowerShell modules on your station. (opens in a new tab)">Coding guidelines</a>, you will need to install the following PowerShell modules on your station.</p>



<ul class="wp-block-list"><li>The <strong>VMware PowerCLI</strong> modules<ul><li>See&nbsp;<a rel="noreferrer noopener" aria-label="The VMware PowerCLI module See&nbsp;Welcome PowerCLI to the PowerShell Gallery – Install Process Updates The Pester module (opens in a new tab)" href="https://blogs.vmware.com/PowerCLI/2017/04/powercli-install-process-powershell-gallery.html" target="_blank">Welcome PowerCLI to the PowerShell Gallery – Install Process Updates</a></li></ul></li><li>The <strong>Pester</strong> module<ul><li>If not already present on your station install (Install-Module), or update (Update-Module).</li></ul></li></ul>



<h3 class="wp-block-heading">A &#8220;work&#8221; copy</h3>



<p>

We are nearly there.

</p>



<p>To start coding, you will need to <strong>clone</strong> your copy of the repository (remember the <strong>fork</strong> earlier on) to your station.</p>



<p>With a <strong>Git GUI</strong> this is a simple process, provided you have set up the connection to your GitHub account beforehand.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="995" height="611" src="https://www.lucd.info/wp-content/uploads/2018/12/clone.jpg" alt="" class="wp-image-6158" srcset="https://www.lucd.info/wp-content/uploads/2018/12/clone.jpg 995w, https://www.lucd.info/wp-content/uploads/2018/12/clone-300x184.jpg 300w, https://www.lucd.info/wp-content/uploads/2018/12/clone-768x472.jpg 768w, https://www.lucd.info/wp-content/uploads/2018/12/clone-720x442.jpg 720w" sizes="auto, (max-width: 995px) 100vw, 995px" /></figure>



<ol class="wp-block-list"><li>Select the Clone option</li><li>Select your GitHub connection</li><li>Select the target Folder for the clone</li><li>Specify the name of the clone folder</li><li>Clone the repository</li></ol>



<div style="padding-top:50px;padding-bottom:50px;margin-top:50px;margin-bottom:50px" class="wp-block-editor-blocks-wrapper alignfull"><div class="wrapper-inner"><div class="wrapper-inner-blocks">
<p class="has-background has-medium-font-size has-very-light-gray-background-color">

Since we don&#8217;t want to mess with our copy of the master, it is good practice to create a&nbsp;<strong>branch</strong>, and do our development work on that branch.

</p>
</div></div></div>



<p>To create a branch, right click on the <strong>Master</strong> entry on the left side and then enter the name of your branch in the text box. In the example I create a branch named&nbsp;<strong>dev-lucd</strong>.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="396" height="192" src="https://www.lucd.info/wp-content/uploads/2018/12/branch-1.jpg" alt="" class="wp-image-6159" srcset="https://www.lucd.info/wp-content/uploads/2018/12/branch-1.jpg 396w, https://www.lucd.info/wp-content/uploads/2018/12/branch-1-300x145.jpg 300w" sizes="auto, (max-width: 396px) 100vw, 396px" /></figure>



<p>Once the branch is created, it becomes the active path. Meaning that when I open the local files, I&#8217;ll be working in the <strong>dev-lucd</strong> branch. And while I&#8217;m doing my coding, the <strong>master</strong> branch stays unchanged. That is one of the reasons we use repositories.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="373" height="192" src="https://www.lucd.info/wp-content/uploads/2018/12/branch-2.jpg" alt="" class="wp-image-6160" srcset="https://www.lucd.info/wp-content/uploads/2018/12/branch-2.jpg 373w, https://www.lucd.info/wp-content/uploads/2018/12/branch-2-300x154.jpg 300w" sizes="auto, (max-width: 373px) 100vw, 373px" /></figure>



<p>In summary, and to show how easy it is.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1044" height="848" src="https://www.lucd.info/wp-content/uploads/2018/12/CloneRepo.gif" alt="" class="wp-image-6177"/></figure>



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



<div style="padding-top:50px;padding-bottom:50px;margin-top:50px;margin-bottom:50px" class="wp-block-editor-blocks-wrapper alignfull"><div class="wrapper-inner"><div class="wrapper-inner-blocks">
<p class="has-background has-medium-font-size has-very-light-gray-background-color">The&nbsp;<a rel="noreferrer noopener" href="https://github.com/vmware/dscr-for-vmware/blob/master/CODING_GUIDELINES.md" target="_blank">Coding Guidelines</a>&nbsp;are required reading!</p>
</div></div></div>



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



<p>Now let&#8217;s get coding, but first some basic information for first time users, or as a refresher for the others.</p>



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



<p>One of the nice features of the Visual Studio Code editor, is that you can <strong>open a folder</strong>. This way all files in this folder will be shown in the <strong>Explorer</strong>.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="382" height="466" src="https://www.lucd.info/wp-content/uploads/2018/12/vsc-explorer.jpg" alt="" class="wp-image-6183" srcset="https://www.lucd.info/wp-content/uploads/2018/12/vsc-explorer.jpg 382w, https://www.lucd.info/wp-content/uploads/2018/12/vsc-explorer-246x300.jpg 246w" sizes="auto, (max-width: 382px) 100vw, 382px" /></figure>



<p>Selecting a file in the Explorer will open that file in an <strong>Editor window</strong>.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="910" height="467" src="https://www.lucd.info/wp-content/uploads/2018/12/vsc-editor.jpg" alt="" class="wp-image-6184" srcset="https://www.lucd.info/wp-content/uploads/2018/12/vsc-editor.jpg 910w, https://www.lucd.info/wp-content/uploads/2018/12/vsc-editor-300x154.jpg 300w, https://www.lucd.info/wp-content/uploads/2018/12/vsc-editor-768x394.jpg 768w, https://www.lucd.info/wp-content/uploads/2018/12/vsc-editor-720x369.jpg 720w" sizes="auto, (max-width: 910px) 100vw, 910px" /></figure>



<h4 class="wp-block-heading">Git Integration</h4>



<p>A nice and useful feature of the Visual Studio Code editor is that it offers builtin Git integration. From within the editor you have access to the most common the Git commands.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="634" height="880" src="https://www.lucd.info/wp-content/uploads/2018/12/vsc-git.jpg" alt="" class="wp-image-6241" srcset="https://www.lucd.info/wp-content/uploads/2018/12/vsc-git.jpg 634w, https://www.lucd.info/wp-content/uploads/2018/12/vsc-git-216x300.jpg 216w" sizes="auto, (max-width: 634px) 100vw, 634px" /></figure>



<div style="padding-top:50px;padding-bottom:50px;margin-top:50px;margin-bottom:50px;background-color:#eeeeee" class="wp-block-editor-blocks-wrapper alignfull"><div class="wrapper-inner"><div class="wrapper-inner-blocks">
<p class="has-background has-medium-font-size has-very-light-gray-background-color">Visual Studio Code has many keyboard shortcuts. After a while you will know the important ones by heart. To get you started, here are shortcuts to some handy cheat sheets.</p>



<p class="has-background has-medium-font-size has-very-light-gray-background-color"><a rel="noreferrer noopener" aria-label="Windows (opens in a new tab)" href="https://go.microsoft.com/fwlink/?linkid=832145" target="_blank">Windows</a></p>



<p class="has-background has-medium-font-size has-very-light-gray-background-color"><a rel="noreferrer noopener" aria-label="Windows Linux (opens in a new tab)" href="https://code.visualstudio.com/shortcuts/keyboard-shortcuts-linux.pdf" target="_blank">Linux</a></p>



<p class="has-background has-medium-font-size has-very-light-gray-background-color"><a rel="noreferrer noopener" aria-label="Windows Linux macOS (opens in a new tab)" href="https://code.visualstudio.com/shortcuts/keyboard-shortcuts-macos.pdf" target="_blank">macOS</a></p>
</div></div></div>



<h3 class="wp-block-heading">Resource Organisation</h3>



<p>Due to current limitations, all DSC resources are contained in the module&#8217;s single&nbsp;<strong>.psm1</strong> file, namely&nbsp;VMware.vSphereDSC.psm1. </p>



<p>The DSC resources that are exported, are specified in the module&#8217;s <strong>.psd1</strong> file (VMware.vSphereDSC.psd1) under the <strong>DscResourcesToExport</strong> entry.</p>



<p>There is a third file, named&nbsp;VMware.vSphereDSC.Helper.psm1, which contains a number of &#8220;helper&#8221; functions.</p>



<p>The DSC resources are defined as PowerShell <strong>classes</strong>. This allows the important feature of <strong>class inheritance</strong>.</p>



<p>A DSC resource, defined as a class, needs to have at least three methods <strong>Get</strong>, <strong>Set</strong> and <strong>Test</strong>. You can find more information in&nbsp;<a href="http://docs.microsoft.com/en-us/powershell/dsc/resources/authoringResourceClass" target="_blank" rel="noreferrer noopener" label="">Writing a custom DSC resource with PowerShell classes</a>.</p>



<h3 class="wp-block-heading">Our First DSC Resource</h3>



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



<p>As a proof of concept and to document the workflow, this post will document how we can add a DSC Resource for the Issues message and the MoTD message on ESXi nodes and on a VCSA.</p>



<p>The theory behind these two features, motd and issue, is quite straightforward, and there are ample blog posts around the subject. Have for example a look at Wiliam Lam&#8217;s&nbsp;<a rel="noreferrer noopener" aria-label="The theory behind these two features, motd and issue, is quite straightforward, and there are ample blog posts around the subject. Have for example a look at Wiliam's&nbsp;Easily manage ESXi &amp; VCSA SSH login banner &amp; MOTD in vSphere 6.0 post. (opens in a new tab)" href="https://www.virtuallyghetto.com/2015/02/easily-manage-esxi-vcsa-ssh-login-banner-motd-in-vsphere-6-0.html" target="_blank">Easily manage ESXi &amp; VCSA SSH login banner &amp; MOTD in vSphere 6.0 </a>post.</p>



<p>It all comes down to setting or clearing the two advanced settings on the target ESXi node ir VCSA.</p>



<pre class="wp-block-code"><code>#
# ESXi node
#

# Get
Get-AdvancedSetting -Entity $esx -Name 'Config.Etc.issue'
Get-AdvancedSetting -Entity $esx -Name 'Config.Etc.motd'

# Set
Get-AdvancedSetting -Entity $esx -Name 'Config.Etc.issue' |
Set-AdvancedSetting -Value 'Hello World!'
Get-AdvancedSetting -Entity $esx -Name 'Config.Etc.motd' |
Set-AdvancedSetting -Value 'Hello World!'

#
# VCSA
#

# Get
Get-AdvancedSetting -Entity $esx -Name 'etc.issue'
Get-AdvancedSetting -Entity $esx -Name 'etc.motd'

# Set
Get-AdvancedSetting -Entity $esx -Name 'etc.issue' |
Set-AdvancedSetting -Value 'Hello World!'
Get-AdvancedSetting -Entity $esx -Name 'etc.motd' |
Set-AdvancedSetting -Value 'Hello World!'
</code></pre>



<p>There is one caveat with these advanced settings, how do differentiate between clearing an entry and not passing a value in a parameter. One solution would be to make the property that defines the motd and issue, a mandatory property. But this brings other disadvantages, like obliging you to add this property to each configuration file.</p>



<p>I decided to go for a separate, also not mandatory, property that explicetely asks for clearing the content of the motd and issue setting. You will see how this was implemented later on in this post.</p>



<h4 class="wp-block-heading">Class setup</h4>



<p>From the <a href="https://github.com/vmware/dscr-for-vmware/blob/master/CODING_GUIDELINES.md" target="_blank" rel="noreferrer noopener" aria-label="From the Coding Guidelines we know how we have to set up our classes for the new resources. (opens in a new tab)">Coding Guidelines</a> we know how we have to set up our classes for the new resources.</p>



<p>The resource for the <strong>ESXi</strong> based resource inherits from <strong>VMHostBaseDSC</strong> and <strong>BaseDSC</strong>.&nbsp;</p>



<p>The resource for the <strong>VCSA</strong> based resource inherits from <strong>BaseDSC</strong>.</p>



<p>As an example, new DSC resources for ESXi and VCSA would start like this. Note that this just a skeleton, you will most probably have to add properties and methods.</p>



<pre class="wp-block-code"><code>class vCenterMyResource : BaseDSC
{}

class VMHostMyResource : VMHostBaseDSC
{}</code></pre>



<p>With the class inheritance, your class will have a number of inherited properties and methods. The following screenshot shows what you get from those two base classes.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="663" height="647" src="https://www.lucd.info/wp-content/uploads/2018/12/baseclass.jpg" alt="" class="wp-image-6198" srcset="https://www.lucd.info/wp-content/uploads/2018/12/baseclass.jpg 663w, https://www.lucd.info/wp-content/uploads/2018/12/baseclass-300x293.jpg 300w, https://www.lucd.info/wp-content/uploads/2018/12/baseclass-265x260.jpg 265w" sizes="auto, (max-width: 663px) 100vw, 663px" /></figure>



<h2 class="wp-block-heading">Two examples</h2>



<h3 class="wp-block-heading">Expand an Existing Resource</h3>



<p>The first release of the&nbsp;<a rel="noreferrer noopener" href="https://github.com/vmware/dscr-for-vmware" target="_blank">DSCR for VMware</a> module already contains a resource named <strong>vCenterSettings</strong>. Let&#8217;s try to add the <strong>motd</strong> and <strong>issue</strong> entries to this class. As a first step, I add the <strong>properties</strong> I will need to control the configuration of the <strong>motd</strong> and <strong>issue</strong> settings.</p>



<pre class="wp-block-code"><code>  &lt;#
    .DESCRIPTION

    Motd value.
    #>
  [DscProperty()]
  [string] $Motd

  &lt;#
    .DESCRIPTION

    Clear the Motd content
    #>
  [DscProperty()]
  [bool] $MotdClear

  &lt;#
    .DESCRIPTION

    Issue value.
    #>
  [DscProperty()]
  [string] $Issue
  &lt;#
    .DESCRIPTION

    Clear the Issue content
    #>
  [DscProperty()]
  [bool] $IssueClear


  hidden [string] $LogLevelSettingName = "log.level"
  hidden [string] $EventMaxAgeEnabledSettingName = "event.maxAgeEnabled"
  hidden [string] $EventMaxAgeSettingName = "event.maxAge"
  hidden [string] $TaskMaxAgeEnabledSettingName = "task.maxAgeEnabled"
  hidden [string] $TaskMaxAgeSettingName = "task.maxAge"
  hidden [string] $MotdSettingName = "etc.motd"
  hidden [string] $IssueSettingName = "etc.issue"</code></pre>



<p>This is only a partial extract of the vCenterSettings class properties. Also note how we added the paths to the advanced settings in two hidden properties (<strong>$MotdSettingName</strong> and <strong>$IssueSettingName</strong>).</p>



<p>Next we will have to update the existing methods already available in the class, in such a way that they can handle the two new advanced settings we are adding. We don not have to touch the three required methods <strong>Set</strong>, <strong>Test</strong> and <strong>Get</strong>, that every DSC resource class must have, since they are already written in a general way without any specific dependencies on the Advanced Settings.</p>



<p>One such method we have to adapt is the&nbsp;ShouldUpdatevCenterSettings method. The logic in this method is quite straightforward.</p>



<ul class="wp-block-list"><li>Get all the Advanced Settings</li><li>With the Name of the setting (see the hidden properties from earlier), the method retrieves current Value for the Advanced Setting</li><li>With the&nbsp;ShouldUpdateSettingValue method the requested value is compared with the actual value. This method returns a result Boolean with the outcome of that comparison</li><li>For the Motd and Issue settings, the MotdClear and IssueClear comes into play. As remarked earlier, this allows us to clear the content of advanced setting without the need to make it a Mandatory Property in the class</li><li>If any advanced setting needs to be changed, the method returns $true, otherwise it returns $false</li></ul>



<pre class="wp-block-code"><code>  [bool] ShouldUpdatevCenterSettings($vCenter) {
    $vCenterCurrentAdvancedSettings = Get-AdvancedSetting -Server $this.Connection -Entity $vCenter

    $currentLogLevel = $vCenterCurrentAdvancedSettings | Where-Object { $_.Name -eq $this.LogLevelSettingName }
    $currentEventMaxAgeEnabledValue = $vCenterCurrentAdvancedSettings | Where-Object { $_.Name -eq $this.EventMaxAgeEnabledSettingName }
    $currentEventMaxAgeValue = $vCenterCurrentAdvancedSettings | Where-Object { $_.Name -eq $this.EventMaxAgeSettingName }
    $currentTaskMaxAgeEnabledValue = $vCenterCurrentAdvancedSettings | Where-Object { $_.Name -eq $this.TaskMaxAgeEnabledSettingName }
    $currentTaskMaxAgeValue = $vCenterCurrentAdvancedSettings | Where-Object { $_.Name -eq $this.TaskMaxAgeSettingName }
    $currentMotdValue = $vCenterCurrentAdvancedSettings | Where-Object { $_.Name -eq $this.MotdSettingName }
    $currentIssueValue = $vCenterCurrentAdvancedSettings | Where-Object { $_.Name -eq $this.IssueSettingName }

    $shouldUpdate = @()
    $shouldUpdate += $this.ShouldUpdateSettingValue($this.LoggingLevel, $currentLogLevel.Value)
    $shouldUpdate += $this.ShouldUpdateSettingValue($this.EventMaxAgeEnabled, $currentEventMaxAgeEnabled.Value)
    $shouldUpdate += $this.ShouldUpdateSettingValue($this.EventMaxAge, $currentEventMaxAge.Value)
    $shouldUpdate += $this.ShouldUpdateSettingValue($this.TaskMaxAgeEnabled, $currentTaskMaxAgeEnabled.Value)
    $shouldUpdate += $this.ShouldUpdateSettingValue($this.TaskMaxAge, $currentTaskMaxAge.Value)
    $shouldUpdate += ($this.MotdClear -and ($currentMotd.Value -ne '')) -or (-not $this.MotdClear -and ($this.Motd -ne $currentMotd.Value))
    $shouldUpdate += $this.ShouldUpdateSettingValue($this.Issue, $currentIssueValue)
    $shouldUpdate += ($this.IssueClear -and ($currentIssue.Value -ne '')) -or (-not $this.MotdClear -and ($this.Motd -ne $currentIssue.Value))

    return ($shouldUpdate -contains $true)
  }</code></pre>



<p>The methods that actually change the Value of an Advanced Setting, take care of the MotdClear and IssueClear properties. In fact we use that fact that we can define separate methods depending on the number and type of parameters we pass to a method. So there is a separate method SetAdvancedSetting when we call the method with four parameters.</p>



<pre class="wp-block-code"><code>[void] SetAdvancedSetting($advancedSetting, $advancedSettingDesiredValue, $advancedSettingCurrentValue) {
    if ($this.ShouldUpdateSettingValue($advancedSettingDesiredValue, $advancedSettingCurrentValue)) {
      Set-AdvancedSetting -AdvancedSetting $advancedSetting -Value $advancedSettingDesiredValue -Confirm:$false
    }
  }

  [void] SetAdvancedSetting($advancedSetting, $advancedSettingDesiredValue, $advancedSettingCurrentValue, $clearValue) {
    Write-Verbose -Message "$(Get-Date) $($s = Get-PSCallStack; "Entering {0}" -f $s[0].FunctionName)"

    if ($clearValue) {
      if ($this.ShouldUpdateSettingValue('', $advancedSettingCurrentValue)) {
        Set-AdvancedSetting -AdvancedSetting $advancedSetting -Value '' -Confirm:$false
      }
    }
    else {
      if ($this.ShouldUpdateSettingValue($advancedSettingDesiredValue, $advancedSettingCurrentValue)) {
        Set-AdvancedSetting -AdvancedSetting $advancedSetting -Value $advancedSettingDesiredValue -Confirm:$false
      }
    }
  }

[void] UpdatevCenterSettings($vCenter) {
    $vCenterCurrentAdvancedSettings = Get-AdvancedSetting -Server $this.Connection -Entity $vCenter

    $currentLogLevel = $vCenterCurrentAdvancedSettings | Where-Object { $_.Name -eq $this.LogLevelSettingName }
    $currentEventMaxAgeEnabled = $vCenterCurrentAdvancedSettings | Where-Object { $_.Name -eq $this.EventMaxAgeEnabledSettingName }
    $currentEventMaxAge = $vCenterCurrentAdvancedSettings | Where-Object { $_.Name -eq $this.EventMaxAgeSettingName }
    $currentTaskMaxAgeEnabled = $vCenterCurrentAdvancedSettings | Where-Object { $_.Name -eq $this.TaskMaxAgeEnabledSettingName }
    $currentMotd = $vCenterCurrentAdvancedSettings | Where-Object { $_.Name -eq $this.MotdSettingName }
    $currentIssue = $vCenterCurrentAdvancedSettings | Where-Object { $_.Name -eq $this.IssueSettingName }

    $currentTaskMaxAge = $vCenterCurrentAdvancedSettings | Where-Object { $_.Name -eq $this.TaskMaxAgeSettingName }
    $this.SetAdvancedSetting($currentLogLevel, $this.LoggingLevel, $currentLogLevel.Value)
    $this.SetAdvancedSetting($currentEventMaxAgeEnabled, $this.EventMaxAgeEnabled, $currentEventMaxAgeEnabled.Value)
    $this.SetAdvancedSetting($currentEventMaxAge, $this.EventMaxAge, $currentEventMaxAge.Value)
    $this.SetAdvancedSetting($currentTaskMaxAgeEnabled, $this.TaskMaxAgeEnabled, $currentTaskMaxAgeEnabled.Value)
    $this.SetAdvancedSetting($currentTaskMaxAge, $this.TaskMaxAge, $currentTaskMaxAge.Value)
    $this.SetAdvancedSetting($currentMotd, $this.Motd, $currentMotd.Value,$this.MotdClear)
    $this.SetAdvancedSetting($currentIssue, $this.Issue, $currentIssue.Value,$this.IssueClear)
  }</code></pre>



<p>There are a number of other changes I had to make in the vCenterSettings class, but I leave it to the reader to further inspect those once my changes are merged in the repository.</p>



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



<p>There is currently no resource yet for Advanced Settings for ESXi nodes. In this new class I will try to create such a resource, and for starters only provide the <strong>motd</strong> and <strong>issue</strong> settings. It should be rather trivial to add other advanced settings later on.</p>



<pre class="wp-block-code"><code>[DscResource()]
class VMHostSettings : VMHostBaseDSC {
  &lt;#
    .DESCRIPTION

    Motd value.
    #>
  [DscProperty()]
  [string] $Motd

  &lt;#
    .DESCRIPTION

    Clear the Motd content
    #>
  [DscProperty()]
  [bool] $MotdClear

  &lt;#
    .DESCRIPTION

    Issue value.
    #>
  [DscProperty()]
  [string] $Issue

  &lt;#
    .DESCRIPTION

    Clear the Issue content
    #>
  [DscProperty()]
  [bool] $IssueClear


  hidden [string] $IssueSettingName = "Config.Etc.issue"
  hidden [string] $MotdSettingName = "Config.Etc.motd"</code></pre>



<p>Note how use two hidden properties (<strong>$IssueSettingName</strong> and <strong>$MotdSettingName</strong>) in the class to specify the <strong>path</strong> to both advanced settings.</p>



<p>The basic layout of the methods we need in the class are more or less copies of the methods we also found in the vCenterSettings class. The major difference is that I left out the&nbsp;ShouldUpdateSettingValue method, since it was not required for the motd and issue settings. The decision if a change is required is already made in the&nbsp;ShouldUpdateVMHostSettings method.</p>



<pre class="wp-block-code"><code>[bool] ShouldUpdateVMHostSettings($VMHost) {
    Write-Verbose -Message "$(Get-Date) $($s = Get-PSCallStack; "Entering {0}" -f $s[0].FunctionName)"

    $VMHostCurrentAdvancedSettings = Get-AdvancedSetting -Server $this.Connection -Entity $VMHost

    $currentMotd = $VMHostCurrentAdvancedSettings | Where-Object { $_.Name -eq $this.MotdSettingName }
    $currentIssue = $VMHostCurrentAdvancedSettings | Where-Object { $_.Name -eq $this.IssueSettingName }

    $shouldUpdate = @()
    $shouldUpdate += ($this.MotdClear -and ($currentMotd.Value -ne '')) -or (-not $this.MotdClear -and ($this.Motd -ne $currentMotd.Value))
    $shouldUpdate += ($this.IssueClear -and ($currentIssue.Value -ne '')) -or (-not $this.IssueClear -and ($this.Issue -ne $currentIssue.Value))

    return ($shouldUpdate -contains $true)
  }
</code></pre>



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



<p>So you have coded your new resource, or update an existing, and you want to try it out.</p>



<p>As a first step you would run your Pester Unit tests, and fix all the issues you encounter in there. There is a section later on on these Pester tests.</p>



<p>The next step would of course be that you are going to try and use your resource on a test environment. But, just like always happens to me, you might notice some errors and/or issues when you run your tests.</p>



<p>As I imagine that using and writing DSC resources might be new territory for some of you, I added this Debugging section. It is definitely not final, but just lists some of the handy tricks I discovered/learned while developing DSC resources.</p>



<h3 class="wp-block-heading">How do you test these?</h3>



<p>First, make sure that you have read&nbsp;<a rel="noreferrer noopener" aria-label="First, make sure that you have read&nbsp;Getting Started with Desired State Configuration Resources for VMware.&nbsp; (opens in a new tab)" href="https://blogs.vmware.com/PowerCLI/2018/12/getting-started-dsc-for-vmware.html" target="_blank">Getting Started with Desired State Configuration Resources for VMware</a>. The only comment I can make on that post is that for &#8220;Kyle and the beard&#8221; everything always works <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f642.png" alt="🙂" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p>For us mere mortals some handy tips.</p>



<ul class="wp-block-list"><li>Configure the LCM agent on the station you will test on to ignore configuration signing and to work in debug mode (that will force the LCM to reload the DSC module each time, which is handy when you are making changes to the module)</li></ul>



<pre class="wp-block-code"><code>[DSCLocalConfigurationManager()]
configuration LCMConfig
{
    Node localhost
    {
        Settings
        {
            RefreshMode = 'Push'
            ConfigurationMode = 'ApplyAndMonitor'
            SignatureValidations  = @{}
            DebugMode = 'ForceModuleImport'
        }
    }
}

LCMconfig
Set-DscLocalConfigurationManager -Path '.\LCMconfig' -ComputerName localhost</code></pre>



<ul class="wp-block-list"><li>Use the <strong>Verbose</strong> switch when applying a configuration with <strong>Start-DscConfiguration</strong>. It provides you with a lot of information on what your DSC module is doing.</li></ul>



<pre class="wp-block-code"><code>$motdConfigInput = @{
    name = 'esx.local.lab'
    server = 'vcsa.local.lab'
    user = 'administrator@vsphere.local'
    password = 'VMware1!'
    motd = 'Hello Stranger!'
    issue = 'Enter on your own risk!'
}

. .\VMHostSettings_Config.ps1 @motdConfigInput

$sConfig = @{
    ComputerName = 'localhost'
    Path = '.\VMHostSettings_Config'
    Verbose = $true
    Wait = $true
    Force = $true
}
Start-DscConfiguration @sConfig</code></pre>



<figure class="wp-block-image is-resized"><a href="https://www.lucd.info/wp-content/uploads/2018/12/verbose2.jpg" target="_blank" rel="noreferrer noopener"><img loading="lazy" decoding="async" src="https://www.lucd.info/wp-content/uploads/2018/12/verbose2.jpg" alt="" class="wp-image-6233" width="593" height="304" srcset="https://www.lucd.info/wp-content/uploads/2018/12/verbose2.jpg 1185w, https://www.lucd.info/wp-content/uploads/2018/12/verbose2-300x154.jpg 300w, https://www.lucd.info/wp-content/uploads/2018/12/verbose2-768x393.jpg 768w, https://www.lucd.info/wp-content/uploads/2018/12/verbose2-1024x525.jpg 1024w, https://www.lucd.info/wp-content/uploads/2018/12/verbose2-720x369.jpg 720w" sizes="auto, (max-width: 593px) 100vw, 593px" /></a></figure>



<ul class="wp-block-list"><li>Write your own verbose messages in your DSC resource classes. With a simple Write-Verbose in your code, you can provide debugging information. I for example, add a message at the start of each method that shows the code has entered that method. You will find some examples in the verbose output above</li></ul>



<pre class="wp-block-code"><code>Write-Verbose -Message "$(Get-Date) $($s = Get-PSCallStack; "Entering {0}" -f $s[0].FunctionName)"</code></pre>



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



<p>The ultimate goal of your DSC resource is of course to use it to configure and monitor parts of your vSphere environment.</p>



<p>As was shown, perhaps implicitly, in&nbsp;<a href="https://blogs.vmware.com/PowerCLI/2018/12/getting-started-dsc-for-vmware.html">Getting Started with Desired State Configuration Resources for VMware</a> the commonly accepted way to apply DSC configurations, is to separate <strong>configuration</strong> from <strong>environment</strong> data. See also&nbsp;<a rel="noreferrer noopener" label="" href="http://docs.microsoft.com/en-us/powershell/dsc/configurations/separatingEnvData" target="_blank">Separating configuration and environment data</a>.</p>



<p>In practice this comes down to having code, that is static, unless you add properties to your DSC resource class, and code that contains the actual configuration data. These two types of data can be combined in one .ps1 file, or they can be kept in two separate .ps1 files.</p>



<p>For our newly created DSC resource, we would have a configuration file, something like this. Note how this file works with parameters, and that nothing would need to be changed to this file, while we change the configuration itself, the so-called environmental data.</p>



<pre class="wp-block-code"><code>param(
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter(Mandatory = $true)]
        [string]
        $Server,

        [Parameter(Mandatory = $true)]
        [string]
        $User,

        [Parameter(Mandatory = $true)]
        [string]
        $Password,

        [string]
        $Motd,

        [string]
        $Issue,

        [Boolean]$MotdClear = $false,

        [Boolean]$IssueClear = $false
)

$script:configurationData = @{
    AllNodes = @(
        @{
            NodeName = 'localhost'
            PSDscAllowPlainTextPassword = $true
        }
    )
}

Configuration VMHostSettings_Config
{
    Import-DscResource -ModuleName VMware.vSphereDSC

    Node localhost
    {
        $Password = $Password | ConvertTo-SecureString -AsPlainText -Force
        $Credential = New-Object System.Management.Automation.PSCredential($User, $Password)

        VMHostSettings vmHostSettings
        {
            Name = $Name
            Server = $Server
            Credential = $Credential
            Motd = $motd
            MotdClear = $MotdClear
            Issue = $Issue
            IssueClear = $IssueClear
        }
    }
}

VMHostSettings_Config -ConfigurationData $script:configurationData</code></pre>



<p>The environmental data is kept in a separate file. Something like this for example.</p>



<pre class="wp-block-code"><code>$motdConfigInput = @{
    name = 'esx1.local.lab'
    server = 'vcsa.local.lab'
    user = 'administrator@vsphere.local'
    password = 'VMware1!'
    motd = 'Hello Stranger!'
    issue = 'Welcome to my world!'
}

. .\VMHostSettings_Config.ps1 @motdConfigInput

$sConfig = @{
    ComputerName = 'localhost'
    Path = '.\VMHostSettings_Config'
    Verbose = $true
    Wait = $true
    Force = $true
}
Start-DscConfiguration @sConfig</code></pre>



<p>As you notice, we call the other file (VMHostSettings_Config.ps1).&nbsp;This script will generate the MOF file, and will apply the configuration with Start-DscConfiguration. Notice how I added the Verbose switch to have a better view on what is happening. Once your DSC resource is sufficiently tested and considered stable, you might remove the Verbose switch.</p>



<p>Since the proof is in the pudding, this is what a SSH session looks like after the configuration was applied.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="658" height="160" src="https://www.lucd.info/wp-content/uploads/2018/12/esx-ssh.jpg" alt="" class="wp-image-6237" srcset="https://www.lucd.info/wp-content/uploads/2018/12/esx-ssh.jpg 658w, https://www.lucd.info/wp-content/uploads/2018/12/esx-ssh-300x73.jpg 300w" sizes="auto, (max-width: 658px) 100vw, 658px" /></figure>



<p>For the vCenterSettings resource we can apply our configuration in the same way. First our configuration part.</p>



<p>Note that I left out the parameters for the settings that were already available in this vCenterSettings resource.</p>



<pre class="wp-block-code"><code>param(
        [Parameter(Mandatory = $true)]
        [string]
        $Server,

        [Parameter(Mandatory = $true)]
        [string]
        $User,

        [Parameter(Mandatory = $true)]
        [string]
        $Password,

        [string]
        $Motd,

        [string]
        $Issue,

        [Boolean]$MotdClear = $false,

        [Boolean]$IssueClear = $false
)

$script:configurationData = @{
    AllNodes = @(
        @{
            NodeName = 'localhost'
            PSDscAllowPlainTextPassword = $true
        }
    )
}

Configuration vCenterSettings_Config
{
    Import-DscResource -ModuleName VMware.vSphereDSC

    Node localhost
    {
        $Password = $Password | ConvertTo-SecureString -AsPlainText -Force
        $Credential = New-Object System.Management.Automation.PSCredential($User, $Password)

        vCenterSettings vCenterSettings
        {
            Server = $Server
            Credential = $Credential
            Motd = $motd
            MotdClear = $MotdClear
            Issue = $Issue
            IssueClear = $IssueClear
        }
    }
}

vCenterSettings_Config -ConfigurationData $script:configurationData
</code></pre>



<p>And then the environmental part.</p>



<pre class="wp-block-code"><code>$motdConfigInput = @{
    server = 'vcsa.local.lab'
    user = 'administrator@vsphere.local'
    password = 'VMware1!'
    motd = 'Hello VCSA Stranger!'
    issue = 'Welcome to my VCSA world!'
}

. .\vCenterSettings_Config.ps1 @motdConfigInput

$sConfig = @{
    ComputerName = 'localhost'
    Path = '.\vCenterSettings_Config'
    Verbose = $true
    Wait = $true
    Force = $true
}
Start-DscConfiguration @sConfig</code></pre>



<p>And the result after applying the configuration.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="658" height="183" src="https://www.lucd.info/wp-content/uploads/2018/12/vcsa-ssh.jpg" alt="" class="wp-image-6238" srcset="https://www.lucd.info/wp-content/uploads/2018/12/vcsa-ssh.jpg 658w, https://www.lucd.info/wp-content/uploads/2018/12/vcsa-ssh-300x83.jpg 300w" sizes="auto, (max-width: 658px) 100vw, 658px" /></figure>



<h2 class="wp-block-heading">Pester Tests</h2>



<h3 class="wp-block-heading">Unit Testing</h3>



<p>As specified in the&nbsp;<a rel="noreferrer noopener" aria-label="As specified in the&nbsp;Coding guidelines, you have to provide (opens in a new tab)" href="https://github.com/vmware/dscr-for-vmware/blob/master/CODING_GUIDELINES.md" target="_blank">Coding Guidelines</a>, you have to provide Pester Unit test scripts for the resources you add to the module. Unit tests test how the code flows through your methods.</p>



<p>From my own experience, all this Pester code looks daunting and complex at first, but at some point you &#8216;<em>make the click</em>&#8216;, and from then on it all looks obvious and not too hard at all.</p>



<p>Let&#8217;s take a small extract from the Pester Unit tests I wrote for the <strong>VMHostSettings</strong> class. There is some structure and logic in such tests.</p>



<pre class="wp-block-code"><code>  Describe 'VMHostSettings\Set' {
    AfterEach {
      $script:resourceProperties.Motd = [string]::Empty
      $script:resourceProperties.Issue = [string]::Empty
    }

    Context 'Invoking with empty settings' {
      BeforeAll {
        # Arrange
        $viServer = [VMware.Vim.VIServer] @{ Name = '10.23.82.112'; User = 'user' }
        $vmhost = [VMware.Vim.VMHost] @{ Id = 'VMHostId' }

        $viServerMock = {
          return [VMware.Vim.VIServer] @{ Name = '10.23.82.112'; User = 'user' }
        }
        $vmHostMock = {
          return [VMware.Vim.VMHost] @{ Id = 'VMHostId' }
        }

        Mock -CommandName Connect-VIServer -MockWith $viServerMock -ModuleName $script:moduleName
        Mock -CommandName Get-VMHost -MockWith $vmHostMock -ModuleName $script:moduleName
        Mock -CommandName Get-AdvancedSetting -MockWith { return $null } -ModuleName $script:moduleName
      }

      # Arrange
      $resource = New-Object -TypeName $script:resourceName -Property $script:resourceProperties

      It 'Should call the Connect-VIServer mock with the passed server and credentials once' {
        # Act
        $resource.Set()

        # Assert
        Assert-MockCalled -CommandName Connect-VIServer <code>
          -ParameterFilter { $Server -eq $script:resourceProperties.Server -and $Credential -eq $script:resourceProperties.Credential } </code>
          -ModuleName $script:moduleName -Exactly 1 -Scope It
      }
   }
}</code></pre>



<ul class="wp-block-list"><li>Your Unit test has to cover the Set, Test and Get functionality of your resource. Each of these is covered in one <strong>Describe</strong> block.</li><li>As not to suffer from relics from test 1 while doing test 2, you can reset your environment. This is defined in a <strong>AfterEach</strong> block</li><li>We group our tests in <strong>Context </strong>blocks. Such a Context block generally consists of a number of tests that start from the same situation. That could for example be, all tests that run with no parameters passed. The text that you specify on such a Context block, will appear on the Pester output.</li><li>Since you don&#8217;t want to run these Unit tests against a live environment, you are going to <strong>mock</strong> the cmdlets used in your methods.</li><li>In a mock definition you specify which cmdlet you are mocking and what this mock shall return to your tests. Most of the time this is minimal information, and just enough that the methods you are testing can proceed.</li><li>These mock definitions are provided in a <strong>BeforeAll</strong> block, meaning that these mock definitions are done before any actual tests.</li><li>Since we are testing a class, we have to create a minimal representation of such a class for the tests. This is done with the <strong>New-Object</strong> cmdlet, and the resulting object is stored in the variable <strong>$resource</strong>.</li><li>Now we can define the actual tests. These are defined in <strong>It</strong> blocks. Add a meaningful description of the test, this is the text you will see appearing, hopefully in green, when the Unit test are run.</li><li>Since we are in the Set testing part, remember the Describe from earlier on, we have to call the Set method on the class with <strong>$resource.Set()</strong></li><li>A series of tests will verify that all cmdlets that you expect to be called are called. This is defined with the <strong>Assert-MockCalled</strong>. There are multiple options on that command, including how many times you expect the (mocked) cmdlet to be called.</li></ul>



<p>The&nbsp;<a rel="noreferrer noopener" href="https://github.com/vmware/dscr-for-vmware" target="_blank">DSCR for VMware</a>&nbsp;provides a script to run all the Unit tests in one run, but sometimes you might have the need to run a specific Unit test. That can be done by just running that single test.</p>



<p>Go to Tests folder, and start the test.</p>



<pre class="wp-block-code"><code>Invoke-Pester -Path .\Unit\VMHostSettings.Unit.Tests.ps1</code></pre>



<p>If all goes well, you should see something like this.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="734" height="620" src="https://www.lucd.info/wp-content/uploads/2018/12/unit-test2.jpg" alt="" class="wp-image-6222" srcset="https://www.lucd.info/wp-content/uploads/2018/12/unit-test2.jpg 734w, https://www.lucd.info/wp-content/uploads/2018/12/unit-test2-300x253.jpg 300w, https://www.lucd.info/wp-content/uploads/2018/12/unit-test2-720x608.jpg 720w" sizes="auto, (max-width: 734px) 100vw, 734px" /></figure>



<h3 class="wp-block-heading">Integration Testing</h3>



<p>Integration tests verify that your code actually does what it is supposed to do, by running it against an actual environment. There is no mocking involved, when your code is supposed to create a VM, the Integration test will verify that the VM is actually created.</p>



<p>All the Pester testing will eventually be incorporated in CI/CD pipeline. In that pipeline you will specify how the test environment for your Integration test can be reached. That is why you will notice that driving scripts for this testing requires (mandatory) some parameters like:</p>



<ul class="wp-block-list"><li><strong>Server: </strong>which vSphere Server</li><li><strong>Name: </strong>only for tests that run against a VMHost resource</li><li><strong>User/Password</strong>: the credentials to connect to Server</li></ul>



<p>For an Integration test you will need at least two files:</p>



<ul class="wp-block-list"><li><strong>Pester script</strong>: this contains the tests</li><li><strong>Configuration file(s)</strong>: this contains the DSC configurations you are testing with</li></ul>



<h4 class="wp-block-heading">Pester script</h4>



<p>This Pester script contains the actual Integration tests that will be executed. The filename is <strong>&lt;DSC-resource-name>.Integration.Tests.ps1</strong>.</p>



<p>In that file, under the <strong>Describe</strong> block, you have a number of<strong> Context</strong> blocks. Each Context block describes tests for one or more of the properties available in the DSC resource.</p>



<p>In the Context blocks, you have a number of <strong>It</strong> blocks. In these It blocks you perform that actual tests with the DSC resource.</p>



<p>A minimal set of It blocks for a Context are:</p>



<ul class="wp-block-list"><li>Does the Configuration create a MOF and is it applied without any errors?</li><li>Does applying the Configuration have the expected result?</li><li>Does a Test with the Configuration show compliance (is the result $true)?</li></ul>



<p>A sample Describe block looks like this</p>



<pre class="wp-block-code"><code>Describe "$($script:dscResourceName)_Integration" {
  Context "When using configuration $($script:config)" {
    BeforeAll {
      BeforeAllTests
    }

    AfterAll {
      AfterAllTests
    }

    BeforeEach {
      # Arrange
      $startDscConfigurationParameters = @{
        Path         = $script:mofFilePath
        ComputerName = 'localhost'
        Wait         = $true
        Force        = $true
      }

      # Act
      $script:dscConfig = Start-DscConfiguration @startDscConfigurationParameters
    }

    It 'Should compile and apply the MOF without throwing' {
      # Assert
      { $script:dscConfig } | Should -Not -Throw
    }

    It 'Should be able to call Get-DscConfiguration without throwing and all the parameters should match' {
      # Arrange &amp;&amp; Act
      $script:dscConfig = Get-DscConfiguration <code>
        | Where-Object {$_.configurationName -eq $script:config }

      $configuration = $script:dscConfig </code>
        | Select-Object -Last 1

      # Assert
      { $script:dscConfig } | Should -Not -Throw

      $configuration.Name | Should -Be $script:resourceProperties.Name
      $configuration.Server | Should -Be $script:resourceProperties.Server
      $configuration.Motd | Should -Be $script:motd
      $configuration.Issue | Should -Be $script:issue
    }

    It 'Should return $true when Test-DscConfiguration is run' {
      # Arrange &amp;&amp; Act &amp;&amp; Assert
      Test-DscConfiguration | Should -Be $true
    }
  }
}</code></pre>



<h4 class="wp-block-heading">Configuration file</h4>



<p>This Configuration file contains one or more DSC Configuration definitions that you are testing with the Pester script.</p>



<p>The filename follows this layout, <strong>&lt;DSC-resource-name>_Config.ps1</strong>.</p>



<p>In the file there are one or more Configurations that allow you you test all aspects of your DSC resource. In the following example we test the <strong>motd</strong> and <strong>issue</strong> properties of the <strong>VMHostSettings</strong> resource.</p>



<pre class="wp-block-code"><code>$Password = $Password | ConvertTo-SecureString -AsPlainText -Force
$script:vmHostCredential = New-Object System.Management.Automation.PSCredential($User, $Password)

$script:motd = 'VMHostSettings motd test'
$script:issue = 'VMHostSettings issue test'

$script:configurationData = @{
  AllNodes = @(
    @{
      NodeName                    = 'localhost'
      PSDscAllowPlainTextPassword = $true
    }
  )
}

$moduleFolderPath = (Get-Module VMware.vSphereDSC -ListAvailable).ModuleBase
$integrationTestsFolderPath = Join-Path (Join-Path $moduleFolderPath 'Tests') 'Integration'

Configuration VMHostSettings_Config
{
  Import-DscResource -ModuleName VMware.vSphereDSC

  Node localhost
  {
    VMHostSettings vmHostSettings {
      Name       = $Name
      Server     = $Server
      Credential = $script:vmHostCredential
      Motd       = $script:motd
      Issue      = $script:issue
    }
  }
}

VMHostSettings_Config -OutputPath "$integrationTestsFolderPath\VMHostSettings_Config" -ConfigurationData $script:configurationData</code></pre>



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



<p>Once you have your new DSC resources ready, tested and Pester tested, you are ready to submit to the repository.</p>



<p>First, stage all the files that you changed on your station, to your repository. Make sure to clearly document what the changes are all about.</p>



<p>One requirement is that all your commits shall be signed. First you will need to set up global settings for your account and email.</p>



<pre class="wp-block-code"><code>git config --global user.name "Luc Dekens"
git config --global user.email "dekens.luc@gmail.com"</code></pre>



<p>And verify the settings with</p>



<pre class="wp-block-code"><code>git config --list --show-origin</code></pre>



<p>For each commit you make to your repository, you can now &#8220;sign&#8221; it with</p>



<pre class="wp-block-code"><code>git commit --amend --signoff</code></pre>



<p>This will open an editor window with the commit content, including the signoff.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="814" height="477" src="https://www.lucd.info/wp-content/uploads/2018/12/commit-sign.jpg" alt="" class="wp-image-6250" srcset="https://www.lucd.info/wp-content/uploads/2018/12/commit-sign.jpg 814w, https://www.lucd.info/wp-content/uploads/2018/12/commit-sign-300x176.jpg 300w, https://www.lucd.info/wp-content/uploads/2018/12/commit-sign-768x450.jpg 768w, https://www.lucd.info/wp-content/uploads/2018/12/commit-sign-720x422.jpg 720w" sizes="auto, (max-width: 814px) 100vw, 814px" /></figure>



<p>Once your repository has the changes and you made sure all your commits are signed, you are ready to create your <strong>Pull Request</strong> (PR) against the <a href="https://github.com/vmware">vmware</a>/<strong><a href="https://github.com/vmware/dscr-for-vmware">dscr-for-vmware</a></strong> repository. Make sure to follow the instructions in the&nbsp;<a href="https://github.com/vmware/dscr-for-vmware/blob/master/CONTRIBUTING.md">CONTRIBUTING</a> document.</p>



<p>Some important points:</p>



<ul class="wp-block-list"><li>the <strong>base</strong> is the VMware repository</li><li>open the PR against the <strong>dev</strong> branch</li><li>make sure to select the branch with your latest changes on your side (here <strong>dev-lucd</strong>)</li></ul>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="877" height="752" src="https://www.lucd.info/wp-content/uploads/2018/12/pr.jpg" alt="" class="wp-image-6244" srcset="https://www.lucd.info/wp-content/uploads/2018/12/pr.jpg 877w, https://www.lucd.info/wp-content/uploads/2018/12/pr-300x257.jpg 300w, https://www.lucd.info/wp-content/uploads/2018/12/pr-768x659.jpg 768w, https://www.lucd.info/wp-content/uploads/2018/12/pr-720x617.jpg 720w" sizes="auto, (max-width: 877px) 100vw, 877px" /></figure>



<p>Once your PR is submitted, it will be <strong>reviewed</strong> by the PowerCLI Team. When they have questions, or want you make changes, you can follow that up in the comments of your PR.</p>



<p>When the review is completed, the reviewer will approve the <strong>merge</strong> of your code. And you will see your code being merged in the <strong>dev</strong> branch.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="699" height="95" src="https://www.lucd.info/wp-content/uploads/2018/12/merge.jpg" alt="" class="wp-image-6253" srcset="https://www.lucd.info/wp-content/uploads/2018/12/merge.jpg 699w, https://www.lucd.info/wp-content/uploads/2018/12/merge-300x41.jpg 300w" sizes="auto, (max-width: 699px) 100vw, 699px" /></figure>



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



<p>I intend this to be a living post, in other words any new information or any new experiences I gather, I will add them here.<br>And of course, if you have questions or remarks, feel free use the Comments or my Contact Form.</p>



<p>Enjoy!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.lucd.info/2018/12/16/dscr-for-vmware-and-you/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>Deploy Photon 2.0 &#8211; Part 1</title>
		<link>https://www.lucd.info/2018/08/14/deploy-photon-2-0-part-1/</link>
					<comments>https://www.lucd.info/2018/08/14/deploy-photon-2-0-part-1/#comments</comments>
		
		<dc:creator><![CDATA[LucD]]></dc:creator>
		<pubDate>Tue, 14 Aug 2018 10:51:10 +0000</pubDate>
				<category><![CDATA[Guest Operations]]></category>
		<category><![CDATA[Guest OS]]></category>
		<category><![CDATA[Import-VApp]]></category>
		<category><![CDATA[Invoke-VMScriptPlus]]></category>
		<category><![CDATA[Photon]]></category>
		<category><![CDATA[PowerCLI]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[VMware Tools]]></category>
		<category><![CDATA[GuestOperations]]></category>
		<guid isPermaLink="false">http://www.lucd.info/?p=5851</guid>

					<description><![CDATA[Photon 2.0 is definitely a guest OS that is useful in a VMware [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p><a href="https://vmware.github.io/photon/" target="_blank" rel="noopener noreferrer">Photon 2.0</a> is definitely a guest OS that is useful in a VMware vSphere environment. It is light-weight, easy to manage, security hardened and comes with the Docker daemon.&nbsp;<br>That last feature makes it an ideal environment to explore new applications, isolated from your live platforms. As an example, quite a few of us got to know <a href="https://github.com/PowerShell/PowerShell" target="_blank" rel="noopener noreferrer">PowerShell Core</a> with <a href="https://code.vmware.com/web/dp/tool/vmware-powercli/10.1.1" target="_blank" rel="noopener noreferrer">VMware PowerCLI</a>, while running it in a <a href="https://hub.docker.com/r/vmware/powerclicore/" target="_blank" rel="noopener noreferrer">Docker container</a>, well shielded from our other platforms.<br><br></p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="435" height="359" src="https://www.lucd.info/wp-content/uploads/2018/08/sddc-1.jpg" alt="" class="wp-image-5914" srcset="https://www.lucd.info/wp-content/uploads/2018/08/sddc-1.jpg 435w, https://www.lucd.info/wp-content/uploads/2018/08/sddc-1-300x248.jpg 300w" sizes="auto, (max-width: 435px) 100vw, 435px" /></figure>



<p><p>There are numerous articles and blog posts that describe how set up a Photon 2.0 VM, but most of these use the <a href="https://www.vmware.com/support/developer/ovf/" target="_blank" rel="noreferrer noopener">ovftool</a> or the vSphere Web Client to install it. Followed by quite of a bit of editing config files, to have your Photon 2.0 VM running the way you prefer it.<br>With the latest version of my <a href="https://www.lucd.info/2018/08/05/invoke-vmscriptplus-v2/" target="_blank" rel="noreferrer noopener">Invoke-VMScriptPlus</a> function, you can now automate this entire process, the <a href="https://code.vmware.com/sddc-getting-started" target="_blank" rel="noreferrer noopener">SDDC</a> way!&nbsp;</p>
<p>&nbsp;</p></p>


<p><span style="background-color: #ffff00;"><strong>Update August 21st 2018</strong></span></p>
<ul>
<li>Added &#8220;reboot&#8221; action</li>
<li>Added optional Environment variables for the customisation scripts</li>
<li>Added Folder field for template destination</li>
</ul>


<p><span id="more-5851"></span></p>



<p><h2>Intro</h2>
<p>As I briefly mentioned in the introduction paragraph, setting up a VM with the Photon OS 2.0, is a two-step process.</p></p>



<ul class="wp-block-list"><li>Deploy the appliance</li><li>Configure the OS</li></ul>



<p>The first step is rather straight-forward with PowerCLI&#8217;s <a href="https://vdc-repo.vmware.com/vmwb-repository/dcr-public/d402b7ed-b345-4fda-880d-a48e8885e910/b6ff10a7-3769-4346-8a83-d92d99d6caf3/doc/Import-VApp.html" target="_blank" rel="noopener noreferrer">Import-VApp</a> cmdlet.</p>



<p>The second step, configuring the Photon OS, is presented as a manual step in most of the articles I could find. Till now!</p>



<p>With my Invoke-VMScripPlus function, this step can now also be automated. And that automation step is driven by a JSON file. This JSON file is the part that you place under version control.</p>



<p>The following scenario, which I&#8217;m currently using, creates a VM with a configured Photon 2.0 guest OS. Then converts that VM into a Template, which can then be used to roll out your VMs.</p>



<h2 class="wp-block-heading">Create a Photon VM Template</h2>



<p>This step is fully automated and driven by a JSON configuration file. In a matter of minutes you can create a new Photon 2.0 template, without even logging on to the machine!</p>



<h3 class="wp-block-heading">The JSON Configuration file</h3>



<p>The objective to have all configuration parameters in a flat text file is of course to be able to easily place it under version control and to easily compare between versions.</p>



<p>The <strong>JSON file</strong> contains a number of <strong>Level-1</strong> entries, each specifying part of the final configuration of the Photon Template.</p>


<p>&nbsp;</p>


<div class="wp-block-image"><figure class="alignleft is-resized"><img loading="lazy" decoding="async" src="https://www.lucd.info/wp-content/uploads/2018/08/json-main.jpg" alt="" class="wp-image-5867" width="207" height="296"/></figure></div>



<p>&nbsp;</p>
<ul class="wp-block-list">
<li><strong>vSphere</strong>: describes the Location for the template</li>
<li><strong>Template</strong>: characteristics of the Template</li>
<li><strong>Network</strong>: the network configuration inside the guest OS</li>
<li><strong>Account</strong>: the current and new password for the root account</li>
<li><strong>Proxy</strong>: the proxy settings, if present</li>
<li><strong>Docker</strong>: the docker configuration</li>
<li><strong>Code</strong>: a collection of customisation scripts</li>
</ul>



<p>&nbsp;</p>



<h4 class="wp-block-heading"> </h4>
<h4> </h4>
<h4>vSphere</h4>
<p><img loading="lazy" decoding="async" class="alignnone wp-image-5937 size-full" src="https://www.lucd.info/wp-content/uploads/2018/08/json-vsphere-1.jpg" alt="" width="722" height="95" srcset="https://www.lucd.info/wp-content/uploads/2018/08/json-vsphere-1.jpg 722w, https://www.lucd.info/wp-content/uploads/2018/08/json-vsphere-1-300x39.jpg 300w, https://www.lucd.info/wp-content/uploads/2018/08/json-vsphere-1-720x95.jpg 720w" sizes="auto, (max-width: 722px) 100vw, 722px" /></p>





<ul class="wp-block-list"><li><strong>VMHost</strong>: a cluster or an ESXi node where the VM will be installed. If you specify a cluster, the script takes a random ESXi node in that cluster.</li><li><strong>Storage</strong>: a datastorecluster or datastore where the VM will be installed.</li><li><strong>OvaPath</strong>: where the script can find the Photon 2.0 ova file. Note the escaped back-slashes in the path.</li></ul>



<h4 class="wp-block-heading">Template</h4>
<p><img loading="lazy" decoding="async" class="alignnone size-full wp-image-5939" src="https://www.lucd.info/wp-content/uploads/2018/08/json-template-1.jpg" alt="" width="687" height="211" srcset="https://www.lucd.info/wp-content/uploads/2018/08/json-template-1.jpg 687w, https://www.lucd.info/wp-content/uploads/2018/08/json-template-1-300x92.jpg 300w" sizes="auto, (max-width: 687px) 100vw, 687px" /></p>





<ul class="wp-block-list"><li><strong>TemplateName</strong>, <strong>TemplateNote</strong>: self explanatory</li><li><strong>Folder</strong>: a VM type folder where the template shall be stored</li><li><strong>NumCpu</strong>, <strong>MemoryGB</strong>, <strong>HarddiskGB</strong>:&nbsp;self explanatory</li><li><strong>GuestID</strong>: has to be <strong>other3xLinux64Guest</strong> for Photon 2.0</li><li><strong>CreateTemplate</strong>: when set to false, the script will not power off the VM, nor will it convert the VM into a Template. This can be used will fine-tuning the customisation scripts (see later).</li><li><strong>CustomisationCode</strong>: the names of the customisation scripts that will be executed once the VM is installed. See later in the <strong>Code</strong> section.</li></ul>



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



<p>Since most of the customization of the Photon guest OS was done when we created the Template, this step requires very little extra customization steps.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="426" height="171" src="https://www.lucd.info/wp-content/uploads/2018/08/json-network.jpg" alt="" class="wp-image-5872" srcset="https://www.lucd.info/wp-content/uploads/2018/08/json-network.jpg 426w, https://www.lucd.info/wp-content/uploads/2018/08/json-network-300x120.jpg 300w" sizes="auto, (max-width: 426px) 100vw, 426px" /></figure>



<p>I assume these settings all speak for themselves. Nothing out of the ordinary, just you regular network settings. Perhaps one point to note, when you have multiple values for a field, separate them with a space.</p>



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



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="313" height="81" src="https://www.lucd.info/wp-content/uploads/2018/08/json-account.jpg" alt="" class="wp-image-5873" srcset="https://www.lucd.info/wp-content/uploads/2018/08/json-account.jpg 313w, https://www.lucd.info/wp-content/uploads/2018/08/json-account-300x78.jpg 300w" sizes="auto, (max-width: 313px) 100vw, 313px" /></figure>



<p>Again, quite straight-forward. The account, and the old and new password.</p>



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



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="377" height="115" src="https://www.lucd.info/wp-content/uploads/2018/08/json-proxy.png" alt="" class="wp-image-5904" srcset="https://www.lucd.info/wp-content/uploads/2018/08/json-proxy.png 377w, https://www.lucd.info/wp-content/uploads/2018/08/json-proxy-300x92.png 300w" sizes="auto, (max-width: 377px) 100vw, 377px" /></figure>



<p>When your environment into which you are installing the VM is located behind a proxy, you can specify the proxy details in this section. This is required to get the Photon repositories working (tdnf), and also the docker repositories,</p>



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



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="230" height="56" src="https://www.lucd.info/wp-content/uploads/2018/08/json-docker.jpg" alt="" class="wp-image-5876"/></figure>



<p>Only one setting, the default port to access docker.</p>



<h4 class="wp-block-heading">Code</h4>
<p><img loading="lazy" decoding="async" class="alignnone size-full wp-image-5942" src="https://www.lucd.info/wp-content/uploads/2018/08/json-code-3.jpg" alt="" width="481" height="118" srcset="https://www.lucd.info/wp-content/uploads/2018/08/json-code-3.jpg 481w, https://www.lucd.info/wp-content/uploads/2018/08/json-code-3-300x74.jpg 300w" sizes="auto, (max-width: 481px) 100vw, 481px" /></p>





<p>Under the Code section, you can define a number of customisation scripts, that will be executed inside the Photon 2.0 guest OS. Each entry has a two fixed subfields.</p>



<ul class="wp-block-list"><li><strong>Name</strong>: the name for this particular customisation script. This is the name that is used in the <strong>Template</strong> section, in <strong>CustomisationCode</strong> field.</li><li><strong>Script</strong>: the actual customisation script. Due to restrictions in the JSON syntax, the script must be provided as an array of strings. Each line of the script is an element in this array</li><li><strong>Environment</strong>: an array with environment variables that will be passed to the environment where the customisation script is executed.</li></ul>



<p>In the example above, the customisation script is a <strong>one-liner.</strong> Hence one string in the Code array. In this example, the Code contains the line to pull the docker container that contains VMware PowerCLI.</p>



<p>There are of course multi-line customisation scripts. In that case the array contains multiple elements. The following example is an excerpt of the docker customisation script. As you notice, the code also contains comment lines and empty lines.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1013" height="549" src="https://www.lucd.info/wp-content/uploads/2018/08/json-code-2.jpg" alt="" class="wp-image-5879" srcset="https://www.lucd.info/wp-content/uploads/2018/08/json-code-2.jpg 1013w, https://www.lucd.info/wp-content/uploads/2018/08/json-code-2-300x163.jpg 300w, https://www.lucd.info/wp-content/uploads/2018/08/json-code-2-768x416.jpg 768w, https://www.lucd.info/wp-content/uploads/2018/08/json-code-2-720x390.jpg 720w" sizes="auto, (max-width: 1013px) 100vw, 1013px" /></figure>



<p>Since we do not want to hard-code values in our customisation scripts, we use a PowerShell style notation to refer to variables defined in other sections in the JSON file.</p>



<p>In <strong>line 126</strong> of excerpt above, we use the expression <strong>$($paramData.Docker.Port)</strong> to refer to the port number we defined in the Docker section.&nbsp;</p>



<p><p>To avoid that you have to type in your customisation scripts in this specific JSON format, the following short script can help you convert your regular bash script to JSON notation.</p>
<pre class="lang:ps decode:true">function Convert-Script2Json
{
  [cmdletbinding()]
  param(
    [Parameter(Mandatory = $true)]
    [string]$ScriptName,
    [Parameter(Mandatory = $true)]
    [string[]]$Script
  )

  $obj = New-Object PSObject -Property @{
      Name = $ScriptName
      Script = $script.Split("<code>n") | %{$_.TrimEnd("</code>r")}
  }
  $obj | ConvertTo-Json
}</pre></p>



<p><p>A sample run shows you how you could use the above code.</p>
<pre class="lang:ps decode:true">@'
# Update packages
# Separate to avoid script ending with VMware Tools update

tdnf update -y
'@

Convert-Script2Json -ScriptName packages -Script $code</pre></p>



<p>Will result in the following output, which you can copy/paste into your JSON file.</p>



<figure class="wp-block-image"><img loading="lazy" decoding="async" width="604" height="162" src="https://www.lucd.info/wp-content/uploads/2018/08/script2json.jpg" alt="" class="wp-image-5881" srcset="https://www.lucd.info/wp-content/uploads/2018/08/script2json.jpg 604w, https://www.lucd.info/wp-content/uploads/2018/08/script2json-300x80.jpg 300w" sizes="auto, (max-width: 604px) 100vw, 604px" /></figure>



<h3 class="wp-block-heading">The New-PhotonTemplate function</h3>



<p>Before you can run the <strong>New-PhotonTemplate</strong> function, you will need two functions that are used in the function. These are</p>



<ul class="wp-block-list"><li><strong>Invoke-VMScriptPlus</strong> from my <a href="https://www.lucd.info/2018/08/05/invoke-vmscriptplus-v2/" target="_blank" rel="noopener noreferrer">Invoke-VMScriptPlus v2</a> post.</li><li><strong>Set-VMKeystrokes</strong>, from William Lam&#8217;s <a href="https://www.virtuallyghetto.com/2017/09/automating-vm-keystrokes-using-the-vsphere-api-powercli.html" target="_blank" rel="noopener noreferrer">Automating VM keystrokes using the vSphere API &amp; PowerCLI&nbsp;</a></li></ul>



<p>You have multiple options to make these two functions available to the <strong>New-PhotonTemplate</strong> function.</p>



<ul class="wp-block-list"><li>Add them to the .ps1 file where you copied the New-PhotonTemplate function</li><li>Store the two functions in separate .ps1 files, and dot-source these .ps1 files before calling the New-PhotonTemplate function</li><li>Create a module, and add the two functions to that module</li></ul>



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


<pre class="urvanov-syntax-highlighter-plain-tag">function New-PhotonTemplate {
&lt;#
.SYNOPSIS
 Create a Template with the Photon 2.0 guest OS
.DESCRIPTION
 This function will create a new Template with the Photon 2.0
 guest OS. The parameters and the configuration scripts are passed via
 a JSON file.
.NOTES
 Author:  Luc Dekens
 Version:
 1.0 11/08/18  Initial release
 1.1 16/08/18  Create function
               Added support for Reboot action
               Added support for environment variable
 1.2 17/08/18  Added Folder (in JSON file) support
              
.PARAMETER JSONPath
 The location of the JSON file with the configuration parameters
.PARAMETER LogFile
 Optional file for capturing logging information.
 This includes the output of the customisation scripts
.EXAMPLE
 New-PhotonTemplate -JSONPath .\photon.json
.EXAMPLE
 New-PhotonTemplate -JSONPath .\photon.json -LogFile .\photon.log
#&gt;

  [cmdletbinding()]
  param(
    [Parameter(Mandatory = $true)]
    [string]$JSONPath,
    [string]$LogFile
  )

  function Write-ToLog {
    [CmdletBinding()]
    param(
      [string]$Category,
      [string]$Text
    )

    $timeStamp = (Get-Date).ToString('yyyyMMddThhmmss')
    $script = Split-Path -Path $myInvocation.ScriptName -Leaf
    $user = $env:UserName

    $line = "$timeStamp - $script - $user - [$Category] $Text"
    if ($LogFile) {
      $line | Out-File -FilePath $LogFile -Append
    }
    else {
      $line
    }
  }

  function Invoke-VMReboot {
    [CmdletBinding()]
    param(
      [string]$VMName
    )

    $vmGuest = Get-VMGuest -VM $VMName

    Stop-VMGuest -VM $vm -Confirm:$false | Out-Null
    while ($vmGuest.VM.PowerState -ne 'PoweredOff') {
      Start-Sleep 1
      $vmGuest = Get-VMGuest -VM $vmGuest.VM
    }

    $vmGuest = Start-VM -VM $vmGuest.VM -Confirm:$false | Get-VMGuest
    while ($vmGuest.VM.PowerState -ne 'PoweredOn' -or -not $vmGuest.ExtensionData.GuestOperationsReady) {
      Start-Sleep 1
      $vmGuest = Get-VMGuest -VM $vmGuest.VM
    }
  }

  # Get Parameters

  $paramDataJSON = Get-Content -Path $JSONPath | Out-String
  $paramData = ConvertFrom-Json -InputObject $paramDataJSON

  # Determine location for template

  $obj = Get-Inventory -Name $paramData.vSphere.VMHost
  if ($obj -is [VMware.VimAutomation.ViCore.Types.V1.Inventory.Cluster]) {
    $obj = Get-VMHost -Location $obj | Get-Random
  }
  $esx = $obj

  $dsc = Get-DatastoreCluster -Name $paramData.vSphere.Storage -ErrorAction SilentlyContinue
  if ($dsc) {
    $ds = Get-Datastore -RelatedObject $dsc | Get-Random
  }
  else {
    $ds = Get-Datastore -Name $paramData.vSphere.Storage
  }

  # Clean up eventual artifacts

  if ($vm = Get-VM -Name $paramData.Template.TemplateName -ErrorAction SilentlyContinue) {
    if ($vm.PowerState -eq [VMware.VimAutomation.ViCore.Types.V1.Inventory.PowerState]::PoweredOn) {
      Stop-VM -VM $vm -Confirm:$false | Out-Null
    }
    Remove-VM -VM $vm -Confirm:$false -DeletePermanently
    Write-ToLog -Category Info -Text "Removed VM $($vm.Name)"
  }
  if ($template = Get-Template -Name $paramData.Template.TemplateName -ErrorAction SilentlyContinue) {
    Remove-Template -Template $template -DeletePermanently -Confirm:$false
    Write-ToLog -Category Info -Text "Removed Template $($template.Name)"
  }

  # Import Appliance

  $ovfParm = Get-OvfConfiguration -Ovf $paramData.vSphere.OvaPath
  $ovfParm.NetworkMapping.None.Value = $paramData.Network.PortGroup
  $sVApp = @{
    Name              = $paramData.Template.TemplateName
    Source            = $paramData.vSphere.OvaPath
    OvfConfiguration  = $ovfParm
    VMHost            = $esx
    Datastore         = $ds
    DiskStorageFormat = [VMware.VimAutomation.ViCore.Types.V1.VirtualDevice.VirtualDiskStorageFormat]::Thin
    Confirm           = $false
  }
  if($paramData.Template.Folder){
    $sVApp.Add('InventoryLocation',(Get-FolderByPath -Path $paramData.Template.Folder))
  }
  Write-ToLog -Category Info -Text "Importing vApp..."
  $vm = Import-VApp @sVApp
  Write-ToLog -Category Info -Text "Imported vApp $($sVApp.Name)"

  # Configure VM

  $vm = Get-VM -Name $paramData.Template.TemplateName

  $sSet = @{
    VM       = $vm
    NumCpu   = $paramData.Template.NumCpu
    MemoryGB = $paramData.Template.MemoryGB
    Notes    = $paramData.Template.TemplateNote.Replace('#timestamp#', (Get-Date -Format 'dd/MM/yyyy HH:mm'))
    Confirm  = $false
  }
  Set-VM @sSet &gt; $null
  Get-HardDisk -VM $vm | Set-HardDisk -CapacityGB $paramData.Template.HarddiskGB -Confirm:$false  &gt; $null
  Get-FloppyDrive -VM $vm | Remove-FloppyDrive -Confirm:$false &gt; $null
  Write-ToLog -Category Info -Text "Configured VM $($vm.Name)"

  Start-VM -VM $vm &gt; $null
  Write-ToLog -Category Info -Text "Power on VM $($vm.Name)"

  while ($vm.ExtensionData.Guest.ToolsRunningStatus -ne [VMware.Vim.VirtualMachineToolsRunningStatus]::guestToolsRunning -or
    $vm.ExtensionData.Runtime.PowerState -ne [VMware.Vim.VirtualMachinePowerState]::poweredOn) {
    Start-Sleep 2
    $vm.ExtensionData.UpdateViewData()
  }
  Write-ToLog -Category Info -Text "VM $($vm.Name) is powered on"

  # Change Photon OS default root password
  # Thanks to William Lam for his Set-VMKeystrokes function
  # See https://www.virtuallyghetto.com/2017/09/automating-vm-keystrokes-using-the-vsphere-api-powercli.html

  Write-ToLog -Category Info -Text "Change password for user $($paramData.Account.User)"

  Set-VMKeystrokes -VMName $VM -StringInput $paramData.Account.User -ReturnCarriage $true | Out-Null
  Set-VMKeystrokes -VMName $VM -StringInput $paramData.Account.OldPassword -ReturnCarriage $true | Out-Null
  Set-VMKeystrokes -VMName $VM -StringInput $paramData.Account.OldPassword -ReturnCarriage $true | Out-Null
  Set-VMKeystrokes -VMName $VM -StringInput $paramData.Account.NewPassword -ReturnCarriage $true | Out-Null
  Set-VMKeystrokes -VMName $VM -StringInput $paramData.Account.NewPassword -ReturnCarriage $true | Out-Null

  Set-VMKeystrokes -VMName $VM -StringInput "exit" -ReturnCarriage $true | Out-Null

  $vm = Get-VM -Name $paramData.Template.TemplateName

  # Run all customisation scripts

  foreach ($code in $paramData.Template.CustomisationCode) {
    if ($code -eq 'reboot') {
      Write-ToLog -Category Info -Text "Rebooting VM $($vm.Name)..."
      Invoke-VMReboot -VMName $vm.Name
      Write-ToLog -Category Info -Text "Rebooted VM $($vm.Name)"
    }
    else {
      $codeObj = $paramData.Code.Where{$_.Name -eq $code}
      $sInvoke = @{
        VM               = $vm
        ScriptType       = 'Bash'
        ScriptText       = $ExecutionContext.InvokeCommand.ExpandString(($codeObj.Script -join "`r`n"))
        GuestUser        = $paramData.Account.User
        GuestPassword    = ConvertTo-SecureString -String $paramData.Account.NewPassword -AsPlainText -Force
        GuestOSType      = 'Linux'
      }
      if($codeObj.Environment){
        $sInvoke.Add('ScriptEnvironment',$codeObj.Environment.foreach{$ExecutionContext.InvokeCommand.ExpandString($_)})
      }
      Write-ToLog -Category Info -Text "Running $code"
      $result = Invoke-VMScriptPlus @sInvoke
      if ($LogFile) {
        $result | Out-File -FilePath $LogFile -Append
      }
      else {
        $result
      }
    }
  }

  # Create Template

  if ($paramData.Template.CreateTemplate) {
    $vm = Get-VM -Name $paramData.Template.TemplateName

    if ($vm.PowerState -eq [VMware.VimAutomation.ViCore.Types.V1.Inventory.PowerState]::PoweredOn) {
      Write-ToLog -Category Info -Text "Stopping VM $($vm.Name) ..."
      Shutdown-VMGuest -VM $vm -Confirm:$false &gt; $null
      while ($vm.PowerState -ne [VMware.VimAutomation.ViCore.Types.V1.Inventory.PowerState]::PoweredOff) {
        Start-Sleep 3
        $vm = Get-VM -Name $paramData.Template.TemplateName
      }
      Write-ToLog -Category Info -Text "VM $($vm.Name) stopped"
    }

    Write-ToLog -Category Info -Text "Convert VM $($vm.Name) to Template"
    Set-vm -VM $vm -ToTemplate -Confirm:$false &gt; $null
  }
}</pre>
<p> </p>


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



<p><p><strong>Line 36-54</strong>: An inline function to have all messages in the same format.</p>
<p><strong>Line 56-75</strong>: A reboot the guest OS function. Called when the &#8220;reboot&#8221; instruction is encountered. See later.</p></p>



<p><strong>Line 79-80</strong>: These lines read the JSON file and convert the data to a PowerShell object</p>
<p><strong>Line 84-88</strong>: The <strong>vSphere.VMHost</strong> field can contain a cluster or an ESXi node. These lines handle those options. In case of a cluster, the function selects a random ESXi node from the cluster.</p>
<p><strong>Line 90-96</strong>: The <strong>vSphere.Storage</strong> field can contain a DatastoreCluster or a Datastore. These lines handle those options. In case of a datastorecluster, the function selects a random datastore from the datastorecluster.</p>
<p><strong>Line 100-110</strong>: These lines check if there is already a Template or a VirtualMachine with a name as specified in <strong>Template.TemplateName</strong>. If there is, the Template or VirtualMachine are removed.</p>
<p><strong>Line 114-130</strong>: A straight-forward method to import the Photon OVA as defined in <strong>vSphere.OvaPath</strong>.</p>
<p><strong>Line 134-146</strong>:  Further HW configuration of the VM</p>
<p><strong>Line 148-156</strong>: The new VM is powered on, and the function waits till the VMware Tools are responding</p>
<p><strong>Line 162-170</strong>: The function changes the root password with the new password defined in <strong>Account.NewPassword</strong>. For this action the code uses the Set-VMKeystrokes function. <a href="https://twitter.com/lamw" target="_blank" rel="noopener noreferrer">William</a> describes this ingenious method in his <a href="https://www.virtuallyghetto.com/2017/09/automating-vm-keystrokes-using-the-vsphere-api-powercli.html" target="_blank" rel="noopener noreferrer">Automating VM keystrokes using the vSphere API &amp; PowerCLI</a> post, well worth a read!</p>
<p><strong>Line 176-204</strong>: The code now runs all the customisation script that are defined in <strong>Template.CustomisationCode</strong>. The actual customisation scripts are defined in the <strong>Code</strong> section of the JSON file.</p>
<p><strong>Line 177-182</strong>: With reserved name &#8220;reboot&#8221; you can request a reboot of the guest OS. This is executed in the sequence order with the customisation scripts as defined in the Template.CustomisationCode field.</p>
<p><strong>Line 187,193</strong>: With the <strong>ExpandString</strong> method the script expands all $paramData occurrances in the customisation scripts with the actual values. This allows us to keep the customisation script general and not coded for a specific instance.</p>
<p><strong>Line 192-194</strong>: If the customisation script contains environment variables in the Code.Environment field, these are environment variables are passed along with the customisatoin script to the Invoke-VMScriptPlus function.</p>
<p><strong>Line 208-223</strong>: Based on the <strong>Template.CreateTemplate</strong> value (true or false) the VM is converted into a Template. One reason not to convert the VM to a Template, could be that your debugging one or more of the customisation scripts.</p>
<p>



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



</p>
<p>Now we have everything in place to create a Photon 2.0 template, based on the  configuration settings and customisation scripts in our JSON file. Since it can be useful to review the output, especially of the customisation scripts, we use the LogFile parameter to specify a file where the function can write the output.</p>
<p>



</p>
<pre class="wp-block-code"><code>New-PhotonTemplate -JSONPath .\LabParams.json -LogFile C:\Temp\photon-template.log</code></pre>
<p>



</p>
<p>When we call the <strong>New-PhotonTemplate</strong> with the <strong>LogFile</strong> parameter, there will be no regular output on the PowerShell console.</p>
<p>



</p>
<p>The logfile contains useful information, especially if we add statements to our customisation scripts to retrieve information on the components. This excerpt shows some information that was obtained in the <strong>docker</strong> customisation script.</p>
<p>



</p>
<figure class="wp-block-image"><img loading="lazy" decoding="async" width="1716" height="518" class="wp-image-5894" src="https://www.lucd.info/wp-content/uploads/2018/08/cust-out-1.jpg" alt="" srcset="https://www.lucd.info/wp-content/uploads/2018/08/cust-out-1.jpg 1716w, https://www.lucd.info/wp-content/uploads/2018/08/cust-out-1-300x91.jpg 300w, https://www.lucd.info/wp-content/uploads/2018/08/cust-out-1-768x232.jpg 768w, https://www.lucd.info/wp-content/uploads/2018/08/cust-out-1-1024x309.jpg 1024w, https://www.lucd.info/wp-content/uploads/2018/08/cust-out-1-720x217.jpg 720w" sizes="auto, (max-width: 1716px) 100vw, 1716px" /></figure>
<p>



</p>
<p>When all goes well, the <strong>New-PhotonTemplate</strong> will have created a Template with the Photon 2.0 guest OS.</p>
<p>



</p>
<h3 class="wp-block-heading">Known Issues</h3>
<p>



</p>
<h4 class="wp-block-heading">open-vm-tools</h4>
<p>



</p>
<p>When you use one of the customisation scripts to upgrade the Photon components (command <strong>tdnf upgrade</strong>), and the <strong>open-vm-tools</strong> are in there, the returned information of the script might not be complete. You will miss the ScriptOutput and the ExitCode.</p>
<p>



</p>
<p>As a workaround, upgrade the open-vm-tools separately before upgrading any of the other components.</p>
<p>



</p>
<p>



</p>
<h3 class="wp-block-heading">Files</h3>
<p>



</p>
<p>Attached to this post are some files that will help you get started. There is an empty, skeleton JSON file and two Photon customisation scripts, one for the network and one for docker.</p>
<p>



</p>
<h2 class="wp-block-heading">Next</h2>
<p>



</p>
<p>In <strong>Part 2</strong> of this series I will be showing how the Template we just created, can be used to roll out any number of VMs.</p>
<p>



</p>
<p>Enjoy!</p>
<p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.lucd.info/2018/08/14/deploy-photon-2-0-part-1/feed/</wfw:commentRss>
			<slash:comments>5</slash:comments>
		
		
			</item>
		<item>
		<title>Invoke-VMScriptPlus v2</title>
		<link>https://www.lucd.info/2018/08/05/invoke-vmscriptplus-v2/</link>
					<comments>https://www.lucd.info/2018/08/05/invoke-vmscriptplus-v2/#comments</comments>
		
		<dc:creator><![CDATA[LucD]]></dc:creator>
		<pubDate>Sun, 05 Aug 2018 11:16:26 +0000</pubDate>
				<category><![CDATA[Guest Operations]]></category>
		<category><![CDATA[Guest OS]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[PowerCLI]]></category>
		<category><![CDATA[VMware Tools]]></category>
		<category><![CDATA[Windows]]></category>
		<category><![CDATA[Invoke-VMScript]]></category>
		<guid isPermaLink="false">http://www.lucd.info/?p=5804</guid>

					<description><![CDATA[Invoke-VMScriptPlus function V2. Run scripts in a VM's Guest OS. Can target Windows and Linux Guest OS families. And adds support for PowerShell Core (aka PowerShell v6).]]></description>
										<content:encoded><![CDATA[
<p>The ability to execute scripts inside the guest OS of your VMs, is definitely one of the more useful cmdlets available in <a href="https://code.vmware.com/web/dp/tool/vmware-powercli/10.1.1">VMware PowerCLI</a>. A year ago I published the first version of my <a href="https://www.lucd.info/2017/09/14/invoke-vmscriptplus/">Invoke-VMScriptPlus</a> function to solve some of the issues the <a href="https://vdc-repo.vmware.com/vmwb-repository/dcr-public/73d6de02-05fd-47cb-8f73-99d1b33aea17/850c6b63-eb82-4d9c-bfcf-79279b5e5637/doc/Invoke-VMScript.html">Invoke-VMScript</a> cmdlet has in my opinion.<br />That function allowed you to run <strong>multi-line scripts</strong> in a Linux guest OS on your VMs. It also allowed you to use <strong>she-bang</strong> lines, to indicate which interpreter your script had to run in (bash, perl, python, nodejs, php&#8230;). Another handy feature was that you could use Linux <strong>here-documents</strong> in your scripts.</p>
<p><img loading="lazy" decoding="async" class="alignnone wp-image-5807 size-large" src="https://www.lucd.info/wp-content/uploads/2018/08/invokevmscriptplus_v2-1024x921.jpg" alt="" width="770" height="693" srcset="https://www.lucd.info/wp-content/uploads/2018/08/invokevmscriptplus_v2-1024x921.jpg 1024w, https://www.lucd.info/wp-content/uploads/2018/08/invokevmscriptplus_v2-300x270.jpg 300w, https://www.lucd.info/wp-content/uploads/2018/08/invokevmscriptplus_v2-768x691.jpg 768w, https://www.lucd.info/wp-content/uploads/2018/08/invokevmscriptplus_v2-720x648.jpg 720w, https://www.lucd.info/wp-content/uploads/2018/08/invokevmscriptplus_v2.jpg 1043w" sizes="auto, (max-width: 770px) 100vw, 770px" /></p>





<p>With the introduction of <a href="https://github.com/PowerShell/PowerShell">PowerShell Core</a> (aka PowerShell v6), the lack of support for any Guest OS of the Windows family became obvious. Prompted by a recent thread in the <a href="https://communities.vmware.com/community/vmtn/automationtools/powercli">VMTN PowerCLI Community</a>, I decided it was time to publish a new version of my <strong>Invoke-VMScriptPlus</strong> function.</p>
<p>&nbsp;</p>

<p><span style="background-color: #ffff00;"><strong>Update August 21st 2018</strong></span></p>
<ul>
<li>Added ScriptEnvironment</li>
</ul>

<p><span id="more-5804"></span></p>



<h2>Intro</h2>
<p>I defined some target features  for the new version of the <strong>Invoke-VMScriptPlus</strong> function:</p>



<ul class="wp-block-list">
<li>Support the Windows family of Guest OS</li>
<li>Support using PowerShell Core scripts (Windows and Linux)</li>
<li>Fix the ScriptText length limitation</li>
</ul>
<h2>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
.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
.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 PSv6Version
  Indicates which PowerShell Core version to use.
  The default is 6.0.2
.PARAMETER CRLF
  Switch to indicate of the NL that is returned by Linux, shall be
  converted to a CRLF
.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.
.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
  #&gt;
  [cmdletbinding()]
  param(
    [parameter(Mandatory = $true, ValueFromPipeline = $true)]
    [MyOBN('VirtualMachine')]
    [VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine[]]$VM,
    [Parameter(Mandatory = $true, ParameterSetName = 'PlainText')]
    [String]$GuestUser,
    [Parameter(Mandatory = $true, ParameterSetName = 'PlainText')]
    [SecureString]$GuestPassword,
    [Parameter(Mandatory = $true, ParameterSetName = 'PSCredential')]
    [PSCredential[]]$GuestCredential,
    [Parameter(Mandatory = $true)]
    [String]$ScriptText,
    [Parameter(Mandatory = $true)]
    [ValidateSet('bash', 'perl', 'python3', 'nodejs', 'php', 'lua', 'powershell',
      'powershellv6', 'bat')]
    [String]$ScriptType,
    [String[]]$ScriptEnvironment,
    [ValidateSet('Windows', 'Linux')]
    [String]$GuestOSType,
    [String]$PSv6Version = '6.0.2',
    [Switch]$CRLF,
    [MyOBN('VIServer')]
    [VMware.VimAutomation.ViCore.Types.V1.VIServer]$Server = $global:DefaultVIServer
  )
  Begin {
    $si = Get-View ServiceInstance
    $guestMgr = Get-View -Id $si.Content.GuestOperationsManager
    $gFileMgr = Get-View -Id $guestMgr.FileManager
    $gProcMgr = Get-View -Id $guestMgr.ProcessManager
    $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'
    }
  }
  Process {
    foreach ($vmInstance in $VM) {
      # Preamble
      if ($vmInstance.PowerState -ne 'PoweredOn') {
        Write-Error "VM $($vmInstance.Name) is not powered on"
        continue
      }
      if ($vmInstance.ExtensionData.Guest.ToolsRunningStatus -ne 'guestToolsRunning') {
        Write-Error "VMware Tools are not running on VM $($vmInstance.Name)"
        continue
      }
      $moref = $vmInstance.ExtensionData.MoRef
      # Are we targetting a Windows or a Linux box?
      if (-not $GuestOSType) {
        switch -Regex ($vmInstance.Guest.OSFullName) {
          'Windows' {
            $GuestOSType = 'Windows'
            if ('bat', 'powershell', 'powershellv6' -notcontains $ScriptType) {
              Write-Error "For a Windows guest OS the ScriptType can be Bat, PowerShell or PowerShellv6"
              continue
            }
          }
          'Linux' {
            $GuestOSType = 'Linux'
            if ('bat', 'powershell' -contains $ScriptType) {
              Write-Error "For a Linux guest OS the ScriptType cannot be Bat or PowerShell"
              continue
            }
          }
          Default {
            Write-Error "Unable to determine the guest OS type on VM $($vmInstance.Name)"
            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)"
        }
      }
      # Create Authentication Object (User + Password)
      if ($PSCmdlet.ParameterSetName -eq 'PSCredential') {
        $GuestUser = $GuestCredential.GetNetworkCredential().username
        $plainGuestPassword = $GuestCredential.GetNetworkCredential().password
      }
      if ($PSCmdlet.ParameterSetName -eq 'PlainText') {
        $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
      # Copy script to temp file in guest
      # Create temp file for script
      $suffix = ''
      if ($ScriptType -eq 'bat') {
        $suffix = ".cmd"
      }
      if ('powershell', 'powershellv6' -contains $ScriptType) {
        $suffix = ".ps1"
      }
      Try {
        $tempFile = $gFileMgr.CreateTemporaryFileInGuest($moref, $auth, "$($env:USERNAME)_$($PID)", $suffix, $null)
          Write-Verbose "Created temp script file in guest OS $($tempFile.Name)"
      }
      Catch {
        Throw "$error[0].Exception.Message"
      }
      # Create temp file for output
      Try {
        $tempOutput = $gFileMgr.CreateTemporaryFileInGuest($moref, $auth, "$($env:USERNAME)_$($PID)_output", $null, $null)
          Write-Verbose "Created temp output file in guest OS $($tempOutput.Name)"
      }
      Catch {
        Throw "$error[0].Exception.Message"
      }
      # Copy script to temp file
      if ($GuestOSType -eq 'Linux') {
        $ScriptText = $ScriptText.Split("</code>r") -join ''
      }
      $attr = New-Object VMware.Vim.GuestFileAttributes
      $clobber = $true
      $filePath = $gFileMgr.InitiateFileTransferToGuest($moref, $auth, $tempFile, $attr, $ScriptText.Length, $clobber)
      $ip = $filePath.split('/')[2].Split(':')[0]
      $hostName = Resolve-DnsName -Name $ip | Select-Object -ExpandProperty NameHost
      $filePath = $filePath.replace($ip, $hostName)
      $copyResult = Invoke-WebRequest -Uri $filePath -Method Put -Body $ScriptText
      if ($copyResult.StatusCode -ne 200) {
        Throw "ScripText copy failed!<code>rStatus $($copyResult.StatusCode)</code>r$(($copyResult.Content | ForEach-Object{[char]$_}) -join '')"
      }
      Write-Verbose "Copied scipttext to temp script file"

      # Get current environment variables

      $SystemEnvironment = $gProcMgr.ReadEnvironmentVariableInGuest($moref, $auth, $null)

      # Run script

      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 "Run script file"
          }
          Catch {
            Throw "$error[0].Exception.Message"
          }
          # Run temp file
          $spec = New-Object VMware.Vim.GuestProgramSpec
          if($ScriptEnvironment){
            $spec.EnvVariables = $SystemEnvironment + $ScriptEnvironment
          }
          $spec.Arguments = " &gt; $($tempOutput)"
          $spec.ProgramPath = "$($tempFile)"
          Try {
            $procId = $gProcMgr.StartProgramInGuest($moref, $auth, $spec)
            Write-Verbose "Run script with '$($tempFile) &gt; $($tempOutput)'"
          }
          Catch {
            Throw "$error[0].Exception.Message"
          }
        }
        'Windows' {
          # Run temp file
          $spec = New-Object VMware.Vim.GuestProgramSpec
          if($ScriptEnvironment){
            $spec.EnvVariables = $SystemEnvironment + $ScriptEnvironment
          }
          switch ($ScriptType) {
            'PowerShell' {
              $spec.Arguments = " /C powershell -NonInteractive -File $($tempFile) &gt; $($tempOutput)"
              $spec.ProgramPath = "cmd.exe"
            }
            'PowerShellv6' {
              $spec.Arguments = " /C ""C:\Program Files\PowerShell\$($PSv6Version)\pwsh.exe"" -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 {
            Throw "$error[0].Exception.Message"
          }
        }
      }
      # 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 {
        Throw "$error[0].Exception.Message"
      }
      # Retrieve output from script
      $fileInfo = $gFileMgr.InitiateFileTransferFromGuest($moref, $auth, $tempOutput)
      $fileContent = Invoke-WebRequest -Uri $fileInfo.Url -Method Get
      if ($fileContent.StatusCode -ne 200) {
        Throw "Retrieve of script output failed!<code>rStatus $($fileContent.Status)</code>r$(($fileContent.Content | ForEach-Object{[char]$_}) -join '')"
      }
      Write-Verbose "Get output from $($fileInfo.Url)"
      # Clean up
      # Remove output file
      $gFileMgr.DeleteFileInGuest($moref, $auth, $tempOutput)
      Write-Verbose "Removed file $($tempOutput.Name)"
      # Remove temp script file
      $gFileMgr.DeleteFileInGuest($moref, $auth, $tempFile)
      Write-Verbose "Removed file $($tempFile.Name)"
      New-Object PSObject -Property @{
        VM           = $vmInstance
        ScriptOutput = &amp; {
          $out = ($fileContent.Content | 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
        GuestOS      = $GuestOSType
      }
    }
  }
}</pre>
<p>&nbsp;</p>
<h2>Annotations</h2>
<p><strong>Line 1-35</strong>: The latest version of my OBN (Object By Name) class. It allows one to pass, or the actual .Net object, or the name of the object, as an argument to a parameter. See also <a href="https://www.lucd.info/2017/05/30/home-made-obn/" target="_blank" rel="noopener">Home Made OBN</a></p>
<p><strong>Line 136</strong>: When the Invoke-VMScriptPlus  function cannot determine the Guest OS family, this parameter allows you to force a specific Guest OS family. Accepted values are <strong>Windows</strong> and <strong>Linux</strong>.</p>
<p><strong>Line 133</strong>: The installation path of <strong>PowerShell Core</strong> on a Windows OS contains the <strong>version number</strong>. I could not find an easy way to determine the version number. This parameter allows to specify a specific PowerShell Core version. The default version is 6.0.2, and that is because that is the highest version VMware PowerCLI currently supports. <strong>Note</strong> that for the time being running VMware PowerCLI does not officially support running in PowerShell Core on a Windows Guest OS. Always consult the latest <a href="https://vdc-download.vmware.com/vmwb-repository/dcr-public/5258f6a7-9431-4960-a68e-bc5ef87a4ebf/49a4989b-e7a4-46e6-adae-1f39a68dd347/powercli1000-compat-matrix.html" target="_blank" rel="noopener">Compatibility Matrix</a> to find out what is officially supported by <a href="https://code.vmware.com/web/dp/tool/vmware-powercli/10.1.1" target="_blank" rel="noopener">VMware PowerCLI</a>.</p>
<p><strong>Line 134</strong>: This parameter allows you to pass a number of environment variables as an array of strings</p>
<p><strong>Line 138</strong>: Most Linux OS return output with only a LF. This switch can be used to convert the LF to a CRLF, when the resulting output of the script is returned.</p>
<p><strong>Line 147-155</strong>: A hard-coded table with the supported interpreters, and their corresponding SheBang line. This version of the Invoke-VMScriptPlus  function adds PowerShell Core to the table.</p>
<p><strong>Line 160-167</strong>: If the VM is not powered on, or if the VMware Tools are not running, or if the Guest OS family can not be determined, the function will return with an error message.</p>
<p><strong>Line 170-191</strong>: The Invoke-VMScriptPlus  function tests, based on the Guest OS family, if the requested ScriptType is valid.</p>
<p><strong>Line 192-199</strong>: For a VM with a Guest OS in the Linux family, tests if there is a SheBang line in the ScriptText. If not, add a line based on the ScriptType value.</p>
<p><strong>Line 202-213:</strong> Create the Authentication object for the GuestOperations methods. <strong>Note</strong> that in this release of the function, the GuestPassword requires a <strong>SecureString</strong> instead of a String.</p>
<p><strong>Line 216-222</strong>: Depending on the ScriptType, the temporary file, see the following annotations, will have a filetype.</p>
<p><strong>Line 223-237</strong>: The Invoke-VMScriptPlus  function uses two temporary files to store the script and the script’s output.</p>
<p><strong>Line 239-241</strong>: If the target Guest OS family is Linux, the line endings in the ScriptText are converted from CRLF to LF</p>
<p><strong>Line 242-252</strong>: The ScriptText is copied to the temporary file. This is done over HTTPS with the Invoke-WebRequest cmdlet. Note that the temporary filename returned earlier contains the IP address of the ESXi node on which the VM is running. This potentially causes problems with the Invoke-WebRequest and invalid certificates. In the URI for the file, the function replaces the IP address with FQDN.</p>
<p><strong>Line 256</strong>: Fetch the current system environment variables</p>
<p><strong>Line 263-272</strong>: On a Guest OS in the Linux family, the file containing the ScriptText needs to be made “executable”</p>
<p><strong>Line 276</strong>: The function merges the system environment variables with the provided environment variables. The resulting array is passed to the StartProgramInGuest call.</p>
<p><strong>Line 278-286</strong>: On a Guest OS in the Windows family, the path to the PowerShell Core EXE, contains the version number. This is where the $PSv6Version parameter comes into play. The default value is currently set to 6.0.2.</p>
<p><strong>Line 280-286,308-314</strong>: Script execution is started</p>
<p><strong>Line 318-328</strong>: Wait for the script execution to end</p>
<p><strong>Line 330-335</strong>: The output is fetched, again with an Invoke-WebRequest.</p>
<p><strong>Line 338-341</strong>: Clean up the temporary files</p>
<p><strong>Line 343-364</strong>: Return an object containing the script output and further info about the script execution</p>
<h2>Sample Use</h2>
<p>The following snippet lists the content of the <strong>os-release</strong> file, a common method in Linux distros to obtain the name and version of the OS. We use the following simple bash script.</p>
<p><strong>Note</strong> that in this new version of the Invoke-VMScriptPlus function you have to use a <strong>SecureString</strong> (instead of a String in the previous version) for the GuestPassword parameter. This is a breaking change, but was done to comply with the <a href="https://www.powershellgallery.com/packages/PSScriptAnalyzer/1.17.1" target="_blank" rel="noopener">ScriptAnalyzer</a> rules!</p>
<pre class="lang:ps decode:true">$user = 'luc'
$pswd = 'SuperSecret1!'
$sPswd = ConvertTo-SecureString -String $pswd -AsPlainText -Force

$code =@'
if [ -f /etc/os2-release ]
then
        cat /etc/os-release
else
        echo 'File os-release not found'
fi
'@

$sInvP = @{
   VM = 'opensuse1'
   ScriptType = 'Bash'
   ScriptText = $code
   GuestOSType = 'Linux'
   GuestUser = $user
   GuestPassword = $sPswd
}
Invoke-VMScriptPlus @sInvP</pre>
<p>For an <strong>openSuse</strong> guest OS the result comes back as</p>
<p><img loading="lazy" decoding="async" class="alignnone size-medium wp-image-5816" src="https://www.lucd.info/wp-content/uploads/2018/08/snip1-opensuse-1-278x300.jpg" alt="" width="278" height="300" srcset="https://www.lucd.info/wp-content/uploads/2018/08/snip1-opensuse-1-278x300.jpg 278w, https://www.lucd.info/wp-content/uploads/2018/08/snip1-opensuse-1.jpg 441w" sizes="auto, (max-width: 278px) 100vw, 278px" /></p>
<p>For an <strong>Ubuntu</strong> guest OS</p>
<p><img loading="lazy" decoding="async" class="alignnone size-medium wp-image-5817" src="https://www.lucd.info/wp-content/uploads/2018/08/snip1-ubuntu-296x300.jpg" alt="" width="296" height="300" srcset="https://www.lucd.info/wp-content/uploads/2018/08/snip1-ubuntu-296x300.jpg 296w, https://www.lucd.info/wp-content/uploads/2018/08/snip1-ubuntu.jpg 497w" sizes="auto, (max-width: 296px) 100vw, 296px" /></p>
<p>And for a <strong>CentOS</strong> guest OS as</p>
<p><img loading="lazy" decoding="async" class="alignnone size-medium wp-image-5818" src="https://www.lucd.info/wp-content/uploads/2018/08/snip1-centos-223x300.jpg" alt="" width="223" height="300" srcset="https://www.lucd.info/wp-content/uploads/2018/08/snip1-centos-223x300.jpg 223w, https://www.lucd.info/wp-content/uploads/2018/08/snip1-centos.jpg 428w" sizes="auto, (max-width: 223px) 100vw, 223px" /></p>
<p>One of the target goals of the <strong>Invoke-VMScriptPlus v2</strong> was to be able to use the function to runs scripts in a <strong>Windows guest OS</strong>. The following sample uses the <strong>wmic</strong> command.</p>
<pre class="lang:ps decode:true">$user = 'local\administrator'
$pswd = 'SuperSecret1!'
$sPswd = ConvertTo-SecureString -String $pswd -AsPlainText -Force

$code = @'
wmic os get version
'@

$sInvP = @{
   VM = 'Server1'
   ScriptType = 'bat'
   ScriptText = $code
   GuestUser = $user
   GuestPassword = $sPswd
}
Invoke-VMScriptPlus @sInvP

</pre>
<p>The result comes back as</p>
<p><img loading="lazy" decoding="async" class="alignnone size-medium wp-image-5819" src="https://www.lucd.info/wp-content/uploads/2018/08/snip2-win-300x194.jpg" alt="" width="300" height="194" srcset="https://www.lucd.info/wp-content/uploads/2018/08/snip2-win-300x194.jpg 300w, https://www.lucd.info/wp-content/uploads/2018/08/snip2-win.jpg 415w" sizes="auto, (max-width: 300px) 100vw, 300px" /></p>
<p>And of course PowerShell scripts</p>
<pre class="lang:ps decode:true">$user = 'local\administrator'
$pswd = 'SuperSecret1!'
$sPswd = ConvertTo-SecureString -String $pswd -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential -ArgumentList $user,$sPswd

$code = @'
(Get-CimInstance Win32_OperatingSystem).version
'@


$sInvP = @{
   VM = 'vEng'
   ScriptType = 'PowerShell'
   ScriptText = $code
   GuestCredential = $cred
}
Invoke-VMScriptPlus @sInvP</pre>
<p>Which returns</p>
<p><img loading="lazy" decoding="async" class="alignnone size-medium wp-image-5820" src="https://www.lucd.info/wp-content/uploads/2018/08/snip3-win-300x151.jpg" alt="" width="300" height="151" srcset="https://www.lucd.info/wp-content/uploads/2018/08/snip3-win-300x151.jpg 300w, https://www.lucd.info/wp-content/uploads/2018/08/snip3-win.jpg 461w" sizes="auto, (max-width: 300px) 100vw, 300px" /></p>
<p><strong>Note</strong> how this last invocation uses the <strong>GuestCredential</strong> parameter instead of <strong>GuestUser</strong> and <strong>GuestPassword</strong>. The <strong>Invoke-VMScriptPlus</strong> function supports both methods of passing the guest OS credentials.</p>
<p>This last sample is run under the &#8220;normal&#8221; PowerShell installed in the Windows guest OS, as we can easily see with</p>
<pre class="lang:ps decode:true">$user = 'local\administrator'
$pswd = 'SuperSecret1!'
$sPswd = ConvertTo-SecureString -String $pswd -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential -ArgumentList $user,$sPswd

$code = @'
$PSVersionTable
'@

$sInvP = @{
   VM = 'vEng'
   ScriptType = 'PowerShell'
   ScriptText = $code
   GuestCredential = $cred
}
Invoke-VMScriptPlus @sInvP</pre>
<p>The output of that script</p>
<p><img loading="lazy" decoding="async" class="alignnone size-medium wp-image-5822" src="https://www.lucd.info/wp-content/uploads/2018/08/snip4-win-300x249.jpg" alt="" width="300" height="249" srcset="https://www.lucd.info/wp-content/uploads/2018/08/snip4-win-300x249.jpg 300w, https://www.lucd.info/wp-content/uploads/2018/08/snip4-win.jpg 512w" sizes="auto, (max-width: 300px) 100vw, 300px" /></p>
<p>Another new feature of the <strong>Invoke-VMScriptPlus</strong> function is the ability to call <strong>PowerShell Core</strong>.</p>
<p>On Windows</p>
<pre class="lang:ps decode:true ">$user = 'local\administrator'
$pswd = 'SuperSecret1!'
$sPswd = ConvertTo-SecureString -String $pswd -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential -ArgumentList $user,$sPswd

$code = @'
$PSVersionTable
'@

$sInvP = @{
   VM = 'vEng'
   ScriptType = 'PowerShellv6'
   ScriptText = $code
   GuestCredential = $cred
}
Invoke-VMScriptPlus @sInvP</pre>
<p>which gives.<br />As you might have noted, we have both PowerShell version installed <strong>side-by-side</strong> on that station.</p>
<p><img loading="lazy" decoding="async" class="alignnone size-medium wp-image-5823" src="https://www.lucd.info/wp-content/uploads/2018/08/snip5-win-300x242.jpg" alt="" width="300" height="242" srcset="https://www.lucd.info/wp-content/uploads/2018/08/snip5-win-300x242.jpg 300w, https://www.lucd.info/wp-content/uploads/2018/08/snip5-win.jpg 551w" sizes="auto, (max-width: 300px) 100vw, 300px" /></p>
<p>And also on Linux</p>
<pre class="lang:ps decode:true">$user = 'luc'
$pswd = 'SuperSecret1!'
$sPswd = ConvertTo-SecureString -String $pswd -AsPlainText -Force

$code =@'
$PSVersionTable
'@

$sInvP = @{
   VM = 'opensuse1'
   ScriptType = 'PowerShellv6'
   ScriptText = $code
   GuestOSType = 'Linux'
   GuestUser = $user
   GuestPassword = $sPswd
}
Invoke-VMScriptPlus @sInvP</pre>
<p>Which results in</p>
<p><img loading="lazy" decoding="async" class="alignnone size-medium wp-image-5824" src="https://www.lucd.info/wp-content/uploads/2018/08/snip2-ubuntu-300x200.jpg" alt="" width="300" height="200" srcset="https://www.lucd.info/wp-content/uploads/2018/08/snip2-ubuntu-300x200.jpg 300w, https://www.lucd.info/wp-content/uploads/2018/08/snip2-ubuntu.jpg 684w" sizes="auto, (max-width: 300px) 100vw, 300px" /></p>
<p>The final new feature I introduced in <strong>Invoke-VMScriptPlus v2</strong> is the <strong>length limitation</strong> that exists in the regular <a href="https://vdc-repo.vmware.com/vmwb-repository/dcr-public/d402b7ed-b345-4fda-880d-a48e8885e910/b6ff10a7-3769-4346-8a83-d92d99d6caf3/doc/Invoke-VMScript.html" target="_blank" rel="noopener">Invoke-VMScript</a> cmdlet. That limitation blocks you when you want to transfer scripts that surpass a specific length. Depending on the circumstances that limitation pops up in scripts that have a length somewhere in the neighbourhood of 2600 characters.</p>
<p>A new feature that was introduced is the <strong>ScriptEnvironment</strong> parameter. This allows the caller to pass environment variables. A simple example:</p>
<pre class="lang:ps decode:true">$vm = Get-VM ubuntu1

$sInvoke = @{
    VM            = $vm
    ScriptType    = 'Bash'
    ScriptText    = 'printenv'
    GuestUser     = 'luc'
    GuestPassword = ConvertTo-SecureString -String 'SuperSecret1!' -AsPlainText -Force
    GuestOSType   = 'Linux'
    ScriptEnvironment = 'Msg1=Hello','Msg2=World'
}

Invoke-VMScriptPlus @sInvoke</pre>
<p>And we see our environment variables added to the execution environment.</p>
<p><img loading="lazy" decoding="async" class="alignnone size-medium wp-image-5931" src="https://www.lucd.info/wp-content/uploads/2018/08/invoke-environment-300x268.jpg" alt="" width="300" height="268" srcset="https://www.lucd.info/wp-content/uploads/2018/08/invoke-environment-300x268.jpg 300w, https://www.lucd.info/wp-content/uploads/2018/08/invoke-environment.jpg 619w" sizes="auto, (max-width: 300px) 100vw, 300px" /></p>
<p>If you encounter &#8220;issues&#8221; or have suggestions for additions and improvements, please let me know.</p>
<p>Enjoy!</p>
<p>&nbsp;</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.lucd.info/2018/08/05/invoke-vmscriptplus-v2/feed/</wfw:commentRss>
			<slash:comments>23</slash:comments>
		
		
			</item>
		<item>
		<title>PowerCLI, REST API and a sample module for Tag management</title>
		<link>https://www.lucd.info/2018/02/22/powercli-rest-api-sample-module-tag-management/</link>
					<comments>https://www.lucd.info/2018/02/22/powercli-rest-api-sample-module-tag-management/#comments</comments>
		
		<dc:creator><![CDATA[LucD]]></dc:creator>
		<pubDate>Thu, 22 Feb 2018 08:23:03 +0000</pubDate>
				<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[rCisTag]]></category>
		<category><![CDATA[REST API]]></category>
		<category><![CDATA[SOAP]]></category>
		<category><![CDATA[REST]]></category>
		<category><![CDATA[TechGenix]]></category>
		<guid isPermaLink="false">http://www.lucd.info/?p=5782</guid>

					<description><![CDATA[As most of you might know by now, VMware is moving away from [&#8230;]]]></description>
										<content:encoded><![CDATA[<p>As most of you might know by now, VMware is moving away from SOAP and going to <a href="https://blogs.vmware.com/code/2017/02/02/getting-started-vsphere-automation-sdk-rest/" target="_blank" rel="noopener">REST API</a>.</p>
<p>Is this something you should know about? Yes, you should!</p>
<p>In a two-part article on <a href="https://techgenix.com/" target="_blank" rel="noopener">TechGenix</a>, I wrote down my thoughts and observations on REST API. The article goes into what this move towards the REST API might mean for you as a scripter/administrator.</p>
<p><img loading="lazy" decoding="async" class="wp-image-5784 size-full alignnone" src="https://www.lucd.info/wp-content/uploads/2018/02/rCisTag.png" alt="" width="351" height="434" srcset="https://www.lucd.info/wp-content/uploads/2018/02/rCisTag.png 351w, https://www.lucd.info/wp-content/uploads/2018/02/rCisTag-243x300.png 243w" sizes="auto, (max-width: 351px) 100vw, 351px" /></p>
<p>&nbsp;</p>
<p>Since an article on coding, without a coding example doesn&#8217;t make much sense, I added a module, named <a href="https://github.com/vmware/PowerCLI-Example-Scripts/tree/master/Modules/rCisTag" target="_blank" rel="noopener">rCisTag</a> on the <a href="https://github.com/vmware/PowerCLI-Example-Scripts" target="_blank" rel="noopener">PowerCLI Examples repository</a>.</p>
<h2>Articles</h2>
<p>Enjoy reading <strong>Part 1</strong>, <a href="https://techgenix.com/vmware-rest-api/" target="_blank" rel="noopener">Understanding the VMware REST API interface</a>!</p>
<p>and <strong>Part 2</strong>, <a href="https://techgenix.com/soap-vs-rest-vmware-environments/" target="_blank" rel="noopener">SOAP vs REST for performing tasks in VMware environments</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.lucd.info/2018/02/22/powercli-rest-api-sample-module-tag-management/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>Invoke-VMScriptPlus</title>
		<link>https://www.lucd.info/2017/09/14/invoke-vmscriptplus/</link>
					<comments>https://www.lucd.info/2017/09/14/invoke-vmscriptplus/#comments</comments>
		
		<dc:creator><![CDATA[LucD]]></dc:creator>
		<pubDate>Thu, 14 Sep 2017 10:55:32 +0000</pubDate>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[OBN]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[SER1875BU]]></category>
		<category><![CDATA[Bash]]></category>
		<category><![CDATA[Invoke-VMScript]]></category>
		<category><![CDATA[Perl]]></category>
		<category><![CDATA[PowerCLI]]></category>
		<category><![CDATA[Python]]></category>
		<guid isPermaLink="false">http://www.lucd.info/?p=5649</guid>

					<description><![CDATA[The Invoke-VMScript cmdlet is definitely one of the PowerCLI cmdlets that is indispensable [&#8230;]]]></description>
										<content:encoded><![CDATA[<p>The <a href="https://www.vmware.com/support/developer/PowerCLI/PowerCLI65R1/html/Invoke-VMScript.html" target="_blank" rel="noopener">Invoke-VMScript</a> cmdlet is definitely one of the <a href="https://communities.vmware.com/community/vmtn/automationtools/powercli" target="_blank" rel="noopener">PowerCLI</a> cmdlets that is indispensable when you need to do things inside the <strong>Guest OS</strong> of your VMs.</p>
<p>When you are interacting with a <strong>Windows</strong> based Guest OS you can run old-fashioned <strong>BAT</strong> files or use <strong>PowerShell</strong> scripts. When the Guest OS is <strong>Linux</strong> based, you currently only can run <strong>Bash</strong> scripts.</p>
<p>Most Linux flavours have a feature that is called <a href="https://en.wikipedia.org/wiki/Shebang_(Unix)" target="_blank" rel="noopener">SheBang</a>, and which allows you to specify in the first line of your bash script, which interpreter shall be used to run the following lines of the script. Unfortunately, the current <a href="https://www.vmware.com/support/developer/PowerCLI/PowerCLI65R1/html/Invoke-VMScript.html" target="_blank" rel="noopener">Invoke-VMScript</a> cmdlet doesn&#8217;t allow one to use that feature.</p>
<p><a href="https://www.lucd.info/2017/09/14/invoke-vmscriptplus/invokevmscriptplu/" rel="attachment wp-att-5653"><img loading="lazy" decoding="async" class="alignnone wp-image-5653 size-large" src="https://www.lucd.info/wp-content/uploads/2017/09/invokevmscriptplu-1024x525.jpg" alt="" width="770" height="395" srcset="https://www.lucd.info/wp-content/uploads/2017/09/invokevmscriptplu-1024x525.jpg 1024w, https://www.lucd.info/wp-content/uploads/2017/09/invokevmscriptplu-300x154.jpg 300w, https://www.lucd.info/wp-content/uploads/2017/09/invokevmscriptplu-768x394.jpg 768w, https://www.lucd.info/wp-content/uploads/2017/09/invokevmscriptplu-720x369.jpg 720w, https://www.lucd.info/wp-content/uploads/2017/09/invokevmscriptplu.jpg 1152w" sizes="auto, (max-width: 770px) 100vw, 770px" /></a></p>
<p>Time to tackle that issue, and expand the possibilities for all VMs that have a Linux-based Guest OS. So I decided to write my Invoke-VMScriptPlus function.</p>
<p><span style="background-color: #ffff00;"><strong>Update October 14th 2017</strong></span></p>
<ul>
<li>Added here-document bash sample</li>
</ul>
<p><span id="more-5649"></span></p>
<p>When the <a href="https://powercli.ideas.aha.io/" target="_blank" rel="noopener">PowerCLI Feature Request</a> website was <a href="https://twitter.com/kmruddy/status/903043133004431360" target="_blank" rel="noopener">announced</a> during VMworld session <strong>#SER2529BU</strong>, it was no surprise to me, to rather quickly see a request appearing to support other languages, besides bash.</p>
<p><a href="https://www.lucd.info/2017/09/14/invoke-vmscriptplus/phyton-support/" rel="attachment wp-att-5652"><img loading="lazy" decoding="async" class="alignnone wp-image-5652 size-medium" src="https://www.lucd.info/wp-content/uploads/2017/09/phyton-support-300x72.jpg" alt="" width="300" height="72" srcset="https://www.lucd.info/wp-content/uploads/2017/09/phyton-support-300x72.jpg 300w, https://www.lucd.info/wp-content/uploads/2017/09/phyton-support-768x183.jpg 768w, https://www.lucd.info/wp-content/uploads/2017/09/phyton-support-720x172.jpg 720w, https://www.lucd.info/wp-content/uploads/2017/09/phyton-support.jpg 980w" sizes="auto, (max-width: 300px) 100vw, 300px" /></a></p>
<p>First I did some tests, to check if I could get the Invoke-VMScript cmdlet to work with a SheBang line.</p><pre class="urvanov-syntax-highlighter-plain-tag">$vmName = 'ubuntu1'
$vm = Get-VM -Name $vmName

$codeBashPlain = @"
echo "Hello World!"
"@

$codeBashSheBang = @"
#!/usr/bin/env bash
echo "Hello World!"
"@

$sScript = @{
    VM = $vm
    GuestUser = 'lucd'
    GuestPassword = 'Just@Password1!'
    ScriptType = 'Bash'
    ScriptText = $codeBashPlain
}

Invoke-VMScript @sScript

$sScript['ScriptText'] = $codeBashSheBang
Invoke-VMScript @sScript</pre><p>Unfortunately it doesn&#8217;t.</p>
<p><a href="https://www.lucd.info/2017/09/14/invoke-vmscriptplus/script-out/" rel="attachment wp-att-5655"><img loading="lazy" decoding="async" class="alignnone wp-image-5655 size-medium" src="https://www.lucd.info/wp-content/uploads/2017/09/script-out-300x95.png" alt="" width="300" height="95" srcset="https://www.lucd.info/wp-content/uploads/2017/09/script-out-300x95.png 300w, https://www.lucd.info/wp-content/uploads/2017/09/script-out-768x244.png 768w, https://www.lucd.info/wp-content/uploads/2017/09/script-out-720x228.png 720w, https://www.lucd.info/wp-content/uploads/2017/09/script-out.png 939w" sizes="auto, (max-width: 300px) 100vw, 300px" /></a></p>
<p>When a cmdlet doesn&#8217;t do what you are trying to achieve, there is always the <a href="https://code.vmware.com/apis/196/vsphere#/doc/vim.vm.guest.ProcessManager.ProgramSpec.html" target="_blank" rel="noopener">vSphere API</a> that can help. The <a href="https://code.vmware.com/apis/196/vsphere#/doc/vim.vm.guest.GuestOperationsManager.html" target="_blank" rel="noopener">GuestOperationsManager</a> is the place to look. From there we can access methods to start and monitor a process in the guest  and to handle files inside the guest OS.<br />
The <a href="https://code.vmware.com/apis/196/vsphere#/doc/vim.vm.guest.ProcessManager.html#startProgram" target="_blank" rel="noopener">StartProgramInGuest</a> is the central method of the <strong>Invoke-VMScriptPlus</strong> function. The function uses the <strong>arguments</strong> property on the <a href="https://code.vmware.com/apis/196/vsphere#/doc/vim.vm.guest.ProcessManager.ProgramSpec.html" target="_blank" rel="noopener">GuestProgramSpec</a> object to <strong>redirect the stdio</strong> of the process.</p>
<p>The following flow-chart shows a high-level view of the logic that is used in the <strong>Invoke-VMScriptPlus</strong> function, and shows which method is used at which point.</p>
<p><a href="https://www.lucd.info/2017/09/14/invoke-vmscriptplus/invoke-vmscriptplus-flow/" rel="attachment wp-att-5656"><img loading="lazy" decoding="async" class="alignnone wp-image-5656 size-large" src="https://www.lucd.info/wp-content/uploads/2017/09/invoke-vmscriptplus-flow-1024x458.jpg" alt="" width="770" height="344" srcset="https://www.lucd.info/wp-content/uploads/2017/09/invoke-vmscriptplus-flow-1024x458.jpg 1024w, https://www.lucd.info/wp-content/uploads/2017/09/invoke-vmscriptplus-flow-300x134.jpg 300w, https://www.lucd.info/wp-content/uploads/2017/09/invoke-vmscriptplus-flow-768x344.jpg 768w, https://www.lucd.info/wp-content/uploads/2017/09/invoke-vmscriptplus-flow-720x322.jpg 720w, https://www.lucd.info/wp-content/uploads/2017/09/invoke-vmscriptplus-flow.jpg 1772w" sizes="auto, (max-width: 770px) 100vw, 770px" /></a></p>
<h2>The Code</h2>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">#requires -Version 5.0
#requires -Modules VMware.VimAutomation.Core

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 | %{
                if($_ -is [String])
                {
                    return (Invoke-Expression -Command "Get-$($this.Type.Replace('VirtualMachine','VM')) -Name `$_")
                }
                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
.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
.PARAMETER CRLF
  Switch to indicate of the NL that is returned by Linux, shall be
  converted to a CRLF
.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.  
.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
#&gt;    
    [cmdletbinding()]    
    param(
        [parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [MyOBN('VirtualMachine')]
        [VMware.VimAutomation.ViCore.Types.V1.Inventory.VirtualMachine[]]$VM,
        [Parameter(Mandatory=$true,ParameterSetName='PlainText')]
        [String]$GuestUser,
        [Parameter(Mandatory=$true,ParameterSetName='PlainText')]
        [String]$GuestPassword,
        [Parameter(Mandatory=$true,ParameterSetName='PSCredential')]
        [PSCredential[]]$GuestCredential,
        [Parameter(Mandatory=$true)]
        [String]$ScriptText,
        [Parameter(Mandatory=$true)]
        [ValidateSet('bash','perl','python3','nodejs','php','lua')]
        [String]$ScriptType,
        [Switch]$CRLF,
        [MyOBN('VIServer')]
        [VMware.VimAutomation.ViCore.Types.V1.VIServer]$Server = $global:DefaultVIServer

    )

    Begin
    {
        $si = Get-View ServiceInstance
        $guestMgr = Get-View -Id $si.Content.GuestOperationsManager
        $gFileMgr = Get-View -Id $guestMgr.FileManager
        $gProcMgr = Get-View -Id $guestMgr.ProcessManager

        $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'
        }
    }

    Process
    {
        foreach($vmInstance in $VM){
            # Preamble
            if($vmInstance.PowerState -ne 'PoweredOn')
            {
                Write-Error "VM $($vmInstance.Name) is not powered on"
                continue
            }
            if($vmInstance.ExtensionData.Guest.ToolsRunningStatus -ne 'guestToolsRunning')
            {
                Write-Error "VMware Tools are not running on VM $($vmInstance.Name)"
                continue
            }

            $moref = $vmInstance.ExtensionData.MoRef

            # Test if code contains a SheBang, otherwise add it
            $targetCode = $shebangTab[$ScriptType]
            if($ScriptText -notmatch "^$($targetCode)"){
                $ScriptText = "$($targetCode)`n`r$($ScriptText)"
            }
    
            # Create Authentication Object (User + Password)
            
            if($PSCmdlet.ParameterSetName -eq 'PSCredential')
            {
                $GuestUser = $GuestCredential.GetNetworkCredential().username
                $GuestPassword = $GuestCredential.GetNetworkCredential().password
            }
    
            $auth = New-Object VMware.Vim.NamePasswordAuthentication
            $auth.InteractiveSession = $false
            $auth.Username = $GuestUser
            $auth.Password = $GuestPassword
            
            # Copy script to temp file in guest
            
            # Create temp file for script
            Try{
                $tempFile = $gFileMgr.CreateTemporaryFileInGuest($moref,$auth,"$($env:USERNAME)_","_$($PID)",'/tmp')
            }
            Catch{
                Throw "$error[0].Exception.Message"
            }
            
            # Create temp file for output
            Try{
                $tempOutput = $gFileMgr.CreateTemporaryFileInGuest($moref,$auth,"$($env:USERNAME)_","_$($PID)_output",'/tmp')
            }
            Catch{
                Throw "$error[0].Exception.Message"
            }
           
            # Copy script to temp file
            $lCode = $ScriptText.Split("`r") -join ''
            $attr = New-Object VMware.Vim.GuestFileAttributes
            $clobber = $true
            $filePath = $gFileMgr.InitiateFileTransferToGuest($moref,$auth,$tempFile,$attr,$lCode.Length,$clobber)
            $copyResult = Invoke-WebRequest -Uri $filePath -Method Put -Body $lCode
            
            if($copyResult.StatusCode -ne 200)
            {
                Throw "ScripText copy failed!`rStatus $($copyResult.StatusCode)`r$(($copyResult.Content | %{[char]$_}) -join '')"
            }
                
            # Make temp file executable
            $spec = New-Object VMware.Vim.GuestProgramSpec
            $spec.Arguments = "751 $($tempFile.Split('/')[-1])"
            $spec.ProgramPath = '/bin/chmod'
            $spec.WorkingDirectory = '/tmp'
            Try{
                $procId = $gProcMgr.StartProgramInGuest($moref,$auth,$spec)
            }
            Catch{
                Throw "$error[0].Exception.Message"
            }
            
            # Run temp file
            
            $spec = New-Object VMware.Vim.GuestProgramSpec
            $spec.Arguments = " &gt; $($tempOutput)"
            $spec.ProgramPath = "$($tempFile)"
            $spec.WorkingDirectory = '/tmp'
            Try{
                $procId = $gProcMgr.StartProgramInGuest($moref,$auth,$spec)
            }
            Catch{
                Throw "$error[0].Exception.Message"
            }
            
            # Wait for script to finish
            Try{
                $pInfo = $gProcMgr.ListProcessesInGuest($moref,$auth,@($procId))
                while($pInfo.EndTime -eq $null){
                    sleep 1
                    $pInfo = $gProcMgr.ListProcessesInGuest($moref,$auth,@($procId))
                }
            }
            Catch{
                Throw "$error[0].Exception.Message"
            }

            # Retrieve output from script
            
            $fileInfo = $gFileMgr.InitiateFileTransferFromGuest($moref,$auth,$tempOutput)
            $fileContent = Invoke-WebRequest -Uri $fileInfo.Url -Method Get
            if($fileContent.StatusCode -ne 200)
            {
                Throw "Retrieve of script output failed!`rStatus $($fileContent.Status)`r$(($fileContent.Content | %{[char]$_}) -join '')"
            }
            
            # Clean up

            # Remove output file
            $gFileMgr.DeleteFileInGuest($moref,$auth,$tempOutput)
            
            # Remove temp script file
            $gFileMgr.DeleteFileInGuest($moref,$auth,$tempFile)
    
            New-Object PSObject -Property @{
                VM = $vmInstance
                ScriptOutput = &amp;{
                    $out = ($fileContent.Content | %{[char]$_}) -join ''
                    if($CRLF)
                    {
                        $out.Replace("`n","`n`r")
                    }
                    else
                    {
                        $out
                    }
                }
                Pid = $procId
                PidOwner = $pInfo.Owner
                Start = $pInfo.StartTime
                Finish = $pInfo.EndTime
                ExitCode = $pInfo.ExitCode
                ScriptType = $ScriptType
                ScriptText = $ScriptText
            }
        }
    }
}</pre><p></p>
<h3>Annotations</h3>
<p><strong>Line 1</strong>: The function requires PowerShell v5 or higher</p>
<p><strong>Line 2</strong>: The function requires the PowerCLI Core module</p>
<p><strong>Line 4-48</strong>: The latest version of my OBN (Object By Name) class. It allows one to pass or the actual .Net object, or the name of the object, as an argument to a parameter. See also <a href="https://www.lucd.info/2017/05/30/home-made-obn/" target="_blank" rel="noopener">Home Made OBN</a></p>
<p><strong>Line 132</strong>: Most Linux OS return output with only a LF. This switch can be used to convert the LF to a CRLF, when the resulting output of the script is returned.</p>
<p><strong>Line 145-152</strong>: A hard-coded table with the supported interpreters, and their corresponding SheBang line.</p>
<p><strong>Line 159-168</strong>: If the VM is not powered on, or if the VMware Tools are not running, the function will return with a result.</p>
<p><strong>Line 173-176</strong>: Tests if there is a SheBang line in the ScriptText. If not, it will add a line based on the ScriptType value.</p>
<p><strong>Line 193-207</strong>: The function uses two temporary files to store the script and the script&#8217;s output.</p>
<p><strong>Line 210-219</strong>: The ScriptText is copied to the temporary file. This is done over HTTPS with the Invoke-WebRequest cmdlet.</p>
<p><strong>Line 222-231</strong>: The file containing the ScriptText needs to be made &#8220;executable&#8221;</p>
<p><strong>Line 235-256</strong>: Script execution is started, and the function waits till the process completes.</p>
<p><strong>Line 236</strong>: The function uses the Arguments property to redirect stdio to the second temporary file</p>
<p><strong>Line 260-265</strong>: The output is fetched, again with an Invoke-WebRequest.</p>
<p><strong>Line 270-273</strong>: Clean up the temporary files</p>
<p><strong>Line 275-295</strong>: Return an object containing the script output and further info about the script execution</p>
<h2>Sample Use</h2>
<p>The use of the function Invoke-VMScriptPlus is quite similar to the use of the original Invoke-VMScript.</p>
<p>Some examples.</p>
<h3>Bash</h3>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">$codeBash = @"
#!/usr/bin/env bash
echo "Hello World!"
"@

$sCode = @{
  VM = $vm
  GuestCredential = $cred
  ScriptTYpe = 'bash'
  ScriptText = $codeBash
}

Invoke-VMScriptPlus @sCode</pre><p>And the result</p>
<p><a href="https://www.lucd.info/2017/09/14/invoke-vmscriptplus/bash-out/" rel="attachment wp-att-5658"><img loading="lazy" decoding="async" class="alignnone wp-image-5658 size-medium" src="https://www.lucd.info/wp-content/uploads/2017/09/bash-out-300x218.png" alt="" width="300" height="218" srcset="https://www.lucd.info/wp-content/uploads/2017/09/bash-out-300x218.png 300w, https://www.lucd.info/wp-content/uploads/2017/09/bash-out.png 542w" sizes="auto, (max-width: 300px) 100vw, 300px" /></a></p>
<h4>Here document</h4>
<p>One feature that is often used in bash, is the so-called <a href="https://tldp.org/LDP/abs/html/here-docs.html" target="_blank" rel="noopener">here document</a>.</p>
<p>Unfortunately the <a href="https://vdc-repo.vmware.com/vmwb-repository/dcr-public/de211814-fded-41f0-bc26-c70cb7b8a9e9/076370ed-067b-4740-be9c-835da7485932/doc/Invoke-VMScript.html" target="_blank" rel="noopener">Invoke-VMScript</a> cmdlet doesn&#8217;t seem to support that feature for bash scripts. Code like this &#8230;</p><pre class="urvanov-syntax-highlighter-plain-tag">$code = @"
rm /tmp/test.txt
cat &gt; /tmp/test.txt &lt;&lt; EOF
Line 1
Line 2
EOF
ls -l /tmp/test.txt
echo File content
echo ------------
cat /tmp/test.txt
echo ------------
"@

$sINvoke = @{
    VM = Get-VM -Name $vmName
    ScriptType = 'bash'
    ScriptText = $code
    GuestUser = $user
    GuestPassword = $pswd
}
Invoke-VMScript @sINvoke | select -ExpandProperty ScriptOutput</pre><p>&#8230; produces an error like this.</p>
<p><a href="https://www.lucd.info/2017/09/14/invoke-vmscriptplus/error/" rel="attachment wp-att-5673"><img loading="lazy" decoding="async" class="alignnone size-full wp-image-5673" src="https://www.lucd.info/wp-content/uploads/2017/09/error.jpg" alt="" width="615" height="66" srcset="https://www.lucd.info/wp-content/uploads/2017/09/error.jpg 615w, https://www.lucd.info/wp-content/uploads/2017/09/error-300x32.jpg 300w" sizes="auto, (max-width: 615px) 100vw, 615px" /></a></p>
<p>But due to the way the <strong>Invoke-VMScriptPlus</strong> function transfers the script text to the guest, this feature is working without a glitch.</p>
<p>The same code as above, except that <strong>Invoke-VMScript</strong> is replaced by <strong>Invoke-VMScriptPlus</strong>.</p><pre class="urvanov-syntax-highlighter-plain-tag">$code = @"
rm /tmp/test.txt
cat &gt; /tmp/test.txt &lt;&lt; EOF
Line 1
Line 2
EOF
ls -l /tmp/test.txt
echo File content
echo ------------
cat /tmp/test.txt
echo ------------
"@

$sINvoke = @{
    VM = Get-VM -Name $vmName
    ScriptType = 'bash'
    ScriptText = $code
    GuestUser = $user
    GuestPassword = $pswd
}
Invoke-VMScriptPlus @sInvoke | select -ExpandProperty ScriptOutput</pre><p>&#8230; now produces the expected result.</p>
<p><a href="https://www.lucd.info/2017/09/14/invoke-vmscriptplus/ok/" rel="attachment wp-att-5674"><img loading="lazy" decoding="async" class="alignnone size-full wp-image-5674" src="https://www.lucd.info/wp-content/uploads/2017/09/ok.jpg" alt="" width="413" height="94" srcset="https://www.lucd.info/wp-content/uploads/2017/09/ok.jpg 413w, https://www.lucd.info/wp-content/uploads/2017/09/ok-300x68.jpg 300w" sizes="auto, (max-width: 413px) 100vw, 413px" /></a></p>
<h3>Python3</h3>
<p>In this example we are not adding the SheBang line in the ScriptText, but through the ScriptType value, the function will add this line.</p><pre class="urvanov-syntax-highlighter-plain-tag">$codePython = @"
print("Happy 10th Birthday PowerCLI!")
"@

$sCode = @{
 VM = $vm
 GuestCredential = $cred
 ScriptTYpe = 'python3'
 ScriptText = $codePython
}

Invoke-VMScriptPlus @sCode</pre><p>And the result.<br />
Notice how the Invoke-VMScriptPlus function added the SheBang line.</p>
<p><a href="https://www.lucd.info/2017/09/14/invoke-vmscriptplus/python-out/" rel="attachment wp-att-5659"><img loading="lazy" decoding="async" class="alignnone wp-image-5659 size-medium" src="https://www.lucd.info/wp-content/uploads/2017/09/python-out-300x134.png" alt="" width="300" height="134" srcset="https://www.lucd.info/wp-content/uploads/2017/09/python-out-300x134.png 300w, https://www.lucd.info/wp-content/uploads/2017/09/python-out-768x342.png 768w, https://www.lucd.info/wp-content/uploads/2017/09/python-out-720x321.png 720w, https://www.lucd.info/wp-content/uploads/2017/09/python-out.png 799w" sizes="auto, (max-width: 300px) 100vw, 300px" /></a></p>
<h3>Other</h3>
<p>During the VMworld Breakout session, where this function was first demonstrated, we showed additional examples in the following video.</p>
<p><iframe loading="lazy" width="640" height="361" src="https://player.vimeo.com/video/300485769" frameborder="0" webkitallowfullscreen="webkitallowfullscreen" mozallowfullscreen="mozallowfullscreen" allowfullscreen="allowfullscreen"></iframe></p>
<p>Since there are many Linux flavours out there, and since I obviously couldn&#8217;t test them all, I would appreciate it if you can send me feedback about which Linux flavours/versions work, and which don&#8217;t.</p>
<p>If there are requests for other languages, feel free to forward me your requests.</p>
<p>&nbsp;</p>
<p>Enjoy!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.lucd.info/2017/09/14/invoke-vmscriptplus/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
			</item>
		<item>
		<title>Home Made OBN</title>
		<link>https://www.lucd.info/2017/05/30/home-made-obn/</link>
					<comments>https://www.lucd.info/2017/05/30/home-made-obn/#comments</comments>
		
		<dc:creator><![CDATA[LucD]]></dc:creator>
		<pubDate>Tue, 30 May 2017 18:24:16 +0000</pubDate>
				<category><![CDATA[Custom Attribute]]></category>
		<category><![CDATA[Function]]></category>
		<category><![CDATA[OBN]]></category>
		<category><![CDATA[PowerShell]]></category>
		<category><![CDATA[Attribute]]></category>
		<category><![CDATA[Parameter]]></category>
		<guid isPermaLink="false">http://www.lucd.info/?p=5606</guid>

					<description><![CDATA[It is no secret that PowerCLI has lots of amazing options and well-thought trough [&#8230;]]]></description>
										<content:encoded><![CDATA[<p>It is no secret that <a href="https://communities.vmware.com/community/vmtn/automationtools/powercli/overview" target="_blank" rel="noopener noreferrer">PowerCLI</a> has lots of amazing options and well-thought trough features. But there is one feature that most of PowerCLI&#8217;s users take for granted, and most probably do not even realise that they are using what is called <a href="https://www.vmware.com/support/developer/PowerCLI/PowerCLI651/html/about_obn.html" target="_blank" rel="noopener noreferrer">Object By Name</a> or <a href="https://www.vmware.com/support/developer/PowerCLI/PowerCLI651/html/about_obn.html" target="_blank" rel="noopener noreferrer">OBN</a>. In this post I&#8217;ll show you one way of creating your own OBN, a home made Object By Name, which you can use in your own functions and modules.</p>
<p><a href="https://www.lucd.info/2017/05/30/home-made-obn/myobn-title/" rel="attachment wp-att-5611"><img loading="lazy" decoding="async" class="alignnone wp-image-5611 size-medium" title="MyOBN" src="https://www.lucd.info/wp-content/uploads/2017/05/MyOBN-title-300x294.png" alt="" width="300" height="294" srcset="https://www.lucd.info/wp-content/uploads/2017/05/MyOBN-title-300x294.png 300w, https://www.lucd.info/wp-content/uploads/2017/05/MyOBN-title-265x260.png 265w, https://www.lucd.info/wp-content/uploads/2017/05/MyOBN-title.png 305w" sizes="auto, (max-width: 300px) 100vw, 300px" /></a></p>
<p><strong>OBN</strong> allows you to refer to a PowerCLI object by <strong>name, </strong>instead of passing an actual PowerCLI object. A classic example is &#8220;<em>Get-VM -Datastore DS1</em>&#8220;, where we are retrieving all VMs that live on a specific datastore. If one looks at the description of the <a href="https://www.vmware.com/support/developer/PowerCLI/PowerCLI651/html/Get-VM.html" target="_blank" rel="noopener noreferrer">Datastore</a> parameter, it clearly states that a value of type <a href="https://www.vmware.com/support/developer/PowerCLI/PowerCLI651/html/StorageResource.html" target="_blank" rel="noopener noreferrer">StorageResource</a> is expected, but we are able to provide the datastorename, a <strong>string</strong>. Under the cover, PowerCLI converts this string to the required PowerCLI object for the parameter.</p>
<p>When we are writing our own functions, it would be very handy to have the same functionality at our disposal. Define a parameter to be of the type of a PowerCLI object, but then be able to pass the name of the object, instead of the object itself.<br />
The solution is here, with the <strong>MyOBN</strong> attribute. We now have the same functionality available, that was until now only available for PowerCLI cmdlets.</p>
<p><span style="background-color: #ffff00;"><strong>Update September 14th 2017</strong></span></p>
<ul>
<li>Added support for VIServer</li>
<li>Added support for arrays of objects</li>
<li>Fixed an issue with the VirtualMachine object</li>
</ul>
<p><span id="more-5606"></span></p>
<h2>Some History</h2>
<p>To have a better understanding, I&#8217;ll show some example functions.</p>
<p>The first function, Invoke-Test1, uses a Datastore parameter, that accepts a PowerCLI Datastore object.</p><pre class="urvanov-syntax-highlighter-plain-tag">Function Invoke-Test1{
    param(
        [VMware.VimAutomation.ViCore.Types.V1.DatastoreManagement.Datastore]$Datastore
    )

    Process{
        $Datastore | Select Name,CapacityGB
    }
}

$dsName = 'DS1'
$ds = Get-Datastore -Name $dsName

Invoke-Test1 -Datastore $ds</pre><p>If we execute this, we get the Name of the VMs that live on the datastore.</p>
<p><a href="https://www.lucd.info/wp-content/uploads/2017/05/invoke-test1.png" rel="attachment wp-att-5618"><img loading="lazy" decoding="async" class="alignnone wp-image-5618 size-medium" title="Invoke-Test1 with Datastore object" src="https://www.lucd.info/wp-content/uploads/2017/05/invoke-test1-300x235.png" alt="" width="300" height="235" srcset="https://www.lucd.info/wp-content/uploads/2017/05/invoke-test1-300x235.png 300w, https://www.lucd.info/wp-content/uploads/2017/05/invoke-test1-768x601.png 768w, https://www.lucd.info/wp-content/uploads/2017/05/invoke-test1-720x564.png 720w, https://www.lucd.info/wp-content/uploads/2017/05/invoke-test1.png 963w" sizes="auto, (max-width: 300px) 100vw, 300px" /></a></p>
<p>But if we try to use the name of the datastore, as we are used to doing with the <strong>Datastore</strong> parameter on the <a href="https://www.vmware.com/support/developer/PowerCLI/PowerCLI651/html/Get-VM.html" target="_blank" rel="noopener noreferrer">Get-VM</a> cmdlet for example, we get an error.</p>
<p><a href="https://www.lucd.info/wp-content/uploads/2017/05/invoke-test1-b.png" rel="attachment wp-att-5619"><img loading="lazy" decoding="async" class="alignnone wp-image-5619 size-medium" title="Invoke-Test1 with String object" src="https://www.lucd.info/wp-content/uploads/2017/05/invoke-test1-b-300x243.png" alt="" width="300" height="243" srcset="https://www.lucd.info/wp-content/uploads/2017/05/invoke-test1-b-300x243.png 300w, https://www.lucd.info/wp-content/uploads/2017/05/invoke-test1-b-768x621.png 768w, https://www.lucd.info/wp-content/uploads/2017/05/invoke-test1-b-720x583.png 720w, https://www.lucd.info/wp-content/uploads/2017/05/invoke-test1-b.png 959w" sizes="auto, (max-width: 300px) 100vw, 300px" /></a></p>
<p>To get our function to behave more like OBN on the PowerCLI cmdlets, we can define the parameter as a general <strong>PSObject</strong> object. And perform the type casting inside the function. Something like this.</p><pre class="urvanov-syntax-highlighter-plain-tag">Function Invoke-Test2{
    param(
        [PSObject]$Datastore
    )

    Process{
        if($Datastore -is [System.String]){
            $Datastore = Get-Datastore -Name $Datastore
        }

        Get-VM -Datastore $Datastore | select Name
    }
}

$dsName = 'DS1'
$ds = Get-Datastore -Name $dsName

Invoke-Test2 -Datastore $ds</pre><p>Our function now works with a Datastore object</p>
<p><a href="https://www.lucd.info/wp-content/uploads/2017/05/invoke-test2.png" rel="attachment wp-att-5620"><img loading="lazy" decoding="async" class="alignnone wp-image-5620 size-medium" title="Invoke-Test2 with Datastore object" src="https://www.lucd.info/wp-content/uploads/2017/05/invoke-test2-300x248.png" alt="" width="300" height="248" srcset="https://www.lucd.info/wp-content/uploads/2017/05/invoke-test2-300x248.png 300w, https://www.lucd.info/wp-content/uploads/2017/05/invoke-test2-768x636.png 768w, https://www.lucd.info/wp-content/uploads/2017/05/invoke-test2-720x596.png 720w, https://www.lucd.info/wp-content/uploads/2017/05/invoke-test2.png 961w" sizes="auto, (max-width: 300px) 100vw, 300px" /></a></p>
<p>but also with a String.</p>
<p><a href="https://www.lucd.info/wp-content/uploads/2017/05/invoke-test2-b.png" rel="attachment wp-att-5621"><img loading="lazy" decoding="async" class="alignnone wp-image-5621 size-medium" title="Invoke-Test2 with String object" src="https://www.lucd.info/wp-content/uploads/2017/05/invoke-test2-b-300x248.png" alt="" width="300" height="248" srcset="https://www.lucd.info/wp-content/uploads/2017/05/invoke-test2-b-300x248.png 300w, https://www.lucd.info/wp-content/uploads/2017/05/invoke-test2-b-768x635.png 768w, https://www.lucd.info/wp-content/uploads/2017/05/invoke-test2-b-720x595.png 720w, https://www.lucd.info/wp-content/uploads/2017/05/invoke-test2-b.png 959w" sizes="auto, (max-width: 300px) 100vw, 300px" /></a></p>
<p>Note that this way of simulating the OBN functionality is far from fool-proof. There is for example no strict type checking, we could pass a VirtualMachine object on the Datastore parameter, and our function would throw an error.</p>
<p>When I first read about <a href="https://msdn.microsoft.com/en-us/library/system.attribute(v=vs.110).aspx" target="_blank" rel="noopener noreferrer">Custom Attributes</a>, in a blog post, named <a href="https://kevinmarquette.github.io/2017-02-20-Powershell-creating-parameter-validators-and-transforms/" target="_blank" rel="noopener noreferrer">Powershell: Creating and using custom attributes</a>, from <a href="https://twitter.com/KevinMarquette" target="_blank" rel="noopener noreferrer">Kevin Marquette</a>, I immediately saw the potential to get one step closer to a proper OBN in my functions.</p>
<p>For now, I have implemented the <strong>Attribute</strong> as a <a href="https://msdn.microsoft.com/en-us/powershell/wmf/5.0/class_newtype" target="_blank" rel="noopener noreferrer">class</a>, which implies it&#8217;s use is limited to <strong>PowerShell v5</strong> or higher.</p>
<h2>The Code</h2>
<p></p><pre class="urvanov-syntax-highlighter-plain-tag">#requires -Version 5.0

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 | %{
                if($_ -is [String])
                {
                    return (Invoke-Expression -Command "Get-$($this.Type.Replace('VirtualMachine','VM')) -Name `$_")
                }
                elseif($_.GetType().Name -match "$($this.Type)Impl")
                {
                    $_
                }
            })
        }
        throw [System.IO.FileNotFoundException]::New()
    }
}</pre><p>&nbsp;</p>
<h3>Annotations</h3>
<p><strong>Line 1</strong>: this code requires PowerShell v5 or up</p>
<p><strong>Line 3</strong>: we base the MyOBN class on the <a href="https://msdn.microsoft.com/en-us/library/system.management.automation.argumenttransformationattribute(v=vs.85).aspx" target="_blank" rel="noopener noreferrer">ArgumentTransformationAttribute</a> class. We inherit the <a href="https://msdn.microsoft.com/en-us/library/system.management.automation.argumenttransformationattribute.transform(v=vs.85).aspx" target="_blank" rel="noopener noreferrer">Transform</a> method from this class. Our override of the Transform method will contain the logic to simulate the OBN functionality.</p>
<p><strong>Line 5-8</strong>: currently the Transform method in the MyOBN class supports these PowerCLI types.</p>
<p><strong>Line 11</strong>: We need a way to tell the Transform method, which objecttype we want to use. That&#8217;s why we add a property Type to the MyOBN class.</p>
<p><strong>Line 9-12</strong>: the class has a constructor which allows us to initiate the Type property.</p>
<p><strong>Line 13-44</strong>: the Transform method override is where all the magic happens</p>
<p><strong>Line 15</strong>: we need to detect if the parameter is a String, in other words if the parameter was passed as an &#8220;Object By Name&#8221;.</p>
<p><strong>Line 21</strong>: with the help of the Type property we invoke the correct PowerCLI cmdlet to convert the String to a PowerCLI obejct</p>
<p><strong>Line 26-29</strong>: if the value passed to the parameter was already a PowerCLI object, we just pass that object along</p>
<p><strong>Line 30-42</strong>: These lines handle the request for an <strong>array</strong> of supported objects</p>
<h2>Sample Runs</h2>
<p>To use this new parameter attribute is quite straightforward. Make sure the MyOBN class is loaded (this can be through dot-sourcing, adding it to your profile or just including it in your .ps1 file).</p>
<p>Then add the MyOBN attribute like any other parameter attribute. Make sure to specify the &#8220;Type&#8221; for which you want MyOBN to work. Like this</p><pre class="urvanov-syntax-highlighter-plain-tag">function Invoke-Test3{
  [CmdletBinding()]
  param(
    [MyOBN('Datastore')]
    [VMware.VimAutomation.ViCore.Types.V1.DatastoreManagement.Datastore]$Datastore
  )

  Process{
    Get-VM -Datastore $Datastore
  }
}</pre><p>We can now call our function with a Datastore object</p>
<p><a href="https://www.lucd.info/wp-content/uploads/2017/05/invoke-test3.png" rel="attachment wp-att-5622"><img loading="lazy" decoding="async" class="alignnone wp-image-5622 size-medium" title="Invoke-Test3 with Datastore object" src="https://www.lucd.info/wp-content/uploads/2017/05/invoke-test3-300x246.png" alt="" width="300" height="246" srcset="https://www.lucd.info/wp-content/uploads/2017/05/invoke-test3-300x246.png 300w, https://www.lucd.info/wp-content/uploads/2017/05/invoke-test3-768x631.png 768w, https://www.lucd.info/wp-content/uploads/2017/05/invoke-test3-720x591.png 720w, https://www.lucd.info/wp-content/uploads/2017/05/invoke-test3.png 962w" sizes="auto, (max-width: 300px) 100vw, 300px" /></a></p>
<p>but also with a String that contains the name of the Datastore.</p>
<p><a href="https://www.lucd.info/wp-content/uploads/2017/05/invoke-test3-b.png" rel="attachment wp-att-5623"><img loading="lazy" decoding="async" class="alignnone wp-image-5623 size-medium" title="Invoke-Test3 with String object" src="https://www.lucd.info/wp-content/uploads/2017/05/invoke-test3-b-300x242.png" alt="" width="300" height="242" srcset="https://www.lucd.info/wp-content/uploads/2017/05/invoke-test3-b-300x242.png 300w, https://www.lucd.info/wp-content/uploads/2017/05/invoke-test3-b-768x619.png 768w, https://www.lucd.info/wp-content/uploads/2017/05/invoke-test3-b-720x581.png 720w, https://www.lucd.info/wp-content/uploads/2017/05/invoke-test3-b.png 966w" sizes="auto, (max-width: 300px) 100vw, 300px" /></a></p>
<p>And all that without adding any extra code in our function, besides the <strong>MyOBN</strong> attribute.</p>
<p>&nbsp;</p>
<p>Enjoy!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.lucd.info/2017/05/30/home-made-obn/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
	</channel>
</rss>
