function Log {
<# 
 .Synopsis
  Function to log input string to file and display it to screen

 .Description
  Function to log input string to file and display it to screen. Log entries in the log file are time stamped. Function allows for displaying text to screen in different colors.

 .Parameter String
  The string to be displayed to the screen and saved to the log file

 .Parameter Color
  The color in which to display the input string on the screen
  Default is White
  Valid options are
    Black
    Blue
    Cyan
    DarkBlue
    DarkCyan
    DarkGray
    DarkGreen
    DarkMagenta
    DarkRed
    DarkYellow
    Gray
    Green
    Magenta
    Red
    White
    Yellow

 .Parameter LogFile
  Path to the file where the input string should be saved.
  Example: c:\log.txt
  If absent, the input string will be displayed to the screen only and not saved to log file

 .Example
  Log -String "Hello World" -Color Yellow -LogFile c:\log.txt
  This example displays the "Hello World" string to the console in yellow, and adds it as a new line to the file c:\log.txt
  If c:\log.txt does not exist it will be created.
  Log entries in the log file are time stamped. Sample output:
    2014.08.06 06:52:17 AM: Hello World

 .Example
  Log "$((Get-Location).Path)" Cyan
  This example displays current path in Cyan, and does not log the displayed text to log file.

 .Example 
  "Java process ID is $((Get-Process -Name java).id )" | log -color Yellow
  Sample output of this example:
    "Java process ID is 4492" in yellow

 .Example
  "Drive 'd' on VM 'CM01' is on VHDX file '$((Get-SBVHD CM01 d).VHDPath)'" | log -color Green -LogFile D:\Sandbox\Serverlog.txt
  Sample output of this example:
    Drive 'd' on VM 'CM01' is on VHDX file 'D:\VMs\Virtual Hard Disks\CM01_D1.VHDX'
  and the same is logged to file D:\Sandbox\Serverlog.txt as in:
    2014.08.06 07:28:59 AM: Drive 'd' on VM 'CM01' is on VHDX file 'D:\VMs\Virtual Hard Disks\CM01_D1.VHDX'

 .Link
  https://superwidgets.wordpress.com/category/powershell/

 .Notes
  Function by Sam Boutros
  v1.0 - 08/06/2014

#>

    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true,
                   ValueFromPipeLine=$true,
                   ValueFromPipeLineByPropertyName=$true,
                   Position=0)]
            [String]$String, 
        [Parameter(Mandatory=$false,
                   Position=1)]
            [ValidateSet("Black","Blue","Cyan","DarkBlue","DarkCyan","DarkGray","DarkGreen","DarkMagenta","DarkRed","DarkYellow","Gray","Green","Magenta","Red","White","Yellow")]
            [String]$Color = "White", 
        [Parameter(Mandatory=$false,
                   Position=2)]
            [String]$LogFile
    )

    write-host $String -foregroundcolor $Color 
    if ($LogFile.Length -gt 2) {
        ((Get-Date -format "yyyy.MM.dd hh:mm:ss tt") + ": " + $String) | out-file -Filepath $Logfile -append
    } else {
        Write-Verbose "Log: Missing -LogFile parameter. Will not save input string to log file.."
    }
}

