#Requires -Version:3 # bitwise operations needed # Public domain. <# EXAMPLES: 1. Import the module. $module_properties = @{ Name = 'C:\Path\to\vhd.psm1'; Force = $True; Verbose = $False; } if (Test-Path -Path:$module_properties.Name) { Import-Module @module_properties } 2. Create a new fixed-size VHD. This can be run from an unprivileged PowerShell prompt: $vhd_properties = @{ 'Path' = 'C:\Path\to\new.vhd'; 'SizeBytes' = 1 * $1GiB; 'CreatorApplication' = 'barf'; 'CreatorVersion' = @(0,0,0,1); 'CreatorHostOS' = 'blah'; }; New-VHDCustomFixed @vhd_properties 3. Create a new custom VHD and config file. This can be run from an unprivileged PowerShell prompt: $vmconfig_properties = @{ 'IsoPath' = 'C:\Path\to\my.iso'; 'Path' = 'C:\Path\to\config_vm.json'; 'Version' = (Get-Date).ToString('yyyymmDD'); 'VhdSizeBytes' = $1GiB * 16; 'VhdType' = 'Dynamic'; # This can be Fixed or Dynamic 'VMName' = 'my_vm'; }; New-VMCustomConfig @vmconfig_properties 4. Create a new VHD from the config file made in step (3): $vm_properties = @{ 'ConfigurationPath' = 'C:\Path\to\config_vm.json'; 'MemoryMinimumMB' = 2048; 'MemoryMaximumMB' = 4096; 'UseLegacyNetworkAdapter' = $True; 'StartVM' = $True; 'CheckpointVM' = $False; }; $vm = New-VMCustom @vm_properties #> Set-Variable -Option:ReadOnly -Name:1MiB -Value:([Math]::Pow(2,20)) Set-Variable -Option:ReadOnly -Name:1GiB -Value:([Math]::Pow(2,30)) Set-Variable -Option:ReadOnly -Name:VhdDefaultPath -Value:([System.IO.Path]::Combine(${Env:PUBLIC}, 'Documents', 'Hyper-V', 'Virtual hard disks')) Function Disable-SparseFile { Param( [Parameter(Mandatory=$True)] [String] $Path ) if (-not (Test-Path -Path:$Path)) { Write-Error 'Disable-SparseFile: file not found' Return } $attrib = (Get-ItemProperty -Path:$Path).Attributes if ([System.IO.FileAttributes]::SparseFile -band $attrib) { $item_property = @{ 'Path' = $Path; 'Name' = 'Attributes'; 'Value' = [System.IO.FileAttributes]::SparseFile -bxor $attrib; } Set-ItemProperty @item_property } Return } Function Format-VHD { Param( [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [String] $Path, [Parameter(Mandatory=$False)] [ValidateSet('FAT32', 'NTFS')] [String] $FsType = 'NTFS' ) if ('FAT32' -ieq $FsType) { $size_bytes = (Get-Item -Path:$Path).Length if ((64 * $1MiB) -gt $size_bytes) { Write-Error 'vhd file too small for FAT32' Return } } $vhd_obj = Mount-VHD -PassThru:$True -Path:$Path $disk_obj = Get-Disk -Number:$vhd_obj.DiskNumber if ('RAW' -eq $disk_obj.PartitionStyle) { Initialize-Disk -Confirm:$False -InputObject:$disk_obj -PartitionStyle:'MBR' } $part_obj = New-Partition -DiskNumber:$vhd_obj.DiskNumber -UseMaximumSize:$True -AssignDriveLetter:$False -MbrType:'IFS' -IsActive:$True Format-Volume -Partition:$part_obj -Force -Confirm:$False -FileSystem:$FsType | Out-Null Dismount-VHD -Path:$Path Return } Function Get-Checksum { Param( [Parameter(Mandatory=$True)] [Byte[]] $Bytes ) $offset = 64 if (1024 -eq $Bytes.Length) { $offset = 36 } (0..3) | foreach { $Bytes[$offset+$_] = 0 } $sum = ($Bytes | Measure-Object -Sum).Sum $chk = [System.BitConverter]::GetBytes( [System.Net.IPAddress]::HostToNetworkOrder([UInt32]::MaxValue - $sum) ) Return $chk[4..7] } Function New-RandStringHex { Param( [Parameter(Mandatory = $False)] [UInt16] $Length = 6 ) $rand_str = '' (1..$Length) | foreach { $rand_str += (Get-Random -Maximum:16).ToString('x') } Return $rand_str } Function New-VHDCustom { Param( [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [String] $Path, [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [UInt64] $SizeBytes, [Parameter(Mandatory = $False)] [ValidateSet('Fixed', 'Dynamic')] [String] $VhdType = 'Dynamic', [Parameter(Mandatory=$False)] [UInt32] $BlockSize = 2 * $1MiB, [Parameter(Mandatory=$False)] [String] $CreatorApplication = 'cust', [Parameter(Mandatory=$False)] [UInt16[]] $CreatorVersion = @(0,1,0,0), [Parameter(Mandatory=$False)] [String] $CreatorHostOS = 'Wi2k' ) $vhd_properties = @{ 'Path' = $Path; 'SizeBytes' = $SizeBytes; 'CreatorApplication' = $CreatorApplication; 'CreatorVersion' = $CreatorVersion; 'CreatorHostOS' = $CreatorHostOS; } if ('Fixed' -ieq $VhdType) { New-VHDCustomFixed @vhd_properties } if ('Dynamic' -ieq $VhdType) { $vhd_properties['BlockSize'] = $BlockSize New-VHDCustomDynamic @vhd_properties } Return } Function New-VHDCustomDynamicHeader { Param( [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [UInt64] $SizeBytes, [Parameter(Mandatory=$False)] [ValidateNotNullOrEmpty()] [UInt32] $BlockSize = (2 * $1MiB) ) $ascii = New-Object System.Text.ASCIIEncoding $footer = $ascii.GetBytes('cxsparse') # Cookie $footer += @(255,255,255,255,255,255,255,255) # DataOffset $footer += @(0,0,0,0,0,0,6,0) # TableOffset = 1536 $footer += @(0,1,0,0) # HeaderVersion $buf = [System.BitConverter]::GetBytes( [System.Convert]::ToUInt32(($SizeBytes / $BlockSize)) ) [Array]::Reverse($buf) $footer += $buf # MaxTableEntries $buf = [System.BitConverter]::GetBytes($BlockSize) [Array]::Reverse($buf) $footer += $buf # BlockSize $footer += New-Object byte[] (1024 - $footer.Length) $checksum = Get-Checksum -Bytes:$footer (0..3) | foreach { $footer[36+$_] = $checksum[$_] } Return $footer } Function New-VHDCustomDynamic { Param( [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [String] $Path, [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [UInt64] $SizeBytes, [Parameter(Mandatory=$False)] [UInt32] $BlockSize = 2 * $1MiB, [Parameter(Mandatory=$False)] [String] $CreatorApplication = 'cust', [Parameter(Mandatory=$False)] [UInt16[]] $CreatorVersion = @(0,1,0,0), [Parameter(Mandatory=$False)] [String] $CreatorHostOS = 'Wi2k' ) if (3145728 -gt $SizeBytes) { Write-Error "SizeBytes too low, minimum VHD size is 3145728" Return } if (4 -ne $CreatorApplication.Length) { Write-Error "CreatorApplication must be 4 ASCII bytes" Return } if (4 -ne $CreatorVersion.Length) { Write-Error "CreatorVersion must be array of size four, type UInt16" Return } if (4 -ne $CreatorHostOS.Length) { Write-Error "CreatorHostOS must be 4 ASCII bytes" Return } $vhd_dir = [System.IO.Path]::GetDirectoryName($Path) if (-not (Test-Path -Path:$vhd_dir)) { Write-Error 'vhd target dir not found' Return } $vhd_file_path = [System.IO.Path]::GetFileName($Path) $file_name_tmp = [System.IO.Path]::Combine($vhd_dir, ("{0}.tmp" -f $vhd_file_path)) $vhd_footer_properties = @{ 'SizeBytes' = $SizeBytes; 'CreatorApplication' = $CreatorApplication; 'CreatorVersion' = $CreatorVersion; 'CreatorHostOS' = $CreatorHostOS; 'Fixed' = $False; 'Dynamic' = $True; }; $footer = New-VHDCustomFooter @vhd_footer_properties $vhd_dynamic_header_properties = @{ 'SizeBytes' = $SizeBytes; 'BlockSize' = $BlockSize; }; $header = New-VHDCustomDynamicHeader @vhd_dynamic_header_properties $fs = [System.IO.File]::Create($file_name_tmp) $fs.SetLength(0) $fs.Write($footer, 0, $footer.Length) $fs.Write($header, 0, $header.Length) # BAT # 1. The size of the BAT is calculated during creation of the hard disk. # 2. The number of entries in the BAT is the number of blocks needed to store the contents of the disk when fully expanded. # For example, a 2-GiB disk image that uses 2-MiB blocks requires 1024 BAT entries. # 3. Each entry is four bytes long. # 4. All unused table entries are initialized to 0xFFFFFFFF. # 5. The "Max Table Entries" field within the Dynamic Disk Header indicates how many entries are valid. $max_table_entries = [System.Convert]::ToUInt32(($SizeBytes / $BlockSize)) # Write-Host ("table entries: {0}" -f ($max_table_entries)) # DEBUG $table_empty = @(255,255,255,255) for ($i = 0; $i -lt $max_table_entries; $i++) { $fs.Write($table_empty,0,4) } # The BAT is always extended to a sector boundary. while (0 -ne ($fs.Length % 4096)) { $fs.WriteByte([Byte] 255) } $offset_remainder = $fs.Length % 8192 $fs.SetLength($fs.Length + $offset_remainder) $fs.Seek(-512, [System.IO.SeekOrigin]::End) | Out-Null $fs.Write($footer, 0, $footer.Length) $fs.Close() Disable-SparseFile -Path:$file_name_tmp Move-Item -Path:$file_name_tmp -Destination:$Path -Force:$True -Confirm:$False Return } Function New-VHDCustomFixed { Param( [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [String] $Path, [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [UInt64] $SizeBytes, [Parameter(Mandatory=$False)] [String] $CreatorApplication = 'cust', [Parameter(Mandatory=$False)] [Byte[]] $CreatorVersion = @(0,1,0,0), [Parameter(Mandatory=$False)] [String] $CreatorHostOS = 'Wi2k' ) if (3145728 -gt $SizeBytes) { Write-Error "SizeBytes too low, minimum VHD size is 3145728" Return } if (4 -ne $CreatorApplication.Length) { Write-Error "CreatorApplication must be 4 ASCII bytes" Return } if (4 -ne $CreatorVersion.Length) { Write-Error "CreatorVersion must be array of size four, type UInt16" Return } if (4 -ne $CreatorHostOS.Length) { Write-Error "CreatorHostOS must be 4 ASCII bytes" Return } $vhd_dir = [System.IO.Path]::GetDirectoryName($Path) if (-not (Test-Path -Path:$vhd_dir)) { Write-Error 'vhd target dir not found' Return } $vhd_file_path = [System.IO.Path]::GetFileName($Path) $file_name_tmp = [System.IO.Path]::Combine($vhd_dir, ("{0}.tmp" -f $vhd_file_path)) $vhd_footer_properties = @{ 'SizeBytes' = $SizeBytes; 'CreatorApplication' = $CreatorApplication; 'CreatorVersion' = $CreatorVersion; 'CreatorHostOS' = $CreatorHostOS; 'Fixed' = $True; 'Dynamic' = $False; }; $footer = New-VHDCustomFooter @vhd_footer_properties $fs = [System.IO.File]::Create($file_name_tmp) $fs.SetLength($SizeBytes) $fs.Seek(0, [System.IO.SeekOrigin]::End) | Out-Null $fs.Write($footer, 0, $footer.Length) $fs.Close() Disable-SparseFile -Path:$file_name_tmp Move-Item -Path:$file_name_tmp -Destination:$Path -Force:$True -Confirm:$False Return } Function New-VHDCustomFooter { Param( [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [UInt64] $SizeBytes, [Parameter(Mandatory=$False)] [String] $CreatorApplication = 'cust', [Parameter(Mandatory=$False)] [Byte[]] $CreatorVersion = @(0,1,0,0), [Parameter(Mandatory=$False)] [String] $CreatorHostOS = 'Wi2k', [Parameter(Mandatory=$False)] [Switch] $Fixed = $null, [Parameter(Mandatory=$False)] [Switch] $Dynamic = $null ) if (([String]::IsNullOrEmpty($Fixed)) -and ([String]::IsNullOrEmpty($Dynamic))) { $Fixed = $True # default to Fixed } if ((-not ([String]::IsNullOrEmpty($Fixed))) -and (-not ([String]::IsNullOrEmpty($Dynamic)))) { if ($Fixed -and $Dynamic) { Write-Error "must choose either Fixed or Dynamic" Return } } if (3145728 -gt $SizeBytes) { Write-Error "SizeBytes too low, minimum VHD size is 3145728" Return } if (4 -ne $CreatorApplication.Length) { Write-Error "CreatorApplication must be 4 ASCII bytes" Return } if (4 -ne $CreatorVersion.Length) { Write-Error "CreatorVersion must be array of size four, type UInt16" Return } if (4 -ne $CreatorHostOS.Length) { Write-Error "CreatorHostOS must be 4 ASCII bytes" Return } $ascii = New-Object System.Text.ASCIIEncoding $epoch = New-Object System.DateTimeOffset 2000,1,1,0,0,0,0 $footer = $ascii.GetBytes('conectix') # Cookie $footer += @(0,0,0,2) # Features $footer += @(0,1,0,0) # FileFormatVersion if ($Fixed) { $footer += @(255,255,255,255,255,255,255,255) # DataOffset } if ($Dynamic) { $footer += @(0,0,0,0,0,0,2,0) # DataOffset } $ts = (Get-Date).ToUniversalTime() - $epoch.DateTime $buf_ts = [System.BitConverter]::GetBytes( [System.Convert]::ToUInt32([Math]::Floor($ts.TotalSeconds)) ) [Array]::Reverse($buf_ts) $footer += $buf_ts # Timestamp $footer += $ascii.GetBytes($CreatorApplication) # CreatorApplication $footer += $CreatorVersion $footer += $ascii.GetBytes($CreatorHostOS) # CreatorHostOS $footer += [System.BitConverter]::GetBytes( [System.Net.IPAddress]::HostToNetworkOrder($SizeBytes) ) # OriginalSize $footer += [System.BitConverter]::GetBytes( [System.Net.IPAddress]::HostToNetworkOrder($SizeBytes) ) # CurrentSize $footer += Set-CHS -Sectors:([System.Convert]::ToDouble($SizeBytes / 512)) # DiskGeometry if ($Fixed) { $footer += @(0,0,0,2) # DiskType } if ($Dynamic) { $footer += @(0,0,0,3) # DiskType } $footer += @(0,0,0,0) # Checksum init $id = New-Object byte[] 16 $rnd = New-Object System.Random $rnd.NextBytes($id) $footer += $id # UniqueID $footer += 0 # SavedState $checksum = Get-Checksum -Bytes:$footer (0..3) | foreach { $footer[64+$_] = $checksum[$_] } $footer += New-Object byte[] (512 - $footer.Length) Return $footer } Function New-VMCustom { Param( [Parameter(Mandatory = $True)] [String] $ConfigurationPath, [Parameter(Mandatory = $False)] [UInt16] $MemoryMinimumMB = 512, [Parameter(Mandatory = $False)] [UInt16] $MemoryMaximumMB = 1024, [Parameter(Mandatory = $False)] [Switch] $UseLegacyNetworkAdapter = $True, [Parameter(Mandatory = $False)] [Switch] $StartVM = $True, [Parameter(Mandatory = $False)] [Switch] $CheckpointVM = $True ) $vm_config = Read-VMCustomConfig -FilePath:$ConfigurationPath if ([String]::IsNullOrEmpty($vm_config.VMName)) { Write-Error 'invalid config' Return } else { $vm_name = $vm_config.VMName } if (-not ([String]::IsNullOrEmpty($vm_config.Version))) { $vm_name += "-{0}" -f ($vm_config.Version) } $vm_properties = @{ 'VMName' = $vm_name; 'IsoPath' = $vm_config.IsoPath; 'UseLegacyNetworkAdapter' = $UseLegacyNetworkAdapter; 'VhdPath' = $vm_config.VhdPath; 'MemoryMinimumBytes' = $MemoryMinimumMB * $1MiB; 'MemoryMaximumBytes' = $MemoryMaximumMB * $1MiB; 'MemoryStartupBytes' = $MemoryMinimumMB * $1MiB; 'StartVM' = $StartVM; 'CheckpointVM' = $CheckpointVM; } Return (New-VMCustomFull @vm_properties) } Function New-VMCustomFull { Param( [Parameter(Mandatory=$True)] [Alias('Name')] [String] $VMName, [Parameter(Mandatory=$True)] [String] $VHDPath, [Parameter(Mandatory=$False)] [String] $IsoPath = $null, [Parameter(Mandatory=$False)] [UInt16] $ProcessorCount = 1, [Parameter(Mandatory=$False)] [UInt64] $MemoryMinimumBytes = 512 * [Math]::Pow(2,20), [Parameter(Mandatory=$False)] [UInt64] $MemoryMaximumBytes = 2048 * [Math]::Pow(2,20), [Parameter(Mandatory=$False)] [UInt64] $MemoryStartupBytes = $MemoryMinimumBytes, [Parameter(Mandatory=$False)] [UInt64] $Generation = 1, [Parameter(Mandatory=$False)] [Switch] $UseLegacyNetworkAdapter = $False, [Parameter(Mandatory=$False)] [Switch] $UseVhdxFormat = $False, [Parameter(Mandatory=$False)] [Switch] $StartVM = $True, [Parameter(Mandatory=$False)] [Switch] $CheckpointVM = $True ) $vm_switch_name = Get-VMSwitch | Select-Object -First:1 -ExpandProperty:Name $vm_create_properties = @{ 'Name' = $VMName; 'MemoryStartupBytes' = $MemoryStartupBytes; 'Generation' = $Generation; 'VHDPath' = $VhdPath; }; $vm = New-VM @vm_create_properties # Remove the default unconfigured virtual network adapter and add your own: $vm_net_adapter = @{ 'VM' = $vm; 'IsLegacy' = [Boolean] $UseLegacyNetworkAdapter; 'SwitchName' = $vm_switch_name; } Remove-VMNetworkAdapter -VM:$vm Add-VMNetworkAdapter @vm_net_adapter # Update virtual DVD drive with path to ISO: if (Test-Path -Path:$IsoPath) { $dvd_drive = Get-VM -Id:$vm.VMId | Get-VMDvdDrive Set-VMDvdDrive -VMDvdDrive:$dvd_drive -Confirm:$False -Path:$IsoPath } $vm_set_properties = @{ 'VM' = $vm; 'DynamicMemory' = $True; 'MemoryMinimumBytes' = $MemoryMinimumBytes; 'MemoryMaximumBytes' = $MemoryMaximumBytes; 'ProcessorCount' = $ProcessorCount; 'CheckpointType' = 'Standard'; 'AutomaticCheckpointsEnabled' = $False; }; Set-VM @vm_set_properties | Out-Null if ($CheckpointVM) { Checkpoint-VM -VM:$vm | Out-Null } if ($StartVM) { Start-VM -VM:$vm | Out-Null } Return (Get-VM -Id:$vm.VMId) } Function New-VMCustomConfig { Param( [Parameter(Mandatory = $True)] [ValidateNotNullOrEmpty()] [String] $Path, [Parameter(Mandatory = $True)] [ValidateNotNullOrEmpty()] [String] $VMName, [Parameter(Mandatory = $True)] [ValidateNotNullOrEmpty()] [String] $IsoPath, [Parameter(Mandatory = $False)] [String] $Version, [Parameter(Mandatory = $False)] [String] $VhdPath, [Parameter(Mandatory = $False)] [UInt64] $VhdSizeBytes, [Parameter(Mandatory = $False)] [ValidateSet('Fixed', 'Dynamic')] [String] $VhdType = 'Dynamic' ) if (-not (Test-Path -Path:$VhdDefaultPath)) { Write-Error 'config path not found' Return } if ((-not ([String]::IsNullOrEmpty($VhdPath))) -and (-not ([String]::IsNullOrEmpty($VhdType)))) { Write-Error 'VhdPath and VhdType both set, choose only one' Return } if ([String]::IsNullOrEmpty($VMName)) { Write-Error 'VMName not set' Return } $vm_config = @{ 'IsoPath' = $IsoPath; 'VMName' = $VMName; } $timestamp = (Get-Date).ToString('yyyyMMdd_HHmmss') $vhd_file_name = "{0}-{1}.vhd" -f ($VMName, $timestamp) if (-not ([String]::IsNullOrEmpty($Version))) { $vm_config['VMName'] += "{0}" -f ($Version) $vhd_file_name = "{0}{1}-{2}.vhd" -f ($VMName, $Version, $timestamp) } if ([String]::IsNullOrEmpty($VhdPath)) { if ([String]::IsNullOrEmpty($VhdSizeBytes)) { Write-Error 'VhdSizeBytes not set' Return } $vhd_properties = @{ 'Path' = [System.IO.Path]::Combine($VhdDefaultPath, $vhd_file_name); 'SizeBytes' = $VhdSizeBytes; 'CreatorApplication' = 'cust'; 'CreatorVersion' = @(0,0,0,1); 'CreatorHostOS' = 'Wi2K'; 'VhdType' = $VhdType; }; New-VHDCustom @vhd_properties $vm_config['VhdPath'] = $vhd_properties.Path; } else { $vm_config['VhdPath'] = $VhdPath; } $config_path = [System.IO.Path]::Combine($VhdDefaultPath, $Path) Write-VMCustomConfig -FilePath:$config_path -InputObject:$vm_config Return } Function Prepare-VHD { Param( [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [String] $VhdVolumeLetter, [Parameter(Mandatory=$False)] [String] $SdeletePath = [System.IO.Path]::Combine(${Env:UserProfile}, 'bin', 'sdelete161.exe') ) Write-Host ("Begin {0}" -f ($MyInvocation.InvocationName)) $VhdVolumeBase = $VhdVolumeLetter + [System.IO.Path]::VolumeSeparatorChar $VhdVolumeRoot = $VhdVolumeBase + [System.IO.Path]::DirectorySeparatorChar $vhd_pagefiles = @('page', 'swap') | % {"{0}{1}file.sys" -f ($VhdVolumeRoot, $_)} Remove-Item -Verbose:$True -Path:$vhd_pagefiles -Force:$True -Confirm:$False -ErrorAction:SilentlyContinue Run-Process -ProcessName:'defrag.exe' -ProcessArguments:@($VhdVolumeRoot, '/V', '/H', '/X') Run-Process -ProcessName:$SdeletePath -ProcessArguments:@('-accepteula', '-z', $VhdVolumeBase) Write-Host ("End {0}" -f ($MyInvocation.InvocationName)) Return } Function Read-VMCustomConfig { Param( [Parameter(Mandatory=$True)] [String] $FilePath ) if (-not (Test-Path -Path $FilePath)) { Write-Error 'file not found' Return } $json = [System.IO.File]::ReadAllText($FilePath, [System.Text.Encoding]::UTF8) Return ConvertFrom-Json -InputObject:$json } Function Run-Process { Param( [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [String] $ProcessName, [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [String[]] $ProcessArguments ) Write-Host ("Begin {0} {1}" -f ($MyInvocation.InvocationName, $ProcessName)) $proc_properties = @{ 'Wait' = $True; 'NoNewWindow' = $True; 'FilePath' = $ProcessName; 'ArgumentList' = $ProcessArguments; } $t = Measure-Command -Expression { Start-Process @proc_properties } Write-Host ("{0} took {1:0} min {2:0}.{3:000} sec" -f ($ProcessName, $t.Minutes, $t.Seconds, $t.Milliseconds)) Write-Host ("End {0} {1}" -f ($MyInvocation.InvocationName, $ProcessName)) Return } Function Set-CHS { Param([Parameter(Mandatory=$True)] [Double] $Sectors) [Double] $c = 65535 [Double] $h = 16 [Double] $s = 255 if ($Sectors -gt ($c * $h * $s)) { $Sectors = $c * $h * $s } if ($Sectors -ge ($c * $h * 63)) { $c = $Sectors / $s } else { $s = 17 $c = $Sectors / $s $h = ($c + 1023) / 1024 if (4 -gt $h) { $h = 4 } if ( ($c -ge (1024 * $h)) -or (16 -lt $h) ) { $s = 31 $h = 16 $c = $Sectors / $s } if ($c -ge (1024 * $h)) { $s = 63 $h = 16 $c = $Sectors / $s } } $c = [Math]::Floor($c / $h) $outbuf = [System.BitConverter]::GetBytes([System.Convert]::ToUInt16($c)) [Array]::Reverse($outbuf) $outbuf += [System.Convert]::ToByte($h) $outbuf += [System.Convert]::ToByte($s) Return $outbuf } Function Shrink-VHD { Param( [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [String] $Path ) Write-Host ("Begin {0}" -f ($MyInvocation.InvocationName)) $vhd_len_old = (Get-Item -Path:$Path).Length $vhd_obj = Mount-VHD -Path:$Path -PassThru:$True -ReadOnly:$True $part_obj = Get-Partition -DiskNumber $vhd_obj.DiskNumber Optimize-VHD -Path:$Path Dismount-VHD -Path:$Path $vhd_len_new = (Get-Item -Path:$Path).Length $shrink_pct = 100 * (1 - ( $vhd_len_new / $vhd_len_old )) $shrink_pct_int = [Math]::Floor($shrink_pct) $shrink_pct_frac = [Math]::Floor(100 * ($shrink_pct - $shrink_pct_int)) $out = @() $out += "VHD len orig: {0:0.0} MiB" -f ($vhd_len_old / [Math]::Pow(2,20)) $out += "VHD len new: {0:0.0} MiB" -f ($vhd_len_new / [Math]::Pow(2,20)) $out += "shrink pct: {0:0}.{1:00}%" -f ($shrink_pct_int, $shrink_pct_frac) Write-Host ([String]::Join("`n", $out)) Write-Host ("End {0}" -f ($MyInvocation.InvocationName)) Return } Function Wait-VMCustom { Param( [Parameter(Mandatory = $True)] [Microsoft.HyperV.PowerShell.VirtualMachine] $VM, [Parameter(Mandatory = $False)] [UInt16] $SleepInterval = 10 ) while ([String]::IsNullOrEmpty($VM.NetworkAdapters[0].IPAddresses)) { Start-Sleep -Seconds:$SleepInterval } $vm_obj = New-Object -TypeName:PSObject -Property:@{ 'IPAddress' = $VM.NetworkAdapters[0].IPAddresses[0]; 'VMId' = $VM.VMId.Guid; 'Name' = $VM.Name; } Format-Table -AutoSize -inputObject:$vm_obj Return } Function Write-VMCustomConfig { Param( [Parameter(Mandatory=$True)] [PSCustomObject] $InputObject, [Parameter(Mandatory=$True)] [String] $FilePath ) $dir = [System.IO.Path]::GetDirectoryName($FilePath) if (-not (Test-Path -Path:$dir)) { Write-Error 'dir not found' Return } $json = ConvertTo-Json -InputObject:$InputObject -Depth:99 $file = [System.IO.Path]::GetFileName($FilePath) $tmp = [System.IO.Path]::Combine($dir, ("{0}.tmp" -f $file)) Out-File -Encoding:'UTF8' -FilePath:$tmp -InputObject:$json Move-Item -Force:$True -Path:$tmp -Destination:$FilePath Return } Export-ModuleMember -Variable:1MiB Export-ModuleMember -Variable:1GiB Export-ModuleMember -Variable:VhdDefaultPath Export-ModuleMember -Function:Disable-SparseFile Export-ModuleMember -Function:Format-VHD Export-ModuleMember -Function:New-VHDCustom Export-ModuleMember -Function:New-VHDCustomDynamic Export-ModuleMember -Function:New-VHDCustomFixed Export-ModuleMember -Function:New-VMCustom Export-ModuleMember -Function:New-VMCustomFull Export-ModuleMember -Function:New-VMCustomConfig Export-ModuleMember -Function:Prepare-VHD Export-ModuleMember -Function:Read-VMCustomConfig Export-ModuleMember -Function:Run-Process Export-ModuleMember -Function:Shrink-VHD Export-ModuleMember -Function:Wait-VMCustom Export-ModuleMember -Function:Write-VMCustomConfig # END