PowerShell User Group Hamburg – PowerCLI Session

Letzte Woche hatte ich die Gelegenheit im Rahmen der PowerShell User Group Hamburg eine kleine PowerCLI Session zu machen. Ehrlich gesagt war ich etwas überracht, dass das Thema VMware PowerCLI so gut angenommen wurde. Nach meinem Empfinden liegt der Fokus doch ehr auf Windows und Azure Themen in den PowerShell User Groups.

PowerShell User Group Hamburg - PowerCLI Session

Alle Präsentationen der PowerShell User Group Hamburg sind im GitHub Repository- Presentations zu finden.

PowerCLI Session

Meine Präsentation der PowerCLI Session ist auf Slideshare zu finden, alle Code Snippets der Demos finden sich weiter unten in diesem Artikel oder natürlich auch im GitHub Repository der PowerShell User Group Hamburg.

Code Snippets

Vielen Dank an der Stelle auch noch einmal an Luc Dekens für die Inspiration durch die VMworld 2018 Session: [VIN1992BU] – A Deep(er) Dive with PowerCLI 10 und seine Invoke-VMScriptPlus Funktion.

# Init

## Import VMware Modules
Get-Module -Name VMware* -ListAvailable | Import-Module

## Connect vCenter
Connect-VIServer vcenter-01.lab.local

## Prepare VMs
$sourceVMName = "test"

For ($i=1; $i -le 20; $i++) {
    $newVMName = [String]($sourceVMName + $i)
    New-VM -Name $newVMName -VM $sourceVMName -VMHost "esxi-01.lab.local" -Datastore "vol_vmware_01" -DiskStorageFormat Thin
    }

# Perf - Methoden / Get-View mit Filter

## Init Array of VMs
$VMs = Get-VM 

## Where-Object Cmdlet
Measure-Command { $VMs | Where-Object {$_.Name -eq "test"} }

## Where Methode 
Measure-Command { $VMs.Where({$_.Name -eq "test"}) }

## Where Split 
$VMsOn, $VMsOff = $VMs.Where({$_.PowerState -eq "PoweredOn"}, "Split")
$VMsOn.Count
$VMsOff.Count

## Get-View mit Filter gegen Get-VM mit Where-Object
Measure-Command {Get-VM -Name "test" | fl}
Measure-Command {Get-View -ViewType VirtualMachine -Filter @{Name="^test"} | fl}
# PSDrive
[String]$LocalPath = "C:\Users\Markus\Google Drive\PSUG HH\VMW-ESX-6.0.0-igbn-1.4.6-offline_bundle-9909123.zip"
[String]$TempDatastoreName = "*local"
 
