I'm writing a script to audit the disk partition types in our virtual environment. I've got this code that produces the following output:
$vmName = "VM NAME"
$output = Invoke-VMScript -ScriptText {Get-Disk | select Number, @{name='Size (GB)';expr={[int]($_.Size/1GB)}}, PartitionStyle} -VM $vmName -GuestUser $Username -GuestPassword $Password
$output.ScriptOutput | FT -AutoSize
Output:
Number Size (GB) PartitionStyle
------- --------- --------------
0 160 MBR
This alone is fine, however I also want to add the name of the VM as well, since this will be looping through thousands of VMs. I tried this, but it produced blank output for the ComputerName:
$vmName = "VM NAME"
$output = Invoke-VMScript -ScriptText {Get-Disk | select @{l="ComputerName";e={$vmName}}, Number, @{name='Size (GB)';expr={[int]($_.Size/1GB)}}, PartitionStyle} -VM $vmName -GuestUser $Username -GuestPassword $Password
$output.ScriptOutput | FT -AutoSize
Output:
ComputerName Number Size (GB) PartitionStyle
------------ ------ --------- --------------
0 160 MBR
Any ideas for how I can make this work?
As a bonus, any ideas for how I can add the actual name of the VM in there since the vSphere name may not be the actual name of the machine?
tl;dr
Invoke-VMScript -ScriptText
accepts only a string containing PowerShell commands, not a script block ({...}
).
Since there is no separate parameter for passing arguments to the -ScriptText
code, the only way to include - invariably stringified - values from the caller's scope is to use string interpolation, as shown in the next section.
$using:
scope in order to embed values from the caller does not work.Passing a script-block literal { ... }
to -ScriptText
technically works, because on conversion to string the verbatim content between {
and }
is used, but is best avoided, to avoid conceptual confusion.
Note:
Below is an explanation of your problem and a fix for your original approach.
You can bypass your problem if you add the calculated columns via select
(Select-Object
) on the caller's side (as hinted at by iRon), using the VM
property of the object output by Invoke-VMScript
, which contains the remote computer name (which Lee_Dailey helped discover):
$vmName = "VM NAME"
# Only run `Get-Disk` remotely, and run the `select` (`Select-Object`)
# command *locally*:
# Note the need to extract the actual script output via the .ScriptOutput
# property and the use of the .VM property to get the computer name.
$output = Invoke-VMScript -ScriptText { Get-Disk } -VM $vmName -GuestUser $Username -GuestPassword $Password |
select -ExpandProperty ScriptOutput -Property VM |
select @{l="ComputerName";e='VM'}, Number, @{name='Size (GB)';expr={[int]($_.Size/1GB)}}, PartitionStyle
The command you're passing to Invoke-VMScript
via -ScriptText
is executed on a different machine, which knows nothing about your local $vmName
variable.
In PowerShell remoting commands such as Invoke-Command
, the $using:
scope would be the solution: ... | select @{l="ComputerName";e={$using:vmName}}, ...
However, $using:
can only be used in script blocks ({ ... }
).
While you are trying to pass a script block to Invoke-VMScript -ScriptText
, it is actually converted to a string before it is submitted to the VM, because Invoke-VMScript
's -ScriptText
parameter is [string]
-typed.
Since Invoke-VMScript
has no separate parameter for passing arguments from the caller's scope, the only way to include values from the caller's scope is to use string interpolation:
$vmName = "VM NAME"
$output = Invoke-VMScript -ScriptText @"
Get-Disk |
select @{ l="ComputerName"; e={ '$vmName' } },
Number,
@{ name='Size (GB)'; expr={[int](`$_.Size/1GB)} },
PartitionStyle
"@ -VM $vmName -GuestUser $Username -GuestPassword $Password
Note the use of an expandable here-string (@"<newline>...<newline>"@
); important: the closing delimiter, "@
, must be at the very start of a line (not even whitespace is permitted before it); see this answer for more information about string literals in PowerShell.
$vmName
is now expanded up front in the caller's scope; to make the resulting e={ ... }
script block syntactically valid, the expanded $vmName
value must be enclosed in '...'
.
By contrast, the $
in $_
must be _escaped - as `$
- since it must be passed through to the remote machine.
That said, since you want to determine the actual computer name on that remote computer, you don't even need string interpolation; use $env:COMPUTERNAME
:
$vmName = "VM NAME"
$output = Invoke-VMScript -ScriptText @'
Get-Disk |
select @{ l="ComputerName"; e={ $env:COMPUTERNAME } },
Number,
@{ name='Size (GB)'; expr={[int]($_.Size/1GB)} },
PartitionStyle
'@ -VM $vmName -GuestUser $Username -GuestPassword $Password
Note that in this case - since no string interpolation is needed - a verbatim (literal) here-string (@'<newline>...<newline>'@
) is used, which also means that no escaping of pass-through $
chars. is required.
When string interpolation isn't needed, you could actually pass the script text via a script block ({ ... }
), which simplifies the syntax somewhat (a script block stringifies to its verbatim content between {
and }
), but since a string is ultimately passed, it is better to use one to begin with, for conceptual clarity.