4

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.

Craig
  • 627
  • 7
  • 14
  • 1
    What about: `Get-Item -Path '.\myfile.ext' | Get-VHD`? Does it yield the same results? – Abraham Zinala Dec 19 '21 at 16:55
  • 2
    `[` and `]` are special characters and this might be the reason why the cmdlet is failing, cmdlets like `Get-ChildItem` have a `-LiteralPath` for cases like this however not sure if `Get-Vhd` have it. – Santiago Squarzon Dec 19 '21 at 17:10
  • 1
    @AbrahamZinala... same result. – Craig Dec 19 '21 at 19:47
  • 1
    @SantiagoSquarzon with no -LiteralPath option available for Get-VHD, I fear that the ultimate solution will be discontinuing the use of the square brackets in their file names. – Craig Dec 19 '21 at 19:49
  • 1
    Yeah, if that's a possibility then the best you can do is stop using special chars on the paths. I do think that mklement0's answer should be [accepted](https://meta.stackexchange.com/questions/5234/how-does-accepting-an-answer-work/5235#5235) though, his answer explain the issue you're facing in depth and even tho it could not be fixed, it's a cmdlet (`Get-VHD`) issue... – Santiago Squarzon Dec 19 '21 at 19:54

2 Answers2

4

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.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • @Lee_Dailey, please see my update. – mklement0 Dec 19 '21 at 18:15
  • Thanks for all the feedback, @Craig. I've restructured the answer to paint what is hopefully now the complete picture, which includes a simplified version of your own answer, because if `Get-VM $ComputerName | Select-Object -Property VMId | Get-VHD` works, so does `Get-VM $ComputerName | Get-VHD`, because the implication is that the `Get-VM` output objects are automatically bound to `-VMId` by their `.VMId` property values - the `Select-Object` call is redundant, as it simply creates another object with a `.VMid` property (only). – mklement0 Dec 19 '21 at 22:37
  • @mklement0 Thanks so much for the help in steering towards the right answer on this. For what it's worth, I did originally try to bypass the select-object command as I too thought it would be redundant, but Get-VM apparently won't bind on the computer name alone. Maybe someone at Microsoft will want to look at revisiting Get-VHD down the road. ;) Also, something I haven't yet tried but probably will in the next month or 2 is using disknumber for when the VHDX file is mounted to the host hypervisor for compacting. I cross my fingers that it will have the same level of success as VMId. – Craig Dec 20 '21 at 03:56
  • Glad to hear I could help, @Craig; thank your for your repeated feedback that helped shape this answer. It definitely sounds like these cmdlets - which I have no personal experience with - need some work. As for why `Select-Object VMId` may be needed: I suspect that using the `Get-VM` output object directly would _also_ bind its `.Path` property. However, I _think_ that `(Get-VM $vmName).Id | Get-VHD` should work (I've updated the answer) though I suspect that if a different `-ComputerName` is targeted that _both_ `Get-VM` and `Get-VHD` must use that parameter. – mklement0 Dec 20 '21 at 04:52
0

Ok... So, I couldn't get the escape characters to be accepted by Get-VHD... be it by hand or programmatically. I gave it a go of passing it on the pipeline using Get-ChildItem too without success. However... I did manage to find an alternative for my particular use case. In addition to a path to a VHDX file, the Get-VHD command will also accept vmid, and disknumber as parameters. So, not that it's the way I wanted to go about obtaining what I need (because this method spits out info on all the attached drives), I can still manage to accomplish the task at hand by using the following example:

Get-VM $ComputerName | Select-Object -Property VMId | Get-VHD

By referencing them in this manner the Get-VHD commandlet is happy. This works for today's problem only because the VHDX files in question are attached to VMs. However, I'll still need to figure out about referencing unattached files at some point in the future. Which... Maybe ultimately require a slow and painful renaming of all the VHDX files to not use the square brackets in their name.

Craig
  • 627
  • 7
  • 14