function Get-ParentPath {
<# 
 .Synopsis
  Function to get parent disk/path tree of VHD(x) file

 .Description
  Function to get parent disk/path tree of VHD(x) file

 .Parameter VHDPath
  Full path to the VHD(x) file.
  For example: "I:\VMs\v-2012R2-G2a\v-2012R2-G2a-C_55DB25B0-EFA9-415F-A5D1-738A62742B4E.avhdx" 

 .Parameter ComputerName
  Name of Hyper-V host where the VHD(x) file resides
  If absent, defaults to localhost

 .Example
  Get-ParentPath -VHDPath "I:\VMs\v-2012R2-G2a\v-2012R2-G2a-C_55DB25B0-EFA9-415F-A5D1-738A62742B4E.avhdx" -ComputerName "xhost16"
  Retunrs an array of the disk path and all its parents paths

 .Example
  $VMName = "v-2012R2-G2a"
  $HVName = "xHost16"
  $VMDisks = Invoke-Command -ComputerName $HVName -ArgumentList $VMName -ScriptBlock { 
    Param($VMName)
    Get-VMHardDiskDrive -VMName $VMName
  }
  ($VMDisks.Path | Where { $_ -match ":" }) | % {
    Get-ParentPath -VHDPath $_ -ComputerName $HVName
  }

  Retunrs an array for each disk attached to the VM $VMName, containing the disk tree.
  Sample output:
    I:\VMs\v-2012R2-G2a\v-2012R2-G2a-C_55DB25B0-EFA9-415F-A5D1-738A62742B4E.avhdx
    I:\VMs\v-2012R2-G2a\v-2012R2-G2a-C_0020A6D3-0371-48E3-B67D-DE2ADF0BEDF1.avhdx
    I:\VMs\v-2012R2-G2a\v-2012R2-G2a-C_C2BA8DE5-8FE6-49AD-B12B-789853306524.avhdx
    I:\VMs\v-2012R2-G2a\v-2012R2-G2a-C_79E7AFEB-F867-4068-A3DA-6BF64F49E819.avhdx
    I:\VMs\v-2012R2-G2a\v-2012R2-G2a-C_232D8FD8-5855-4518-800C-2659385521FA.avhdx
    I:\VMs\v-2012R2-G2a\v-2012R2-G2a-C.VHDX
    I:\VMs\v-2012R2-G2a\v-2012R2-G2a-D_C492420D-011D-42F4-862A-A04A7222B7FC.avhdx
    I:\VMs\v-2012R2-G2a\v-2012R2-G2a-D_7B0A54C4-36E5-4229-9746-49566B0566AF.avhdx
    I:\VMs\v-2012R2-G2a\v-2012R2-G2a-D_460EDAAF-F30A-4744-8396-87449EAF1A8C.avhdx
    I:\VMs\v-2012R2-G2a\v-2012R2-G2a-D_9D3805B2-79F5-4E5F-865D-D69620E31B6A.avhdx
    I:\VMs\v-2012R2-G2a\v-2012R2-G2a-D_F37513BE-AD00-41FD-B24E-CBB3E0BDEFC4.avhdx
    I:\VMs\v-2012R2-G2a\v-2012R2-G2a-D.vhdx

 .Link
  https://superwidgets.wordpress.com/category/powershell/

 .Notes
  Function by Sam Boutros
  v1.0 - 11/01/2014

#>

    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Low')] 
    Param(
        [Parameter(Mandatory=$true,
                   ValueFromPipeLine=$true,
                   ValueFromPipeLineByPropertyName=$true,
                   Position=0)]
            [ValidateNotNullorEmpty()]
            [String]$VHDPath,
        [Parameter(Mandatory=$false,
                   Position=1)]
            [alias("HVName","HyperVHostName")]
            [String]$ComputerName = ".",       
        [Parameter(Mandatory=$false,
                   Position=2)]
            [String]$LogFile = ".\Get-ParentPath_$(Get-Date -format yyyyMMdd_hhmmsstt).txt"
    )

    log "Getting disk information for file '$VHDPath' on computer '$ComputerName'" Green $LogFile

    try {
        $VHDSVC = Get-WmiObject -ComputerName $ComputerName -Namespace root\virtualization\v2 -Class Msvm_ImageManagementService -ErrorAction Stop
        $VHDInfo = [xml]$VHDSVC.GetVirtualHardDiskSettingData($VHDPath).SettingData
        if ($VHDInfo) {
            $ParentPath = ($VHDInfo.INSTANCE.PROPERTY | Where { $_.Name -eq "ParentPath" }).Value
            if ($ParentPath) { 
                $Result = $VHDPath,$ParentPath 
                While ($ParentPath.Split(".")[1] -match "avhd") {
                    $VHDInfo = [xml]$VHDSVC.GetVirtualHardDiskSettingData($ParentPath).SettingData
                    $ParentPath = ($VHDInfo.INSTANCE.PROPERTY | Where { $_.Name -eq "ParentPath" }).Value
                    $Result += $ParentPath
                }
                log "Got disk chain information:" Green $LogFile
                $Result | % { log "  $_" Green $LogFile }
            } else {
                $Result = $VHDPath  
                log "Disk '$Result' is not a differencing disk - does not have any parent.." Yellow $LogFile
            }
        } else {
            log "Disk file '$VHDPath' does not exist on computer '$ComputerName'" Yellow $LogFile
        }
        $Result
    } catch {
        log "Computer '$ComputerName' is offline or cannot be contacted" Yellow $LogFile
    }
}

