Public/Copy-PVEVMGuestFile.ps1
|
function Copy-PVEVMGuestFile { <# .SYNOPSIS Copies a file between this computer and a VM, through the QEMU guest agent. .DESCRIPTION -LocalToGuest writes the local file into each VM with POST /nodes/{node}/qemu/{vmid}/agent/file-write. The API limits the content to 61440 characters; the file is sent base64-encoded (so any bytes copy unchanged), which limits it to 46080 bytes. A larger file is an error and nothing is sent. The guest file is replaced if it exists. Writes nothing. -GuestToLocal reads the guest file with GET .../agent/file-read, in chunks (the API limits one read to 16777216 bytes), and writes it to -Destination. Exactly one VM. An existing local file is an error unless -Force is given. Writes the local file's FileInfo. The VM must be running, with the agent running in the guest. The API documentation requires VM.GuestAgent.FileWrite or FileRead (or Unrestricted). .PARAMETER VMId VM ID(s). Binds from piped PVEPS.VM objects. .PARAMETER VMName VM name(s). Wildcards allowed. .PARAMETER Source The file to copy: a local path with -LocalToGuest, a guest path with -GuestToLocal. .PARAMETER Destination Where to put it: a guest path with -LocalToGuest, a local path with -GuestToLocal. .PARAMETER LocalToGuest Copy from this computer into the VM. .PARAMETER GuestToLocal Copy from the VM to this computer. .PARAMETER Force With -GuestToLocal, replace an existing local file. .PARAMETER Server Connection(s) to use. Defaults to the default connection; binds from piped objects. .EXAMPLE Copy-PVEVMGuestFile -VMId 100 -LocalToGuest -Source ./motd -Destination /etc/motd .EXAMPLE Copy-PVEVMGuestFile -VMId 100 -GuestToLocal -Source /var/log/syslog -Destination ./syslog-web01.txt #> [CmdletBinding(DefaultParameterSetName = 'IdToGuest', SupportsShouldProcess, ConfirmImpact = 'Medium')] [OutputType([System.IO.FileInfo])] param( [Parameter(Mandatory, ParameterSetName = 'IdToGuest', ValueFromPipelineByPropertyName)] [Parameter(Mandatory, ParameterSetName = 'IdToLocal', ValueFromPipelineByPropertyName)] [ValidateRange(100, 999999999)] [int[]]$VMId, [Parameter(Mandatory, ParameterSetName = 'NameToGuest', Position = 0)] [Parameter(Mandatory, ParameterSetName = 'NameToLocal', Position = 0)] [SupportsWildcards()] [ValidateNotNullOrEmpty()] [string[]]$VMName, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Source, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Destination, [Parameter(Mandatory, ParameterSetName = 'IdToGuest')] [Parameter(Mandatory, ParameterSetName = 'NameToGuest')] [switch]$LocalToGuest, [Parameter(Mandatory, ParameterSetName = 'IdToLocal')] [Parameter(Mandatory, ParameterSetName = 'NameToLocal')] [switch]$GuestToLocal, [Parameter(ParameterSetName = 'IdToLocal')] [Parameter(ParameterSetName = 'NameToLocal')] [switch]$Force, [Parameter(ValueFromPipelineByPropertyName)] [Alias('Connection')] [object[]]$Server ) begin { $maxWriteBytes = 46080 $chunkBytes = 4MB if ($LocalToGuest) { $localPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($Source) if (-not (Test-Path -LiteralPath $localPath -PathType Leaf)) { $PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new( [System.IO.FileNotFoundException]::new("Local file '$Source' was not found.", $localPath), 'PVELocalFileNotFound', [System.Management.Automation.ErrorCategory]::ObjectNotFound, $Source)) } $bytes = [System.IO.File]::ReadAllBytes($localPath) if ($bytes.Length -gt $maxWriteBytes) { $PSCmdlet.ThrowTerminatingError((New-PVEArgumentError -Message "'$Source' is $($bytes.Length) bytes; the guest agent file-write call takes at most $maxWriteBytes bytes (61440 base64 characters)." -Target $Source)) } $content = [Convert]::ToBase64String($bytes) } else { $localPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($Destination) if ((Test-Path -LiteralPath $localPath) -and -not $Force) { $PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new( [System.IO.IOException]::new("Local file '$Destination' exists; add -Force to replace it."), 'PVELocalFileExists', [System.Management.Automation.ErrorCategory]::ResourceExists, $Destination)) } } $targets = [System.Collections.Generic.List[psobject]]::new() } process { Add-PVEGuestTarget -Cmdlet $PSCmdlet -Target $targets -Server $Server -VMId $VMId -Name $VMName } end { $guests = @(Resolve-PVEGuestTarget -Cmdlet $PSCmdlet -Type qemu -Target $targets) if ($GuestToLocal -and $guests.Count -gt 1) { $PSCmdlet.ThrowTerminatingError((New-PVEArgumentError -Message "-GuestToLocal copies from exactly one VM; $($guests.Count) are targeted." -Target $guests)) } foreach ($guest in $guests) { $label = Get-PVEGuestLabel -Guest $guest if ($LocalToGuest) { if (-not $PSCmdlet.ShouldProcess($label, "Write $($bytes.Length) bytes from '$Source' to guest file '$Destination'")) { continue } try { $null = Invoke-PVEAgentRequest -Guest $guest -Command file-write -Method POST -Parameters @{ file = $Destination; content = $content; encode = 0 } } catch { $PSCmdlet.WriteError($_) } continue } if (-not $PSCmdlet.ShouldProcess($localPath, "Copy guest file '$Source' from $label")) { continue } $stream = $null try { $stream = [System.IO.File]::Open($localPath, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write) $offset = 0 do { $read = Invoke-PVEAgentRequest -Guest $guest -Command file-read -Parameters @{ file = $Source; offset = $offset; count = $chunkBytes; decode = 0 } $chunk = [Convert]::FromBase64String([string](Get-PVEValue $read 'content')) $stream.Write($chunk, 0, $chunk.Length) $offset += $chunk.Length $more = [bool][int](Get-PVEValue $read 'truncated') -and $chunk.Length -gt 0 } while ($more) $stream.Dispose() $stream = $null Get-Item -LiteralPath $localPath } catch { if ($stream) { $stream.Dispose() Remove-Item -LiteralPath $localPath -ErrorAction SilentlyContinue } $PSCmdlet.WriteError($_) } } } } |