[Array]$VMhosts = Get-VMHost
 
 $VMhosts.ForEach({
    $Datastore =  $_| Get-Datastore -Name $TempDatastoreName
    $DatastoreDriveName = "HostStore_" + $_.Name.Split(".")[0]
    $Datastore | New-DatastoreDrive -Name $DatastoreDriveName | Out-Null
    Copy-DatastoreItem -Item $LocalPath -Destination $($DatastoreDriveName + ":\") -Force:$true -Confirm:$false

    Remove-PSDrive -Name $DatastoreDriveName
    })
# ESXCLI -V2

$VMHost = $VMHosts | select -First 1
$Datastore =  $VMHost | Get-Datastore -Name $TempDatastoreName
$HostPath = $Datastore.ExtensionData.Info.Url.remove(0,5) + $LocalPath.Split("\")[-1]

## Install-VMhost
Install-VMHostPatch -VMHost $VMHost -HostPath $HostPath

## ESXCLI V2
$esxcli2 = Get-ESXCLI -VMHost $VMhost -V2

### List
$esxcli2.software.vib.list.Invoke() | FT -AutoSize

### Install
$CreateArgs = $esxcli2.software.vib.install.CreateArgs()
$CreateArgs.depot = $HostPath
$InstallResponse = $esxcli2.software.vib.install.Invoke($CreateArgs)
# Invoke-VMScript

## Function and Class from Luc (With Modification)
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 `$_")
}
elseif ($_.GetType().Name -match "$($this.Type)Impl") {
$_
}
})
}
throw [System.IO.FileNotFoundException]::New()
}
}
function Invoke-VMScriptPlus {
<#
.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 & 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
#>
[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)`n`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("`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]
## Modiefed by Markus Kraus:
$hostName = (Resolve-DnsName -Name $ip).where({$_.Section -eq "Answer"}) | 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!`rStatus $($copyResult.StatusCode)`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 = " > $($tempOutput)"
$spec.ProgramPath = "$($tempFile)"
Try {
$procId = $gProcMgr.StartProgramInGuest($moref, $auth, $spec)
Write-Verbose "Run script with '$($tempFile) > $($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) > $($tempOutput)"
$spec.ProgramPath = "cmd.exe"
}
'PowerShellv6' {
$spec.Arguments = " /C ""C:\Program Files\PowerShell\$($PSv6Version)\pwsh.exe"" -NonInteractive -File $($tempFile) > $($tempOutput)"
$spec.ProgramPath = "cmd.exe"
}
'Bat' {
$spec.Arguments = " /s /c cmd > $($tempOutput) 2>&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!`rStatus $($fileContent.Status)`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 = & {
$out = ($fileContent.Content | ForEach-Object {[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
ScriptSize   = $ScriptText.Length
ScriptText   = $ScriptText
GuestOS      = $GuestOSType
}
}
}
}

## Example
$code =@'
if [ -f /etc/os-release ]
then
        cat /etc/os-release
else
        echo 'File os-release not found'
fi
'@

$Pass = ConvertTo-SecureString '<Password>' -AsPlainText -Force
 
$sInvP = @{
   VM = 'PhotonOS'
   ScriptType = 'Bash'
   ScriptText = $code
   GuestOSType = 'Linux'
   GuestUser = 'root'
   GuestPassword = $Pass
}
Invoke-VMScriptPlus @sInvP
# VM UUID "DeepDive" 

function Get-VMID {

  [CmdletBinding()]
    param( 
        [Parameter(Mandatory=$true, ValueFromPipeline=$True, Position=0)]
        	[VMware.VimAutomation.ViCore.Impl.V1.Inventory.InventoryItemImpl[]]
        	$myVMs
    )
Process { 

	$MyView = @()
	ForEach ($myVM in $myVMs){
		$UUIDReport = [PSCustomObject] @{
				Name = $myVM.name 
				UUID = $myVM.extensiondata.Config.UUID
				InstanceUUID = $myVM.extensiondata.config.InstanceUUID
				LocationID = $myVM.extensiondata.config.LocationId
				MoRef = $myVM.extensiondata.Moref.Value
				}
		$MyView += $UUIDReport
		}
	$MyView
	}
}

function Convert-UUID {
    ## http://blog-stack.net/convert-windows-uuid-to-vmware-vmx-uuid-format/
    Param (
        [Parameter(Mandatory=$True)]
        [ValidateNotNullorEmpty()]
        [String]$UUIDwin
    )
    
    $0,$1,$2,$3,$4 = $UUIDwin.Split("-")
    [Array]$UUIDBlocks = [String]$UUIDwin.Split("-")
    [Array]$Block0Split = $0 -split '(..)' | Where-Object { $_ }
    [array]::Reverse($Block0Split)
    $Block0 = $Block0Split -join('')
    
    [Array]$Block1Split = $1 -split '(..)' | Where-Object { $_ }
    [array]::Reverse($Block1Split)
    $Block1 = $Block1Split -join('')
    
    [Array]$Block2Split = $2 -split '(..)' | Where-Object { $_ }
    [array]::Reverse($Block2Split)
    $Block2 = $Block2Split -join('')
    
    [String]$UUID = ($Block0 + "-" + $Block1 + "-" + $Block2 + "-" + $3 + "-" + $4).ToLower()
    
    return $UUID
    
    }
    
## Match OS and VMware UUID 

### Get BIOS UUID from Wiondows OS
$code =@'
(Get-WmiObject Win32_ComputerSystemProduct).UUID
'@

$user = 'local\administrator'
$pswd = '<Password>'
$sPswd = ConvertTo-SecureString -String $pswd -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential -ArgumentList $user,$sPswd
 
$sInvP = @{
   VM = 'Veeam-03'
   ScriptType = 'PowerShell'
   GuestOSType = 'Windows'
   ScriptText = $code
   GuestCredential = $cred
}
### Prepare Output
Remove-Variable UUIDwin, UUIDreturn -ErrorAction SilentlyContinue
$UUIDreturn = Invoke-VMScriptPlus @sInvP
[String]$UUIDwin = $UUIDreturn.ScriptOutput

### Output
Convert-UUID -UUIDwin $UUIDwin
Get-VM -Name "Veeam-03" | Get-VMID | select UUID

 

 

Leave a Reply