Copy-VMGuestFile is the PowerCLI cmdlet that moves a file between your workstation and the inside of a virtual machine. It does not use the guest network. The file travels through vCenter Server to the ESXi host, and VMware Tools inside the guest writes it to disk, which is why it still works on a machine whose IP configuration you broke ten minutes ago.
That makes it the way to get a log off an unreachable VM, the way to put an installer onto an appliance with no SSH, and the way to seed a file onto fifty guests without arranging WinRM on any of them. The companion cmdlet Invoke-VMScript runs commands through the same channel, and that cmdlet has its own page here.
Broadcom publishes two examples for this cmdlet. Both of them fail at the PowerShell parameter binder before any connection is attempted. This page shows what the binder actually says, measured rather than remembered, and it spends most of its length on the thing the reference states in two sentences and nobody reads twice: the direction switch decides which of your two paths is allowed to be relative.
Applies to: VMware PowerCLI 13.3 against vCenter Server or ESXi 5.0 and later
Quick answer
Connect, build a credential object, and name the direction. The destination below sits inside the VM, so it has to be an absolute path. If you want to understand why the same command with the paths swapped returns an error instead of a file, read on from the top.
# Get-Credential keeps the guest password out of your console history and
# produces the PSCredential type the cmdlet binds without complaint
$cred = Get-Credential -UserName 'SRV-PROD-01\Administrator' -Message 'Guest OS account'
Connect-VIServer vcenter.corp.local
# -LocalToGuest: the source is on this machine, the destination is inside the
# VM. -Force creates C:\Temp in the guest if it is not there already
Copy-VMGuestFile -Source 'C:\vmguestlab\payload.txt' -Destination 'C:\Temp\' -VM 'SRV-PROD-01' -LocalToGuest -Force -GuestCredential $cred
What you are about to build, and why
The lab on this page is nine PowerShell files and two small text files in one folder, C:\vmguestlab. Nothing is installed, no module is imported, no vCenter object is touched and no guest is written to. Seven of the nine files run on any machine with PowerShell 7, with or without PowerCLI, because they measure the rules and the parameter binding rather than the copy.
At the end you have a preflight that tells you which VMs can be copied into, a rule checker that refuses a path pair the cmdlet would reject, a wrapper that composes the real command line without running it, and a verifier that proves the bytes arrived.
| File | What it does |
|---|---|
GuestPathRules.ps1 | Two helper functions the others load. Holds the documented path rules. |
Test-GuestPath.ps1 | Four ways to ask whether a path is absolute, over eight realistic paths. |
Test-CopyDirection.ps1 | Ten copies run through the rules, four of them rejected. |
Test-GuestPassword.ps1 | What the binder does with the credential arguments the reference prints. |
Test-CopyParameterSet.ps1 | The two parameter sets, and the message you get for two different mistakes. |
Test-GuestCopyReady.ps1 | Power state and VMware Tools status read together, one verdict per VM. |
Invoke-GuestFileCopy.ps1 | The wrapper. Checks the pair, composes the command, and can run nothing. |
Compare-GuestCopy.ps1 | Hashes the local copy and compares it with the hash from inside the guest. |
Remove-GuestCopyLab.ps1 | Lists everything created here so you can remove the folder in one line. |
payload.txt and payload-short.txt | A 48 byte file to copy, and a truncated version of it to fail against. |
Before you start
Five steps, and the fifth is a note rather than a command. Steps 1 to 4 take under a minute and every later section depends on them.
1. Confirm which PowerShell you are in. pwsh.exe is PowerShell 7 and powershell.exe is Windows PowerShell 5.1. PowerCLI 13.3 supports both on Windows, but on Linux and macOS it supports PowerShell 7 only, and every output block on this page was captured on PowerShell 7.4.6.
# Prints the version and the edition. Core means PowerShell 7, Desktop means
# Windows PowerShell 5.1
$PSVersionTable.PSVersion
$PSVersionTable.PSEdition
2. Create the working folder and move into it. Every script below is saved here, and three of them resolve their neighbours relative to their own location.
New-Item -ItemType Directory -Path 'C:\vmguestlab' -Force | Out-Null
Set-Location 'C:\vmguestlab'
3. Allow local scripts for this window only. The scope matters. Process affects the current session and nothing else, so you are not changing a machine policy to read an article.
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force
4. Create the two payload files. Paste this one into the shell rather than saving it. It writes the files with line feed endings on every platform, so the byte counts and hashes it prints are the ones you will see in the verification section.
# Writes the two lab files with LF line endings on every platform, so the byte
# counts and the hashes below match whatever shell wrote them. Set-Content
# would use this machine's line ending and change both.
$lf = [char]10
[System.IO.File]::WriteAllText(
(Join-Path $PWD 'payload.txt'),
'agent=zaur-collector' + $lf + 'version=4.2.1' + $lf + 'mode=install' + $lf)
[System.IO.File]::WriteAllText(
(Join-Path $PWD 'payload-short.txt'),
'agent=zaur-collector' + $lf + 'version')
# Read both back, so what follows is measured rather than assumed.
'{0,-20}{1,-8}{2}' -f 'File', 'Bytes', 'SHA256'
'{0,-20}{1,-8}{2}' -f ('-'*19), ('-'*7), ('-'*64)
foreach ($f in 'payload.txt', 'payload-short.txt') {
$i = Get-Item -LiteralPath $f
$h = Get-FileHash -LiteralPath $f -Algorithm SHA256
'{0,-20}{1,-8}{2}' -f $i.Name, $i.Length, $h.Hash
}
You should see a two row table. If you wrote the files some other way and the byte counts read 51 and 29 rather than 48 and 28, the line endings are carriage return pairs and none of the hashes further down will match.
File Bytes SHA256
------------------- ------- ----------------------------------------------------------------
payload.txt 48 72E2E409B5280D371EF79717361ACC0E9305E068B8A602240DB414C46FD05E42
payload-short.txt 28 C97AB1814010AE5385790B1B6ABF443D1C4200BE2F67DC636D175A74D9C7A337
5. A note on path syntax. Every path on this page is in single quotes. A Windows path contains backslashes, and inside double quotes PowerShell reads some backslash sequences as something else. Single quotes pass the string through exactly as typed.
Connect-VIServer. The nine lab files never import it, so you can work through the measured sections on a laptop with no vCenter in reach.
What it does, and the two parameter sets
The cmdlet hands the file to the vSphere API, which hands it to VMware Tools in the guest. Underneath are two operations on the GuestFileManager managed object, InitiateFileTransferToGuest and InitiateFileTransferFromGuest, which is why one cmdlet needs a switch to say which way the bytes go.
The reference publishes the syntax as two parameter sets, one per direction. They are identical apart from the switch, and that switch is mandatory in both.
Copy-VMGuestFile [-Destination] <String> [-VM] <VirtualMachine[]> -GuestToLocal
-Source <String[]> [-Force] [-GuestCredential <PSCredential>] ...
Copy-VMGuestFile [-Destination] <String> [-VM] <VirtualMachine[]> -LocalToGuest
-Source <String[]> [-Force] [-GuestCredential <PSCredential>] ...
These are the parameters the rest of the page comes back to.
| Parameter | Type | What it is for |
|---|---|---|
-Source | String[] | Required. An array, so one call can name several sources. Wildcards allowed only at the end of the path. |
-Destination | String | Required, position 1. A single string, not an array. |
-VM | VirtualMachine[] | Required, position 2. The parameter the reference marks as taking pipeline input by value. |
-GuestToLocal | SwitchParameter | Mandatory member of one parameter set. The source is in the guest. |
-LocalToGuest | SwitchParameter | Mandatory member of the other set. The destination is in the guest. |
-Force | SwitchParameter | Creates the directories in the destination path that do not exist yet. |
-GuestCredential | PSCredential | Guest OS credentials as one object. The form every call on this page uses. |
-GuestUser and -GuestPassword | String and SecureString | The split form. Read the password type twice: it is not a string. |
-HostUser, -HostPassword, -HostCredential | various | Documented as needed only against vCenter or ESX earlier than 4.0, or a VIX earlier than 1.10. |
-ToolsWaitSecs | Int32 | Seconds to wait for a response from VMware Tools. A non-positive value waits indefinitely. |
The direction switch decides which path must be absolute
Two sentences in the reference carry most of the trouble people have with this cmdlet. On -Source: "If the file is on a virtual machine, specifies the absolute file path. Relative file paths are supported only when copying files from a local storage." On -Destination: "If the destination points to a virtual machine, specify the absolute file path. Relative destination paths are supported only when copying files to a local storage."
Neither sentence names a direction, because it does not have to. The direction switch is what decides which of your two paths points at a virtual machine, and therefore which one has to be absolute. The same two strings are a valid copy one way round and an error the other way round.
| Direction | Path on the VM | Must be absolute | May be relative |
|---|---|---|---|
-GuestToLocal | -Source | -Source | -Destination |
-LocalToGuest | -Destination | -Destination | -Source |
Those rules are short enough to implement, which is the cheapest way to stop guessing. Save the helper file first: the next two scripts load it and will fail without it.
Save this as GuestPathRules.ps1 in C:\vmguestlab. It is loaded by the other scripts and prints nothing when you run it on its own.
<#
GuestPathRules.ps1
Two helpers the other scripts in this lab dot-source. Dot-sourcing this
file defines the functions and prints nothing.
The rules implemented here are the ones the Copy-VMGuestFile reference
states for -Source and -Destination. Nothing here talks to vCenter.
#>
function Test-GuestPathShape {
<#
Returns Windows, Posix or Relative for a path STRING, without asking
the client's own file system anything. A guest path has to be judged
as text, because the client that runs PowerCLI may not be the same
kind of machine as the guest.
#>
param([Parameter(Mandatory)][AllowEmptyString()][string]$Path)
if ($Path -match '^[A-Za-z]:[\\/]') { return 'Windows' } # C:\ or C:/
if ($Path -match '^\\\\[^\\]') { return 'Windows' } # \\server\share
if ($Path -match '^/') { return 'Posix' } # /tmp/
return 'Relative'
}
function Test-GuestCopyPair {
<#
Applies the documented -Source and -Destination rules to one copy and
returns a verdict object. Direction is the switch you would pass to
Copy-VMGuestFile: GuestToLocal or LocalToGuest.
Wildcards: the reference says they are allowed "only at the end of the
source path". This reads that as "only in the last path segment", so a
wildcard followed by a separator is rejected.
#>
param(
[Parameter(Mandatory)][ValidateSet('GuestToLocal','LocalToGuest')][string]$Direction,
[Parameter(Mandatory)][AllowEmptyString()][string]$Source,
[Parameter(Mandatory)][AllowEmptyString()][string]$Destination
)
# The direction decides which of the two paths lives on the VM. That one
# must be absolute; the other one sits on local storage and may be relative.
$guestSide = if ($Direction -eq 'GuestToLocal') { 'Source' } else { 'Destination' }
$reasons = New-Object System.Collections.Generic.List[string]
$srcShape = Test-GuestPathShape -Path $Source
$dstShape = Test-GuestPathShape -Path $Destination
if ($guestSide -eq 'Source' -and $srcShape -eq 'Relative') {
$reasons.Add('Source is on the VM and must be an absolute path')
}
if ($guestSide -eq 'Destination' -and $dstShape -eq 'Relative') {
$reasons.Add('Destination points to the VM and must be an absolute path')
}
# Wildcards are a source-only allowance, and only in the last segment.
$lastSep = [Math]::Max($Source.LastIndexOf('\'), $Source.LastIndexOf('/'))
$head = if ($lastSep -ge 0) { $Source.Substring(0, $lastSep) } else { '' }
if ($head -match '[\*\?]') {
$reasons.Add('Source has a wildcard before the last path separator')
}
if ($Destination -match '[\*\?]') {
$reasons.Add('Destination may not contain a wildcard')
}
$wildcardSource = ($Source -match '[\*\?]')
[pscustomobject]@{
Direction = $Direction
GuestSide = $guestSide
SourceShape = $srcShape
TargetShape = $dstShape
WildcardSource = $wildcardSource
Allowed = ($reasons.Count -eq 0)
Reason = if ($reasons.Count -eq 0) { 'matches the documented rules' } else { $reasons -join '; ' }
}
}
Save this as Test-CopyDirection.ps1 in C:\vmguestlab and run it with .\Test-CopyDirection.ps1.
<#
Test-CopyDirection.ps1
Ten copies, each described the way you would type it, run through the
documented -Source and -Destination rules. Nothing is copied and no
connection is made: this is the rule check only.
#>
. "$PSScriptRoot/GuestPathRules.ps1"
# Eight of these ten pairs are two spellings of four jobs: the same two paths
# in both directions. The last two carry wildcards.
$cases = @(
@{ Direction = 'GuestToLocal'; Source = 'C:\inetpub\logs\u_ex260104.log'; Destination = 'C:\vmguestlab\' }
@{ Direction = 'LocalToGuest'; Source = 'C:\inetpub\logs\u_ex260104.log'; Destination = 'C:\vmguestlab\' }
@{ Direction = 'GuestToLocal'; Source = 'logs\u_ex260104.log'; Destination = 'C:\vmguestlab\' }
@{ Direction = 'LocalToGuest'; Source = 'logs\u_ex260104.log'; Destination = 'C:\vmguestlab\' }
@{ Direction = 'GuestToLocal'; Source = '/var/log/messages'; Destination = 'collected' }
@{ Direction = 'LocalToGuest'; Source = '/var/log/messages'; Destination = 'collected' }
@{ Direction = 'LocalToGuest'; Source = '.\payload.txt'; Destination = '/tmp/' }
@{ Direction = 'GuestToLocal'; Source = '.\payload.txt'; Destination = '/tmp/' }
@{ Direction = 'GuestToLocal'; Source = 'C:\inetpub\logs\*.log'; Destination = 'C:\vmguestlab\' }
@{ Direction = 'GuestToLocal'; Source = 'C:\inetpub\*\u_ex260104.log'; Destination = 'C:\vmguestlab\' }
)
'{0,-14}{1,-32}{2,-18}{3,-9}{4}' -f 'Direction', 'Source', 'Destination', 'Allowed', 'Why'
'{0,-14}{1,-32}{2,-18}{3,-9}{4}' -f ('-'*13), ('-'*31), ('-'*17), ('-'*8), ('-'*58)
$allowed = 0
foreach ($c in $cases) {
$v = Test-GuestCopyPair -Direction $c.Direction -Source $c.Source -Destination $c.Destination
if ($v.Allowed) { $allowed++ }
'{0,-14}{1,-32}{2,-18}{3,-9}{4}' -f $v.Direction, $c.Source, $c.Destination, $v.Allowed, $v.Reason
}
''
'{0,-28}{1}' -f 'Pairs checked:', $cases.Count
'{0,-28}{1}' -f 'Allowed by the rules:', $allowed
'{0,-28}{1}' -f 'Rejected by the rules:', ($cases.Count - $allowed)
You should see a ten row table with an Allowed column, then three counts. Read the first eight rows in pairs: they are four copies, each tried in both directions.
Direction Source Destination Allowed Why
------------- ------------------------------- ----------------- -------- ----------------------------------------------------------
GuestToLocal C:\inetpub\logs\u_ex260104.log C:\vmguestlab\ True matches the documented rules
LocalToGuest C:\inetpub\logs\u_ex260104.log C:\vmguestlab\ True matches the documented rules
GuestToLocal logs\u_ex260104.log C:\vmguestlab\ False Source is on the VM and must be an absolute path
LocalToGuest logs\u_ex260104.log C:\vmguestlab\ True matches the documented rules
GuestToLocal /var/log/messages collected True matches the documented rules
LocalToGuest /var/log/messages collected False Destination points to the VM and must be an absolute path
LocalToGuest .\payload.txt /tmp/ True matches the documented rules
GuestToLocal .\payload.txt /tmp/ False Source is on the VM and must be an absolute path
GuestToLocal C:\inetpub\logs\*.log C:\vmguestlab\ True matches the documented rules
GuestToLocal C:\inetpub\*\u_ex260104.log C:\vmguestlab\ False Source has a wildcard before the last path separator
Pairs checked: 10
Allowed by the rules: 6
Rejected by the rules: 4
Six of the ten pairs are allowed and four are rejected. Of the four copies tried in both directions, one is valid both ways and three are valid in exactly one direction. That is the whole point of the section: nothing about the two strings changed, only the switch.
-GuestToLocal reads like every other PowerShell copy, which is what makes it easy to type by accident. logs\u_ex260104.log means "relative to my current directory" to you and nothing at all to the guest.
One message for two different mistakes
Because the direction switch is a mandatory member of both parameter sets, leaving it out and passing both are the same kind of failure to the binder. The next script declares a stand-in with the same parameter shape so you can read the messages without a vCenter connection.
Copy-GuestFileShim on purpose. A function named Copy-VMGuestFile would shadow the real cmdlet for the rest of your session, and you would spend the afternoon wondering why a copy that reports success never lands.
Save this as Test-CopyParameterSet.ps1 in C:\vmguestlab and run it with .\Test-CopyParameterSet.ps1.
<#
Test-CopyParameterSet.ps1
Copy-VMGuestFile has two parameter sets, and the direction switch is the
mandatory member of each one. This declares a stand-in with the same shape
and calls it four ways, so the binder's own messages can be read without
a vCenter connection.
The stand-in is deliberately NOT named Copy-VMGuestFile. A function with
that name would shadow the real cmdlet for the rest of the session.
#>
function Copy-GuestFileShim {
[CmdletBinding()]
param(
[Parameter(Mandatory, Position = 0)][string[]]$Source,
[Parameter(Mandatory, Position = 1)][string]$Destination,
[Parameter(Mandatory, ParameterSetName = 'GuestToLocal')][switch]$GuestToLocal,
[Parameter(Mandatory, ParameterSetName = 'LocalToGuest')][switch]$LocalToGuest
)
'resolved set: ' + $PSCmdlet.ParameterSetName
}
$src = 'C:\vmguestlab\payload.txt'
$dst = 'C:\Temp\'
function Show-Call {
param([string]$Label, [scriptblock]$Call)
$Label
try {
# -ErrorAction Stop turns a binding failure into something catchable.
# Without it the message still appears, but the script keeps going.
& $Call | ForEach-Object { ' ' + $_ }
} catch {
' error id: ' + $_.FullyQualifiedErrorId
' message: ' + (($_.Exception.Message -split '\r?\n')[0])
}
''
}
Show-Call '1. neither switch' { Copy-GuestFileShim -Source $src -Destination $dst -ErrorAction Stop }
Show-Call '2. both switches' { Copy-GuestFileShim -Source $src -Destination $dst -GuestToLocal -LocalToGuest -ErrorAction Stop }
Show-Call '3. -GuestToLocal' { Copy-GuestFileShim -Source $src -Destination $dst -GuestToLocal -ErrorAction Stop }
Show-Call '4. -LocalToGuest' { Copy-GuestFileShim -Source $src -Destination $dst -LocalToGuest -ErrorAction Stop }
You should see four numbered calls. The first two fail and the last two report which parameter set was resolved. Compare the two error lines before reading on.
1. neither switch
error id: AmbiguousParameterSet,Copy-GuestFileShim
message: Parameter set cannot be resolved using the specified named parameters. One or more parameters issued cannot be used together or an insufficient number of parameters were provided.
2. both switches
error id: AmbiguousParameterSet,Copy-GuestFileShim
message: Parameter set cannot be resolved using the specified named parameters. One or more parameters issued cannot be used together or an insufficient number of parameters were provided.
3. -GuestToLocal
resolved set: GuestToLocal
4. -LocalToGuest
resolved set: LocalToGuest
The first two calls return the same error identifier and the same message. The text names neither switch, so it cannot tell you whether you forgot one or passed both. When you see AmbiguousParameterSet from this cmdlet, count the direction switches on the line: the answer is always zero or two.
The credential argument both published examples get wrong
The reference page carries two examples. Here they are as printed, including the line break in the second one.
Copy-VMGuestFile -Source c:\text.txt -Destination c:\temp\ -VM VM -GuestToLocal -GuestUser user -GuestPassword pass2
$vm = Get-VM -Name VM
Get-Item "c:\FolderToCopy\*.*" | Copy-VMGuestFile -Destination "c:\MyFolder" -VM $vm -LocalToGuest -GuestUser -GuestPassword pass2
Broadcom knowledge base article 421428, on copying files out of a non networked VM, prints the same shape: -GuestUser "Administrator" -GuestPassword "Passwd". The parameter table gives -GuestPassword the type SecureString, and PowerShell has no conversion from a string to one. The next script hands those exact argument patterns to a parameter of the documented types and prints what comes back.
Save this as Test-GuestPassword.ps1 in C:\vmguestlab and run it with .\Test-GuestPassword.ps1.
<#
Test-GuestPassword.ps1
The reference types -GuestPassword as SecureString. This asks PowerShell
what happens when a bare string is handed to a parameter of that type,
and then what happens when the value is converted first.
Nothing here is a PowerCLI call. The function below stands in for the
cmdlet's parameter so the binding can be observed without vCenter.
#>
function Use-GuestPassword {
param(
[Parameter(Mandatory)][string]$GuestUser,
[Parameter(Mandatory)][System.Security.SecureString]$GuestPassword
)
'{0,-24}{1}' -f 'bound parameter type:', $GuestPassword.GetType().FullName
}
'{0,-24}{1}' -f 'PowerShell version:', $PSVersionTable.PSVersion.ToString()
''
# 1. Exactly what the reference's own example types: a bare string.
'1. -GuestPassword pass2'
try {
Use-GuestPassword -GuestUser 'user' -GuestPassword 'pass2'
} catch {
'{0,-24}{1}' -f 'error id:', $_.FullyQualifiedErrorId
'{0,-24}{1}' -f 'message:', $_.Exception.Message
}
''
# 2. The second example in the reference types -GuestUser with no value and
# another parameter name straight after it. This is that argument pattern.
'2. -GuestUser -GuestPassword pass2'
try {
Use-GuestPassword -GuestUser -GuestPassword 'pass2'
} catch {
'{0,-24}{1}' -f 'error id:', $_.FullyQualifiedErrorId
'{0,-24}{1}' -f 'message:', (($_.Exception.Message -split '\r?\n')[0])
}
''
# 3. The same password, converted first. -AsPlainText -Force is the usual way
# to build a SecureString from a literal, and it defeats the point of the
# type: the password is in the script and in the session history.
'3. -GuestPassword (ConvertTo-SecureString ...)'
$sec = ConvertTo-SecureString 'pass2' -AsPlainText -Force
Use-GuestPassword -GuestUser 'user' -GuestPassword $sec
''
# 4. The form both of the reference's examples avoid and every call in this
# article uses. Built here from parts so the script needs no prompt.
'4. -GuestCredential (PSCredential)'
$cred = [System.Management.Automation.PSCredential]::new('SRV-PROD-01\Administrator', $sec)
'{0,-24}{1}' -f 'credential type:', $cred.GetType().FullName
'{0,-24}{1}' -f 'user name:', $cred.UserName
'{0,-24}{1}' -f 'password type:', $cred.Password.GetType().Name
'{0,-24}{1}' -f 'password length:', $cred.Password.Length
You should see four numbered cases. The first two print an error identifier and a message; the last two print types. Case 2 is the argument pattern from the second example above, where a parameter name follows -GuestUser instead of a value.
PowerShell version: 7.4.6
1. -GuestPassword pass2
error id: ParameterArgumentTransformationError,Use-GuestPassword
message: Cannot process argument transformation on parameter 'GuestPassword'. Cannot convert the value of type "System.String" to type "System.Security.SecureString".
2. -GuestUser -GuestPassword pass2
error id: MissingArgument,Use-GuestPassword
message: Missing an argument for parameter 'GuestUser'. Specify a parameter of type 'System.String' and try again.
3. -GuestPassword (ConvertTo-SecureString ...)
bound parameter type: System.Security.SecureString
4. -GuestCredential (PSCredential)
credential type: System.Management.Automation.PSCredential
user name: SRV-PROD-01\Administrator
password type: SecureString
password length: 5
| What the examples type | Documented type | What the binder does |
|---|---|---|
-GuestPassword pass2 | SecureString | Refuses to transform System.String into System.Security.SecureString |
-GuestUser -GuestPassword pass2 | String | Reports a missing argument for -GuestUser and stops there |
-GuestPassword $secure | SecureString | Binds, and the password is in your script |
-GuestCredential $cred | PSCredential | Binds, and the password came from a prompt |
ConvertTo-SecureString -AsPlainText -Force, which does bind. It also puts the password in the script file, in your shell history and in any transcript, which is the one thing the SecureString type was there to discourage. Get-Credential costs one prompt per session.
Get-Item output into the cmdlet while naming -VM on the same line. The reference marks -VM as the parameter that accepts pipeline input by value, not -Source, so piping files in is not the way to name several of them. Pass an array to -Source instead.
Asking this machine about a path on another machine
PowerCLI 13.3 installs on Windows, Linux and macOS, and on Linux and macOS it runs under PowerShell 7. So the client composing the command line is not necessarily the same kind of machine as the guest receiving the file, and the usual way to check a path asks the client.
Four questions, eight paths, one table. The last two lines of the output say which client produced it.
Save this as Test-GuestPath.ps1 in C:\vmguestlab and run it with .\Test-GuestPath.ps1.
<#
Test-GuestPath.ps1
Four ways to ask "is this path absolute", over eight path strings of the
shapes that turn up in a Copy-VMGuestFile command line. Run it on the
machine that runs PowerCLI. The answers depend on that machine, which is
the whole point.
#>
. "$PSScriptRoot/GuestPathRules.ps1"
$paths = @(
'C:\inetpub\logs\u_ex260104.log'
'C:\vmguestlab\'
'D:/exports/report.csv'
'\\SRV-FS-01\exports\report.csv'
'/var/log/messages'
'/tmp/'
'exports\report.csv'
'report.csv'
)
'{0,-34}{1,-10}{2,-13}{3,-14}{4}' -f 'Path', 'Shape', 'IsPathRooted', 'SplitPathAbs', 'Leaf by .NET'
'{0,-34}{1,-10}{2,-13}{3,-14}{4}' -f ('-'*33), ('-'*9), ('-'*12), ('-'*13), ('-'*20)
foreach ($p in $paths) {
# Test-GuestPathShape reads the string only. The other three ask this
# machine, which is a different question than the one the reference asks.
$shape = Test-GuestPathShape -Path $p
$rooted = [System.IO.Path]::IsPathRooted($p)
$split = Split-Path -Path $p -IsAbsolute
$leaf = [System.IO.Path]::GetFileName($p)
'{0,-34}{1,-10}{2,-13}{3,-14}{4}' -f $p, $shape, $rooted, $split, $leaf
}
''
'{0,-34}{1}' -f 'This client reports IsWindows:', $IsWindows
'{0,-34}{1}' -f 'Directory separator char:', [System.IO.Path]::DirectorySeparatorChar
The capture below came from a Linux client. On a Windows client the IsPathRooted column and the last two lines read differently, and that difference is the finding rather than a defect: [System.IO.Path] answers for the platform it is running on, which is documented, while the path in your command line belongs to the guest.
Path Shape IsPathRooted SplitPathAbs Leaf by .NET
--------------------------------- --------- ------------ ------------- --------------------
C:\inetpub\logs\u_ex260104.log Windows False True C:\inetpub\logs\u_ex260104.log
C:\vmguestlab\ Windows False True C:\vmguestlab\
D:/exports/report.csv Windows False True report.csv
\\SRV-FS-01\exports\report.csv Windows False True \\SRV-FS-01\exports\report.csv
/var/log/messages Posix True True messages
/tmp/ Posix True True
exports\report.csv Relative False False exports\report.csv
report.csv Relative False False report.csv
This client reports IsWindows: False
Directory separator char: /
| How you ask | What it actually answers | Use it for |
|---|---|---|
Test-GuestPathShape | What the string looks like, with no reference to any file system | A path on the other machine |
[System.IO.Path]::IsPathRooted | Whether this platform would call the path rooted | A path on the client, in .NET code |
Split-Path -IsAbsolute | Whether PowerShell would call the path absolute, provider aware | A path on the client, in a script |
[System.IO.Path]::GetFileName | The part after the last separator this platform recognises | Only when the path is local |
GetFileName handed back the entire string for the backslash paths, because a backslash is an ordinary character there, and it returned the file name for D:/exports/report.csv because a forward slash is the separator. A script that builds a guest destination by appending a leaf name is silently correct on one client and silently wrong on another.
Wildcards, and the one place they are allowed
The reference allows wildcards on -Source only, and only "at the end of the source path". It adds one condition for the direction that reads out of a guest: copying from a guest to a local directory supports wildcards only on vCenter Server 5.0 and later.
"At the end" is not defined further, so the rule checker reads it as "in the last path segment", the reading that the example in the reference, c:\FolderToCopy\*.*, fits. Rows nine and ten of the run above are the two sides of it.
| Source | What the rule checker returns | Why |
|---|---|---|
C:\inetpub\logs\*.log | Allowed | The wildcard is in the last segment |
C:\inetpub\*\u_ex260104.log | Rejected | A wildcard before the last separator |
C:\inetpub\logs\* | Allowed | The whole folder, and folders are supported |
a wildcard in -Destination | Rejected | The destination is one path and takes no pattern |
-Source "C:\temp\". Expect a recursive copy, not a single file.
Is the VM ready to be copied into
Guest file transfer needs the VM powered on and VMware Tools running inside it. They are two separate facts with two separate fixes, and the vSphere API documents three values for the Tools field: guestToolsNotRunning, guestToolsRunning and guestToolsExecutingScripts. Only guestToolsRunning means the channel is ready.
Save this as Test-GuestCopyReady.ps1 in C:\vmguestlab and run it with .\Test-GuestCopyReady.ps1.
<#
Test-GuestCopyReady.ps1
Guest file copy needs the VM powered on and VMware Tools running inside
it. Those are two facts and they fail separately, so this reads them
together and returns one verdict per VM.
The four rows below are stand-ins with the field names and the field
values the vSphere API documents, so the script can be run and read
anywhere. Against a real vCenter, replace $vms with:
$vms = Get-VM | Select-Object -Property @{n='VMName';e={$_.Name}},
PowerState,
@{n='ToolsRunningStatus';e={$_.ExtensionData.Guest.ToolsRunningStatus}},
@{n='GuestFamily';e={$_.ExtensionData.Guest.GuestFamily}}
#>
# The property is VMName rather than Name. A real VM object has a Name of its
# own, so reusing it on a stand-in makes the two easy to confuse later.
$vms = @(
[pscustomobject]@{ VMName='SRV-PROD-01'; PowerState='PoweredOn'; ToolsRunningStatus='guestToolsRunning'; GuestFamily='windowsGuest' }
[pscustomobject]@{ VMName='SRV-PROD-02'; PowerState='PoweredOn'; ToolsRunningStatus='guestToolsNotRunning'; GuestFamily='windowsGuest' }
[pscustomobject]@{ VMName='SRV-APP-11'; PowerState='PoweredOff'; ToolsRunningStatus='guestToolsNotRunning'; GuestFamily='linuxGuest' }
[pscustomobject]@{ VMName='SRV-APP-12'; PowerState='PoweredOn'; ToolsRunningStatus='guestToolsExecutingScripts'; GuestFamily='linuxGuest' }
)
'{0,-14}{1,-12}{2,-30}{3,-8}{4}' -f 'VM', 'Power', 'ToolsRunningStatus', 'Ready', 'What to fix first'
'{0,-14}{1,-12}{2,-30}{3,-8}{4}' -f ('-'*13), ('-'*11), ('-'*29), ('-'*7), ('-'*42)
$ready = 0
foreach ($vm in $vms) {
$powered = ($vm.PowerState -eq 'PoweredOn')
# Only guestToolsRunning means the channel is up. guestToolsExecutingScripts
# is a different state and is not the one to copy into.
$tools = ($vm.ToolsRunningStatus -eq 'guestToolsRunning')
$ok = ($powered -and $tools)
if ($ok) { $ready++ }
$fix = if (-not $powered) { 'power the VM on, then re-read Tools' }
elseif ($vm.ToolsRunningStatus -eq 'guestToolsExecutingScripts') { 'Tools is busy: wait, then re-read' }
elseif (-not $tools) { 'start or install VMware Tools' }
else { 'nothing' }
'{0,-14}{1,-12}{2,-30}{3,-8}{4}' -f $vm.VMName, $vm.PowerState, $vm.ToolsRunningStatus, $ok, $fix
}
''
'{0,-22}{1}' -f 'VMs checked:', $vms.Count
'{0,-22}{1}' -f 'Ready to copy into:', $ready
'{0,-22}{1}' -f 'Not ready:', ($vms.Count - $ready)
You should see one row per VM with a Ready column and a suggested first fix, then three counts. One of the four rows is ready.
VM Power ToolsRunningStatus Ready What to fix first
------------- ----------- ----------------------------- ------- ------------------------------------------
SRV-PROD-01 PoweredOn guestToolsRunning True nothing
SRV-PROD-02 PoweredOn guestToolsNotRunning False start or install VMware Tools
SRV-APP-11 PoweredOff guestToolsNotRunning False power the VM on, then re-read Tools
SRV-APP-12 PoweredOn guestToolsExecutingScripts False Tools is busy: wait, then re-read
VMs checked: 4
Ready to copy into: 1
Not ready: 3
Get-VM when you have a connection. Building accurate inventory queries with Get-VM covers that side.
guestToolsExecutingScripts is not broken and does not need Tools reinstalled. It is busy, and the right move is to wait and read the field again.
The wrapper you keep
Everything so far goes into one script you can call instead of the cmdlet. It checks the path pair, composes the exact command line, and with -DryRun it stops there. A dry run needs no PowerCLI, no connection and no credentials, which makes it safe to run while you are still arguing about the paths.
This script loads GuestPathRules.ps1 from its own folder, so both files have to sit in the same place.
Save this as Invoke-GuestFileCopy.ps1 in C:\vmguestlab and run it with .\Invoke-GuestFileCopy.ps1 -VMName SRV-PROD-01 -Direction LocalToGuest -Source '.\payload.txt' -Destination 'C:\Temp\' -Force -ToolsWaitSecs 60 -DryRun.
It has mandatory parameters, so the bare file name would only make it prompt.
<#
Invoke-GuestFileCopy.ps1
A wrapper that checks the documented path rules before it calls
Copy-VMGuestFile, and that can compose the command line without running
it. -DryRun needs no PowerCLI, no connection and no credentials.
Run it with -DryRun first, every time. The composed line it prints is the
line that would run.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$VMName,
[Parameter(Mandatory)][ValidateSet('GuestToLocal','LocalToGuest')][string]$Direction,
[Parameter(Mandatory)][string]$Source,
[Parameter(Mandatory)][string]$Destination,
[System.Management.Automation.PSCredential]$GuestCredential,
[int]$ToolsWaitSecs,
[switch]$Force,
[switch]$DryRun
)
. "$PSScriptRoot/GuestPathRules.ps1"
$verdict = Test-GuestCopyPair -Direction $Direction -Source $Source -Destination $Destination
'{0,-22}{1}' -f 'VM:', $VMName
'{0,-22}{1}' -f 'Direction:', $Direction
'{0,-22}{1}' -f 'Path on the VM:', $verdict.GuestSide
'{0,-22}{1}' -f 'Source shape:', $verdict.SourceShape
'{0,-22}{1}' -f 'Destination shape:', $verdict.TargetShape
'{0,-22}{1}' -f 'Wildcard in source:', $verdict.WildcardSource
'{0,-22}{1}' -f 'Rule check:', $verdict.Reason
if (-not $verdict.Allowed) {
'{0,-22}{1}' -f 'Composed command:', '(none: the pair was rejected)'
# A non-zero exit code so a scheduled caller can tell the difference
# between "copied" and "never attempted".
exit 2
}
# Build the argument list in the order the reference prints it, so the line
# can be compared against the documentation by eye.
$parts = @(
'Copy-VMGuestFile'
"-Source '$Source'"
"-Destination '$Destination'"
"-VM '$VMName'"
"-$Direction"
)
if ($Force) { $parts += '-Force' }
# A non-positive -ToolsWaitSecs means "wait forever" to the cmdlet, so 0 cannot
# be used here as "not specified". Ask the binder whether it was passed at all.
$waitGiven = $PSBoundParameters.ContainsKey('ToolsWaitSecs')
if ($waitGiven) { $parts += "-ToolsWaitSecs $ToolsWaitSecs" }
# The credential is referenced by variable name, never expanded: a composed
# line is something people paste into chat tickets.
$parts += '-GuestCredential $cred'
$line = $parts -join ' '
'{0,-22}{1}' -f 'Composed command:', $line
if ($DryRun) {
'{0,-22}{1}' -f 'Dry run:', 'nothing was copied and nothing was connected'
exit 0
}
if (-not (Get-Command -Name Copy-VMGuestFile -ErrorAction SilentlyContinue)) {
throw 'Copy-VMGuestFile is not available in this session. Import VMware.VimAutomation.Core and connect with Connect-VIServer first.'
}
if (-not $GuestCredential) {
throw 'A real run needs -GuestCredential. Build one with Get-Credential.'
}
# A hashtable literal evaluates a variable used as a key, so the line below
# adds either GuestToLocal or LocalToGuest to the splat, never both.
$splat = @{
Source = $Source
Destination = $Destination
VM = $VMName
GuestCredential = $GuestCredential
$Direction = $true
}
if ($Force) { $splat['Force'] = $true }
if ($waitGiven) { $splat['ToolsWaitSecs'] = $ToolsWaitSecs }
# The cmdlet's documented return type is None, so there is nothing to capture
# here. Absence of an error is the only thing the call itself tells you.
Copy-VMGuestFile @splat
'{0,-22}{1}' -f 'Call returned:', 'no object (documented return type is None)'
You should see seven labelled lines, then the composed command, then a line confirming that nothing ran. The composed line is the line that would have run.
VM: SRV-PROD-01
Direction: LocalToGuest
Path on the VM: Destination
Source shape: Relative
Destination shape: Windows
Wildcard in source: False
Rule check: matches the documented rules
Composed command: Copy-VMGuestFile -Source '.\payload.txt' -Destination 'C:\Temp\' -VM 'SRV-PROD-01' -LocalToGuest -Force -ToolsWaitSecs 60 -GuestCredential $cred
Dry run: nothing was copied and nothing was connected
Now the same wrapper with the pair from the mistake above, a relative source read out of a guest. It composes nothing and exits with code 2, so a scheduled caller can tell "copied" from "never attempted".
.\Invoke-GuestFileCopy.ps1 -VMName SRV-PROD-01 -Direction GuestToLocal -Source 'logs\u_ex260104.log' -Destination 'collected' -DryRun
VM: SRV-PROD-01
Direction: GuestToLocal
Path on the VM: Source
Source shape: Relative
Destination shape: Relative
Wildcard in source: False
Rule check: Source is on the VM and must be an absolute path
Composed command: (none: the pair was rejected)
exit code: 2
-ToolsWaitSecs. A non-positive value means "wait indefinitely" to the cmdlet, so zero cannot be used as "not specified". The script asks $PSBoundParameters whether the parameter was passed at all, and passes it on only then. A default of zero would have turned every call into one that can hang.
What the call returns, and how to know the bytes arrived
A successful copy returns nothing. There is no size, no path and no object, so the way to know the file is intact is to ask both sides for a hash and compare them. The script below hashes the copy it can reach and prints the exact line to run inside the guest.
Save this as Compare-GuestCopy.ps1 in C:\vmguestlab and run it with .\Compare-GuestCopy.ps1 -LocalPath 'payload.txt' -GuestPath 'C:\Temp\payload.txt' -GuestHash <hash from the guest>.
<#
Compare-GuestCopy.ps1
Copy-VMGuestFile has a documented return type of None, so a call that
returns quietly has told you nothing about the bytes. This hashes the copy
you can reach and compares it with the hash of the other side.
-GuestHash is the value produced inside the guest. The script prints the
exact line to run there.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$LocalPath,
[string]$GuestPath = 'C:\Temp\payload.txt',
[string]$GuestHash,
[ValidateSet('SHA256','SHA1','MD5')][string]$Algorithm = 'SHA256'
)
if (-not (Test-Path -LiteralPath $LocalPath)) {
throw "Local file not found: $LocalPath"
}
$local = Get-FileHash -LiteralPath $LocalPath -Algorithm $Algorithm
'{0,-18}{1}' -f 'Algorithm:', $Algorithm
'{0,-18}{1}' -f 'Local file:', $LocalPath
'{0,-18}{1}' -f 'Local bytes:', (Get-Item -LiteralPath $LocalPath).Length
'{0,-18}{1}' -f 'Local hash:', $local.Hash
# The first fragment is double quoted so the path and the algorithm are filled
# in here. The second is single quoted so $_ survives for the guest shell.
$guestCommand = "Get-FileHash -LiteralPath '$GuestPath' -Algorithm $Algorithm | " +
'ForEach-Object { $_.Hash }'
''
'Run this inside the guest, through Invoke-VMScript:'
' ' + $guestCommand
if (-not $GuestHash) {
''
'{0,-18}{1}' -f 'Verdict:', 'no -GuestHash supplied, nothing compared'
exit 0
}
# Hex digests differ only in case between tools, so compare case-insensitively
# and on the trimmed string: a captured value often arrives with whitespace.
$match = [string]::Equals($local.Hash.Trim(), $GuestHash.Trim(),
[System.StringComparison]::OrdinalIgnoreCase)
''
'{0,-18}{1}' -f 'Guest hash:', $GuestHash.Trim()
'{0,-18}{1}' -f 'Lengths equal:', ($local.Hash.Trim().Length -eq $GuestHash.Trim().Length)
'{0,-18}{1}' -f 'Verdict:', $(if ($match) { 'MATCH: the copy is byte for byte identical' } else { 'MISMATCH: do not trust this copy' })
if ($match) { exit 0 } else { exit 3 }
Run it first with no -GuestHash to get the guest side line, send that line through Invoke-VMScript, then run it again with the value that came back. The first capture is the matching case.
# A single quoted here-string hands the text over exactly as written, so
# $_ is read by the guest shell rather than expanded by yours. The guest runs
# its own PowerShell, and nothing here depends on guest networking
$guest = @'
Get-FileHash -LiteralPath 'C:\Temp\payload.txt' -Algorithm SHA256 | ForEach-Object { $_.Hash }
'@
Invoke-VMScript -VM 'SRV-PROD-01' -GuestCredential $cred -ScriptText $guest
Algorithm: SHA256
Local file: payload.txt
Local bytes: 48
Local hash: 72E2E409B5280D371EF79717361ACC0E9305E068B8A602240DB414C46FD05E42
Run this inside the guest, through Invoke-VMScript:
Get-FileHash -LiteralPath 'C:\Temp\payload.txt' -Algorithm SHA256 | ForEach-Object { $_.Hash }
Guest hash: 72E2E409B5280D371EF79717361ACC0E9305E068B8A602240DB414C46FD05E42
Lengths equal: True
Verdict: MATCH: the copy is byte for byte identical
And the failing case, with the hash of the truncated file standing in for a copy that did not arrive whole. The exit code is 3, which is the one worth checking in a scheduled job.
Algorithm: SHA256
Local file: payload.txt
Local bytes: 48
Local hash: 72E2E409B5280D371EF79717361ACC0E9305E068B8A602240DB414C46FD05E42
Run this inside the guest, through Invoke-VMScript:
Get-FileHash -LiteralPath 'C:\Temp\payload.txt' -Algorithm SHA256 | ForEach-Object { $_.Hash }
Guest hash: C97AB1814010AE5385790B1B6ABF443D1C4200BE2F67DC636D175A74D9C7A337
Lengths equal: True
Verdict: MISMATCH: do not trust this copy
exit code: 3
Privileges, and the names you may be told you need
A guest copy is a privileged vSphere operation, and the privilege it needs is not the one that lets you see the VM. Against vCenter Server or ESXi 5.0 and later the reference names one privilege: VirtualMachine.GuestOperations.Modify. The two older names it also lists, VirtualMachine.Interact.ConsoleInteract and VirtualMachine.Interact.GuestControl, belong to versions earlier than 5.0.
The vSphere security documentation lists five guest operations privileges. The one that matters here is described as allowing operations "that involve modifications to a guest operating system in a virtual machine, such as transferring a file to the virtual machine".
| Privilege in the client | Internal name | Needed for a guest copy |
|---|---|---|
| Guest operation modifications | VirtualMachine.GuestOperations.Modify | Yes, this is the one |
| Guest Operation Queries | VirtualMachine.GuestOperations.Query | No, but it is what lists files in the guest |
| Guest operation program execution | VirtualMachine.GuestOperations.Execute | For Invoke-VMScript, not for the copy |
| Guest operation alias query | VirtualMachine.GuestOperations.QueryAliases | No |
| Guest operation alias modification | VirtualMachine.GuestOperations.ModifyAliases | No |
A Linux guest, and the tools you are not using
Nothing about the cmdlet is Windows specific on the guest side. A Linux guest takes an absolute POSIX destination and the guest account needs write permission on it, which is the usual reason a copy into /opt fails and the same copy into /tmp succeeds.
# The destination is inside the guest, so it is absolute. The source is here
# and may be relative. No flag on this cmdlet can grant the guest account
# permission to write where it is not allowed to
Copy-VMGuestFile -Source '.\payload.txt' -Destination '/tmp/' -VM 'SRV-APP-12' -LocalToGuest -GuestCredential $cred
The tool you would otherwise reach for needs the thing this cmdlet does not: a route to the guest, a listening service and a credential that service accepts.
# Needs sshd running in the guest, port 22 open along the path, and the guest
# reachable from here. All three are exactly what fails on a broken VM
scp ./payload.txt admin@srv-app-12.corp.local:/tmp/
| Route in | Needs the guest network | Needs a service in the guest | Needs vCenter privileges |
|---|---|---|---|
Copy-VMGuestFile | No | VMware Tools | Yes |
scp or sftp | Yes | sshd | No |
| A mapped share | Yes | File and printer sharing | No |
| An ISO mounted to the VM | No | None | Yes |
When it fails: the faults underneath
Each of the two transfer operations on GuestFileManager documents twelve faults, the same twelve in both directions. They are API fault names rather than PowerCLI message text, and these are the six that map onto something you can fix.
| Fault | Documented meaning | Where to look |
|---|---|---|
InvalidPowerState | "Thrown if the VM is not powered on." | The Power column of the readiness check |
GuestOperationsUnavailable | "Thrown if the VM agent for guest operations is not running." | VMware Tools in the guest |
InvalidGuestLogin | "Thrown if the the guest authentication information was not accepted." | The guest account, not the vSphere one |
GuestPermissionDenied | "Thrown if the operation fails because the guest authentication will not allow the operation." | Permissions on the destination path |
OperationNotSupportedByGuest | "Thrown if the operation is not supported by the guest OS." | The guest OS and the Tools version |
FileFault | "Thrown if there is a file error in the guest operating system." | The path itself, and whether it needs -Force |
The other six are GuestComponentsOutOfDate, GuestOperationsFault, InvalidState, OperationDisabledByGuest, RuntimeFault and TaskInProgress. Two of them, the out of date components one and the disabled by guest one, point at the Tools configuration rather than at anything in your command line.
InvalidGuestLogin as a vSphere permission problem. It is the guest account. The vSphere side has its own fault for that, and the privilege table above is where to start when the message is about privileges instead.
Hidden gems
Four things in the reference that change how you write the call, and one that changes how you read it.
-Source is an array and -Destination is not. The types are String[] and String, so one call can name several sources into one destination folder. That is the supported way to copy a handful of named files, and it is the thing the second published example tries to do with a pipeline.
# Two named sources, one destination. The array binds to -Source, which takes
# String[]; the destination stays a single path
$files = @('C:\vmguestlab\payload.txt', 'C:\vmguestlab\payload-short.txt')
Copy-VMGuestFile -Source $files -Destination 'C:\Temp\' -VM 'SRV-PROD-01' -LocalToGuest -Force -GuestCredential $cred
-Force creates directories and nothing else. It is documented as creating "the non-existing directories in the specified destination path". It does not overwrite anything you would otherwise be warned about, and it does not relax a permission in the guest.
A non-positive -ToolsWaitSecs waits indefinitely. That is the documented behaviour of zero and of any negative number, which makes zero a bad default in any wrapper you write around the cmdlet.
The host credential parameters are for versions you do not run. -HostUser, -HostPassword and -HostCredential are documented as needed only against vCenter or ESX earlier than 4.0, or with a VIX earlier than 1.10. Older examples pass them anyway, and copying that habit means handing a second set of credentials to a parameter the documentation says you do not need.
Read the cmdlet name as a hint rather than a limit. The synopsis is "copies files and folders", so a source that names a directory is a recursive copy. Check what you pointed at before running it against a log directory that has been rotating for two years.
Where this matters
- A VM you locked yourself out of. The copy channel does not care that the guest has the wrong subnet mask, so the log that explains the mistake is still reachable.
- An appliance with VMware Tools and no shell you are allowed to use. The copy channel is still there when SSH is disabled by the vendor, so a support bundle can come out the same way.
- Seeding a file onto many guests. One credential, one loop, no WinRM and no SSH to arrange first. Get-VM inventory reporting is how you build the list.
- A network segment you cannot route to. You need a route to vCenter, not to the guest, which is the usual shape of a DMZ or a test network your workstation cannot reach.
- Collecting evidence before a reboot. Pulling a crash log out of a guest that is about to be restarted, without logging in and changing anything inside it.
Tips and limitations
- The direction switch is mandatory and there is no default. Leaving it out and passing both produce the same error, so count the switches rather than reading the message.
- The path that points at the VM must be absolute. Which one that is depends on the switch, not on the parameter name.
- Wildcards are allowed on
-Sourceonly, at the end of the path, and reading out of a guest with a wildcard is documented as needing vCenter Server 5.0 or later. -GuestPasswordis aSecureString. Use-GuestCredentialwith aPSCredentialand the problem does not arise.- A successful call returns nothing, so verify with a hash on both sides if the file matters.
- The vSphere account needs
VirtualMachine.GuestOperations.Modifyon the VM, and the guest account needs write permission on the destination. Both can fail. - SSPI is documented as not supported, so the guest credentials are always explicit.
- PowerCLI 13.3 runs on Windows PowerShell 5.1 and PowerShell 7 on Windows, and on PowerShell 7 only on Linux and macOS. The measurements on this page are from PowerShell 7.4.6.
Clean up the lab
The last script lists everything this page created and confirms the folder holds nothing else. Nothing was installed, no module was imported, no vCenter object was changed and no guest was written to.
Save this as Remove-GuestCopyLab.ps1 in C:\vmguestlab and run it with .\Remove-GuestCopyLab.ps1.
<#
Remove-GuestCopyLab.ps1
Lists everything this article created and confirms it is all in one place.
Nothing was installed, no module was imported, no vCenter object was
changed and no guest was touched: the lab is nine script files and two
text files in a single folder.
The check is relative to this script's own folder, so it reports the same
thing whatever the folder is called.
#>
$expected = @(
'GuestPathRules.ps1'
'Test-GuestPath.ps1'
'Test-CopyDirection.ps1'
'Test-GuestPassword.ps1'
'Test-CopyParameterSet.ps1'
'Test-GuestCopyReady.ps1'
'Invoke-GuestFileCopy.ps1'
'Compare-GuestCopy.ps1'
'Remove-GuestCopyLab.ps1'
'payload.txt'
'payload-short.txt'
)
'{0,-30}{1}' -f 'File', 'Present'
'{0,-30}{1}' -f ('-'*29), ('-'*7)
$present = 0
foreach ($f in $expected) {
# $PSScriptRoot is the folder holding this script, so the listing is true
# whether the lab sits on C: or anywhere else.
$hit = Test-Path -LiteralPath (Join-Path -Path $PSScriptRoot -ChildPath $f)
if ($hit) { $present++ }
'{0,-30}{1}' -f $f, $hit
}
''
'{0,-30}{1}' -f 'Files listed:', $expected.Count
'{0,-30}{1}' -f 'Files present:', $present
'{0,-30}{1}' -f 'Anything else here:', ((Get-ChildItem -LiteralPath $PSScriptRoot -File).Count - $present)
You should see eleven rows, all True, and a last line reading zero.
File Present
----------------------------- -------
GuestPathRules.ps1 True
Test-GuestPath.ps1 True
Test-CopyDirection.ps1 True
Test-GuestPassword.ps1 True
Test-CopyParameterSet.ps1 True
Test-GuestCopyReady.ps1 True
Invoke-GuestFileCopy.ps1 True
Compare-GuestCopy.ps1 True
Remove-GuestCopyLab.ps1 True
payload.txt True
payload-short.txt True
Files listed: 11
Files present: 11
Anything else here: 0
Then remove the folder from a prompt that is not sitting inside it.
Set-Location 'C:\'
Remove-Item -LiteralPath 'C:\vmguestlab' -Recurse -Force
Official documentation
- Copy-VMGuestFile Command, VMware PowerCLI Reference
- Compatibility Matrix for VMware PowerCLI, Broadcom Techdocs
- GuestFileManager managed object, vSphere Web Services API
- GuestInfo data object, vSphere Web Services API
- Virtual Machine Guest Operations Privileges, vSphere Security
- How to copy files from a non-networked virtual machine using PowerCLI, Broadcom knowledge base
- Path.IsPathRooted Method, System.IO, Microsoft Learn
- Path.DirectorySeparatorChar Field, System.IO, Microsoft Learn
- Split-Path, Microsoft.PowerShell.Management, Microsoft Learn
- ConvertTo-SecureString, Microsoft.PowerShell.Security, Microsoft Learn
- Get-Credential, Microsoft.PowerShell.Security, Microsoft Learn
- Get-FileHash, Microsoft.PowerShell.Utility, Microsoft Learn
Related tools
- PowerCLI Command Builder: build the connection and inventory commands that come before any guest copy.
- Hash generator: hash a file in the browser when you want a second opinion on what the guest reported.
Related guides
- Running a script inside a VM with Invoke-VMScript: the same guest channel, used to run the hash command this page sends into the guest.
- Get-FileHash and certutil hashfile on Windows: the comparison step, and the ways a digest comparison reports a good file as bad.
- Get-VM inventory reporting in PowerCLI: building the VM list a bulk copy loops over.
- Reading guest disk free space with Get-VMGuestDisk: check there is room in the guest before you copy a large file into it.
- VMware PowerCLI command reference: where this cmdlet sits among the rest of the guest cmdlets.
- VMware PowerCLI cheat sheet: the one page version for the commands around this one.
Five cheat sheets, one PDF
Subnet masks, PowerShell, Linux commands, HTTP status codes and the ESXi command line - one page each, free to keep. Leave an address and it arrives in a minute.