Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PowerShell Get-VHD "is not an existing virtual hard disk file"

When creating a new VM in Hyper-V, to keep things organized, I use a particular naming convention when creating the associated VHDX files. The naming convention is the VMs FQDN followed by the SCSI controller attachment point followed by what the name of the drive is called or used for inside of the VM. I encapsulate the SCSI and Name parameters inside smooth and square brackets respectively. I find this tends to make things a little bit easier from a human perspective to match the VHDX files in Hyper-V to what the VM sees internally when needing to do maintenance tasks. It has also helped with scripting in the past. An example file name would look as follows...

servername.example.com(0-0)[OS].vhdx

This has worked well for quite some time, but recently I tried to run some PowerShell commands against the VHDX files and ran across a problem. Apparently the square brackets for the internal VM name are being parsed as RegEx or something inside of the PowerShell commandlet (I'm honestly just guessing on this). When I try to use Get-VHD on a file with the above naming convention it spits out an error as follows:

Get-VHD : 'E:\Hyper-V\servername.example.com\Virtual Hard Disks\servername.example.com(0-0)[OS].vhdx' is not an existing virtual hard disk file.
At line:1 char:12
+ $VhdPath | Get-VHD
+            ~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [Get-VHD], VirtualizationException
    + FullyQualifiedErrorId : InvalidParameter,Microsoft.Vhd.PowerShell.Cmdlets.GetVHD

If I simply rename the VHDX file to exclude the "[OS]" portion of the naming convention the command works properly. The smooth brackets for the SCSI attachment point don't seem to bother it. I've tried doing a replace command to add a backtick ''`'' in front of the brackets to escape them, but the same error results. I've also tried double backticks to see if passing in a backtick helped... that at least showed a single backtick in the error it spat out. Suspecting RegEx, I tried the backslash as an escape character too... which had the interesting effect of converting all the backslashes in the file path into double backslashes in the error message. I tried defining the path variable via single and double quotes without success. I've also tried a couple of different ways of obtaining it via pipeline such as this example...

((Get-VM $ComputerName).HardDrives | Select -First 1).Path | Get-VHD

And, for what it's worth, as many VMs as I am attempting to process... I need to be able to run this via pipeline or some other automation scriptable method rather than hand coding a reference to each VHDX file.

Still thinking it may be something with RegEx, I attempted to escape the variable string with the following to no avail:

$VhdPathEscaped = [System.Text.RegularExpressions.Regex]::Escape($VhdPath)

Quite frankly, I'm out of ideas.

When I first ran across this problem was when I tried to compact a VHDX file with PowerShell. But, since the single VM I was working with needed to be offline for that function to run anyway, rather than fight the error with the VHDX name, I simply renamed it, compacted it, and reset the name back. However, for the work I'm trying to do now, I can't afford to take the VM offline as this script is going to run against a whole fleet of live VMs. So, I need to know how to properly escape those characters so the Get-VHD commandlet will accept those file names.

like image 851
Craig Avatar asked Nov 01 '25 22:11

Craig


1 Answers

tl;dr:

  • A design limitation of Get-VHD prevents it from properly recognizing VHD paths that contain [ and ] (see bottom section for details).

  • Workaround: Use short (8.3) file paths assuming the file-system supports them:

$fso = New-Object -ComObject Scripting.FileSystemObject

$VhdPath | 
  ForEach-Object { $fso.GetFile((Convert-Path -LiteralPath $_)) } | 
    Get-VHD
  • Otherwise, your only options are (as you report, in your case the VHDs are located on a ReFS file-system, which does not support short names):

    • Rename your files (and folders, if applicable) to not contain [ or ].

    • Alternatively, if you can assume that your VHDs are attached to VMs, you can provide the VM(s) to which the VHD(s) of interests are attached as input to Get-VHD, via Get-VM (you may have to filter the output down to only the VHDs of interest):

      (Get-VM $vmName).Id | Get-VHD
      

Background information:

It looks like Get-VHD only has a -Path parameter, not also a -LiteralPath parameter, which looks like a design flaw:

Having both parameters is customary for file-processing cmdlets (e.g. Get-ChildItem):

  • -Path accepts wildcard expressions to match potentially multiple files by a pattern.

  • -LiteralPath is used to pass literal (verbatim) paths, to be used as-is.

What you have is a literal path that happens to look like a wildcard expression, due to use of metacharacters [ and ]. In wildcard contexts, these metacharacter must normally be escaped - as `[ and `] - in order to be treated as literals, which the following (regex-based) -replace operation ensures[1] (even with arrays as input).

  • Unfortunately, this appears not to be enough for Get-VHD. (Though you can verify that it works in principle by piping to Get-Item instead, which also binds to -Path).

    • Even double `-escaping (-replace '[][]', '``$&') doesn't work (which is - unexpectedly required in come cases - see GitHub issue #7999).
# !! SHOULD work, but DOES NOT
# !! Ditto for -replace '[][]', '``$&'
$VhdPath -replace '[][]', '`$&' | Get-VHD

Note: Normally, a robust way to ensure that a cmdlet's -LiteralPath parameter is bound by pipeline input is to pipe the output from Get-ChildItem or Get-Item to it.[2] Given that Get-VHD lacks -LiteralPath, this is not an option, however:

# !! DOES NOT HELP, because Get-VHD has no -LiteralPath parameter.
Get-Item -LiteralPath $VhdPath | Get-VHD

[1] See this regex101.com page for an explanation of the regex ($0 is an alias of $& and refers to the text captured by the match at hand, i.e. either [ or ]). Alternatively, you could pass all paths individually to the [WildcardPattern]::Escape() method (e.g., [WildcardPattern]::Escape('a[0].txt') yields a`[0`].txt.

[2] See this answer for the specifics of how this binding, which happens via the provider-supplied .PSPath property, works.

like image 183
mklement0 Avatar answered Nov 03 '25 21:11

mklement0