The Invoke-VMScript cmdlet is definitely one of the PowerCLI cmdlets that is indispensable when you need to do things inside the Guest OS of your VMs.
When you are interacting with a Windows based Guest OS you can run old-fashioned BAT files or use PowerShell scripts. When the Guest OS is Linux based, you currently only can run Bash scripts.
Most Linux flavours have a feature that is called SheBang, 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 Invoke-VMScript cmdlet doesn’t allow one to use that feature.
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.
Update October 14th 2017
- Added here-document bash sample
When the PowerCLI Feature Request website was announced during VMworld session #SER2529BU, it was no surprise to me, to rather quickly see a request appearing to support other languages, besides bash.
First I did some tests, to check if I could get the Invoke-VMScript cmdlet to work with a SheBang line.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
$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 |
Unfortunately it doesn’t.
When a cmdlet doesn’t do what you are trying to achieve, there is always the vSphere API that can help. The GuestOperationsManager 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.
The StartProgramInGuest is the central method of the Invoke-VMScriptPlus function. The function uses the arguments property on the GuestProgramSpec object to redirect the stdio of the process.
The following flow-chart shows a high-level view of the logic that is used in the Invoke-VMScriptPlus function, and shows which method is used at which point.
The Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 |
#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 { <# .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 #> [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 = " > $($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 = &{ $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 } } } } |
Annotations
Line 1: The function requires PowerShell v5 or higher
Line 2: The function requires the PowerCLI Core module
Line 4-48: 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 Home Made OBN
Line 132: 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.
Line 145-152: A hard-coded table with the supported interpreters, and their corresponding SheBang line.
Line 159-168: If the VM is not powered on, or if the VMware Tools are not running, the function will return with a result.
Line 173-176: Tests if there is a SheBang line in the ScriptText. If not, it will add a line based on the ScriptType value.
Line 193-207: The function uses two temporary files to store the script and the script’s output.
Line 210-219: The ScriptText is copied to the temporary file. This is done over HTTPS with the Invoke-WebRequest cmdlet.
Line 222-231: The file containing the ScriptText needs to be made “executable”
Line 235-256: Script execution is started, and the function waits till the process completes.
Line 236: The function uses the Arguments property to redirect stdio to the second temporary file
Line 260-265: The output is fetched, again with an Invoke-WebRequest.
Line 270-273: Clean up the temporary files
Line 275-295: Return an object containing the script output and further info about the script execution
Sample Use
The use of the function Invoke-VMScriptPlus is quite similar to the use of the original Invoke-VMScript.
Some examples.
Bash
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$codeBash = @" #!/usr/bin/env bash echo "Hello World!" "@ $sCode = @{ VM = $vm GuestCredential = $cred ScriptTYpe = 'bash' ScriptText = $codeBash } Invoke-VMScriptPlus @sCode |
And the result
Here document
One feature that is often used in bash, is the so-called here document.
Unfortunately the Invoke-VMScript cmdlet doesn’t seem to support that feature for bash scripts. Code like this …
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
$code = @" rm /tmp/test.txt cat > /tmp/test.txt << 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 |
… produces an error like this.
But due to the way the Invoke-VMScriptPlus function transfers the script text to the guest, this feature is working without a glitch.
The same code as above, except that Invoke-VMScript is replaced by Invoke-VMScriptPlus.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
$code = @" rm /tmp/test.txt cat > /tmp/test.txt << 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 |
… now produces the expected result.
Python3
In this example we are not adding the SheBang line in the ScriptText, but through the ScriptType value, the function will add this line.
1 2 3 4 5 6 7 8 9 10 11 12 |
$codePython = @" print("Happy 10th Birthday PowerCLI!") "@ $sCode = @{ VM = $vm GuestCredential = $cred ScriptTYpe = 'python3' ScriptText = $codePython } Invoke-VMScriptPlus @sCode |
And the result.
Notice how the Invoke-VMScriptPlus function added the SheBang line.
Other
During the VMworld Breakout session, where this function was first demonstrated, we showed additional examples in the following video.
Since there are many Linux flavours out there, and since I obviously couldn’t test them all, I would appreciate it if you can send me feedback about which Linux flavours/versions work, and which don’t.
If there are requests for other languages, feel free to forward me your requests.
Enjoy!
DdR
Thanks for your work. Good job.
Is it possible to inject patameters to defined bash script?
LucD
Not in the current version I’m afraid.
But you should be able to pass environment variables with the ScriptEnvironment variable in the newer version of the function.
See Invoke-VMScriptPlus V2
DDR
Sorry for question, but How to deploy/install Invoke-VMScriptPlus V2?
LucD
You have a couple of options.
The simplest is to store the code from the blog post in a .ps1 file.
Then add the end of the .ps1 file, call the function with Invoke-VMScriptPlus (with the required parameters).