function Merge-VMDisks {
<# 
 .Synopsis
  Function to merge VM disks

 .Description
  Function/script to merge VM disks. The script will power down the VM in the process.

 .Parameter VMName
  Name of the VM whose VHD(x) disks are to be merged 

 .Parameter LogFile
  Name and path of the file where the script will log its steps and progress

 .Example
  Merge-VMDisks -VMName 'MyVM'

 .Link
  https://superwidgets.wordpress.com/category/powershell/

 .Notes
  Function by Sam Boutros
  v1.0 - 11/01/2014

#>

    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='High')] 
    Param(
        [Parameter(Mandatory=$true,
                   ValueFromPipeLine=$true,
                   ValueFromPipeLineByPropertyName=$true,
                   Position=0)]
            [ValidateNotNullorEmpty()]
            [String[]]$VMName,    
        [Parameter(Mandatory=$false,
                   Position=1)]
            [String]$LogFile = ".\Merge-VMDisks_$(Get-Date -format yyyyMMdd_hhmmsstt).txt"
    )

    Begin {
        log "Starting to merge disks on VM(s):" Green $LogFile
        $VMName | % { log "  $_" Green $LogFile }
    }

    Process{
        foreach ($VM in $VMName) {
            try {
                $HVName = Invoke-Command -ComputerName $VM -ErrorAction Stop -ScriptBlock { 
                    $Key = "HKLM:\SOFTWARE\Microsoft\Virtual Machine\Guest\Parameters"
                    (Get-ItemProperty -Path $Key).PhysicalHostName
                }
                $VMDisks = Invoke-Command -ComputerName $HVName -ArgumentList $VM -ScriptBlock { 
                    Param($VM)
                    Get-VMHardDiskDrive -VMName $VM 
                }
                $Differencing = $false
                foreach ($Disk in ($VMDisks | Where { $_.Path -match ":" })) {
                    $DiskTree = Get-ParentPath -VHDPath $Disk.Path -ComputerName $HVName -LogFile $LogFile
                    if ($DiskTree.Count -gt 1) { $Differencing = $true }
                }
                if ($Differencing) { log "About to stop VM '$VM' on Hyper-V host '$HVName'" Green $LogFile }
                if ($PSCmdlet.ShouldProcess($VM) -and $Differencing) {
                    # Stop VM, get its disks' information
                    log "Stopping VM '$VM' on Hyper-V host '$HVName'" Yellow $LogFile
                    Invoke-Command -ComputerName $HVName -ArgumentList $VM -ScriptBlock { 
                        Param($VM)
                        if ((Get-VM -Name $VM).State -eq "Running") {
                            Stop-VM -Name $VM -Force
                        }
                    }

                    # Merge disks, attach new merged disks to VM
                    foreach ($Disk in ($VMDisks | Where { $_.Path -match ":" })) {
                        log "Processing Disk '$($Disk.Path)'" Green $LogFile
                        $DiskTree = Get-ParentPath -VHDPath $Disk.Path -ComputerName $HVName -LogFile $LogFile
                        for ($i=0; $i -lt $DiskTree.Count-1; $i++) {
                            log "Merging file '$($DiskTree[$i])' # $($i+1) of $($DiskTree.Count-1) .." Green $LogFile
                            Invoke-Command -ComputerName $HVName -ArgumentList $DiskTree[$i] -ScriptBlock { 
                                Param($DiskFile)
                                Merge-VHD -Path $DiskFile -Confirm:$false -Force
                            }
                        }
                        log "Attching merged disk '$($DiskTree[$DiskTree.Count-1])'" Green $LogFile
                        Invoke-Command -ComputerName $HVName -ArgumentList $VM,$DiskTree,$Disk -ScriptBlock { 
                            Param($VM,$DiskTree,$Disk)
                            $Splat = @{
                                VMName =             $VM;
                                ControllerType =     $Disk.ControllerType;
                                ControllerNumber =   $Disk.ControllerNumber;
                                ControllerLocation = $Disk.ControllerLocation
                            }
                            Remove-VMHardDiskDrive @Splat
                            $Splat += @{ Path =      $DiskTree[$DiskTree.Count-1]}
                            Add-VMHardDiskDrive @Splat
                        }
                    }

                    # Start VM
                    log "Done merging disks, starting VM '$VM'.." Green $LogFile
                    Invoke-Command -ComputerName $HVName -ArgumentList $VM -ScriptBlock { 
                        Param($VM)
                        Start-VM -VMName $VM
                    }
                } # end if (shouldProcess)
            } catch {
                log "Computer '$VM' is offline or cannot be contacted." Yellow $LogFile
            } # end try/catch
        } # end foreach
    } # end process

    end {
        log "Done with VM(s):" Green $LogFile
        $VMName | % { log "  $_" Green $LogFile }
    }
} 