1

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?

Daniel
  • 476
  • 6
  • 21
  • `FT -AutoSize` -> `FT *,@{Name='VMName';Expression={$vmName}} -AutoSize` – Mathias R. Jessen Dec 11 '20 at 13:22
  • @MathiasR.Jessen I changed the last line to this, but the output hasn't changed at all (still the same as output #1): "$output.ScriptOutput | FT *,@{Name='VMName';Expression={$vmName}} -AutoSize" – Daniel Dec 11 '20 at 13:27
  • @iRon no luck with that either, still the same as Output 1 – Daniel Dec 11 '20 at 13:37
  • are you certain that the target system is not already added to your returned info? the `Invoke-Command` cmdlet adds `.PSComputerName` property that holds the name of the remote system. it is normally hidden ... and your `Invoke-VMScript` may be doing something similar. – Lee_Dailey Dec 11 '20 at 13:50
  • @Lee_Dailey Invoke-VMScript returns the columns "VM", "ExitCode", and "ScriptOutput". VM has the name of the VM, which is great. Now need to figure out how to combine that with ScriptOutput – Daniel Dec 11 '20 at 14:07
  • `$output | Select vm, ExitCode, @{name='Size (GB)';expr={[int]($_.ScriptOutput.Size/1GB)}}` – iRon Dec 11 '20 at 14:11
  • @Daniel - i see that iRon & mklement0 have shown how to get that info. glad to see that you have a working solution! [*grin*] – Lee_Dailey Dec 12 '20 at 01:43

1 Answers1

1

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.

    • Notably, the usual script-block-based mechanism of employing the $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.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    This worked perfectly, thank you! I'm new to Powershell, so this was definitely a learning experience. – Daniel Dec 11 '20 at 14:23
  • Much appreciated. Side question - any idea how I can select a specific column from that ScriptOutput? – Daniel Dec 11 '20 at 15:33
  • 1
    @Daniel, @iRon's comment shows an example of that: `Select @{name='Size (GB)';expr={[int]($_.ScriptOutput.Size/1GB)}}, ...`. For calculated properties that is fine, but it is cumbers some if you simply want a property as-is, without renaming it, say `.ScriptOutput.Number`; this is the reason why my problem-bypassing solution extracts the `ScriptOutput` values first, in an intermediate step. – mklement0 Dec 11 '20 at 15:35