functions/clone/Invoke-DcnRepairClone.ps1

function Invoke-DcnRepairClone {
    <#
    .SYNOPSIS
        Invoke-DcnRepairClone repairs the clones

    .DESCRIPTION
        Invoke-DcnRepairClone has the ability to repair the clones when they have gotten disconnected from the image.
        In such a case the clone is no longer available for the database server and the database will either not show
        any information or the database will have the status (Recovery Pending).

        By running this command all the clones will be retrieved from the database for a certain host.

    .PARAMETER HostName
        Set on or more hostnames to retrieve the configurations for

    .PARAMETER SqlCredential
        Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted. To use:

        $scred = Get-Credential, then pass $scred object to the -SqlCredential parameter.

        Windows Authentication will be used if SqlCredential is not specified. SQL Server does not accept Windows credentials being passed as credentials.
        To connect as a different Windows user, run PowerShell as that user.

    .PARAMETER Credential
        Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted.
        This works similar as SqlCredential but is only meant for authentication to the the host

    .PARAMETER DcnSqlCredential
        Allows you to login to servers using SQL Logins as opposed to Windows Auth/Integrated/Trusted.
        This works similar as SqlCredential but is only meant for authentication to the PSDatabaseClone database server and database.

    .PARAMETER EnableException
        By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message.
        This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting.
        Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch.

    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.

    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.

    .PARAMETER WhatIf
        If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run.

    .PARAMETER Confirm
        If this switch is enabled, you will be prompted for confirmation before executing any operations that change state.

    .NOTES
        Author: Sander Stad (@sqlstad, sqlstad.nl)

        Website: https://psdatabaseclone.org
        Copyright: (C) Sander Stad, sander@sqlstad.nl
        License: MIT https://opensource.org/licenses/MIT

    .LINK
        https://psdatabaseclone.org/

    .EXAMPLE
        Invoke-DcnRepairClone -Hostname Host1

        Repair the clones for Host1

    #>


    [CmdLetBinding(SupportsShouldProcess = $true)]

    param(
        [Parameter(Mandatory = $true)]
        [string[]]$HostName,
        [PSCredential]$SqlCredential,
        [PSCredential]$Credential,
        [PSCredential]$DcnSqlCredential,
        [switch]$EnableException
    )

    begin {
        # Check if the console is run in Administrator mode
        if ( -not (Test-PSFPowerShell -Elevated) ) {
            Stop-PSFFunction -Message "Module requires elevation. Please run the console in Administrator mode" -Continue
        }

        if (-not (Test-DcnModule -SetupStatus)) {
            Stop-PSFFunction -Message "The module setup has NOT yet successfully run. Please run 'Set-DcnConfiguration'" -Continue
        }
    }

    process {
        # Test if there are any errors
        if (Test-PSFFunctionInterrupt) { return }

        # Loop through each of the hosts
        foreach ($hst in $HostName) {

            # Setup the computer object
            $computer = [PSFComputer]$hst

            if (-not $computer.IsLocalhost) {
                # Get the result for the remote test
                $resultPSRemote = Test-DcnRemoting -ComputerName $hst -Credential $Credential

                # Check the result
                if ($resultPSRemote.Result) {

                    $command = [scriptblock]::Create("Import-Module dbaclone")

                    try {
                        Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential
                    }
                    catch {
                        Stop-PSFFunction -Message "Couldn't import module remotely" -Target $command
                        return
                    }
                }
                else {
                    Stop-PSFFunction -Message "Couldn't connect to host remotely.`nVerify that the specified computer name is valid, that the computer is accessible over the network, and that a firewall exception for the WinRM service is enabled and allows access from this computer" -Target $resultPSRemote -Continue
                }
            }

            # Get the clones
            [array]$results = Get-DcnClone -HostName $hst

            # Loop through the results
            foreach ($result in $results) {

                $server = Connect-DbaInstance -SqlInstance $result.SqlInstance -SqlCredential $SqlCredential

                # Get the databases
                Write-PSFMessage -Message "Retrieve the databases for $($result.SqlInstance)" -Level Verbose
                $databases = $server.Databases

                $image = Get-DcnImage -ImageID $result.ImageID

                # Check if the parent of the clone can be reached
                try {
                    $null = New-PSDrive -Name ImagePath -Root (Split-Path $image.ImageLocation) -Credential $Credential -PSProvider FileSystem
                }
                catch {
                    Stop-PSFFunction -Message "Could not create drive for image path '$($image.ImageLocation)'" -ErrorRecord $_ -Continue
                }

                # Test if the image still exists
                if (Test-Path -Path "ImagePath:\$($image.ImageName).vhdx") {
                    # Mount the clone
                    try {
                        Write-PSFMessage -Message "Mounting vhd $($result.CloneLocation)" -Level Verbose

                        if (Test-Path -Path $result.CloneLocation) {
                            $disk = Get-Disk | Where-Object Location -eq $result.CloneLocation

                            if (-not $disk) {
                                # Check if computer is local
                                if ($PSCmdlet.ShouldProcess("Mounting $($result.CloneLocation)")) {
                                    if ($computer.IsLocalhost) {
                                        $null = Mount-DiskImage -ImagePath $result.CloneLocation -NoDriveLetter
                                    }
                                    else {
                                        $command = [ScriptBlock]::Create("Mount-DiskImage -ImagePath '$($result.CloneLocation)' -NoDriveLetter -ErrorAction SilentlyContinue")
                                        $null = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential
                                    }
                                }
                            }
                        }
                        else {
                            Stop-PSFFunction -Message "Couldn't find clone file '$($result.CloneLocation)'" -Target $result -Continue
                        }
                    }
                    catch {
                        Stop-PSFFunction -Message "Couldn't mount vhd" -Target $result -ErrorRecord $_ -Continue
                    }
                }
                else {
                    Stop-PSFFunction -Message "Vhd $($result.CloneLocation) cannot be mounted because image path cannot be reached" -Target $image -Continue
                }

                # Remove the PS Drive
                try {
                    $null = Remove-PSDrive -Name ImagePath
                }
                catch {
                    Stop-PSFFunction -Message "Could not remove drive 'ImagePath'" -ErrorRecord $_ -Continue
                }

                # Check if the database is already attached
                if ($result.DatabaseName -in $databases.Name) {
                    $db = $databases | Where-Object Name -eq $result.DatabaseName

                    if ($db.Status -eq 'RecoveryPending') {
                        try {
                            Write-PSFMessage -Message "Setting database offline" -Level Verbose
                            $db.SetOffline()

                            Write-PSFMessage -Message "Setting database online" -Level Verbose
                            $db.SetOnline()
                        }
                        catch {
                            Stop-PSFFunction -Message "Could not detach database [$($result.DatabaseName)]" -ErrorRecord $_ -Continue
                        }
                    }
                    else {
                        try {
                            $null = Detach-DbaDatabase -SqlInstance $result.SQLInstance -SqlCredential $SqlCredential -Database $result.DatabaseName
                        }
                        catch {
                            Stop-PSFFunction -Message "Could not detach database [$($result.DatabaseName)]" -ErrorRecord $_ -Continue
                        }
                    }
                }
                else {
                    # Get all the files of the database
                    if ($PSCmdlet.ShouldProcess("Retrieving database files from $($result.AccessPath)")) {
                        # Check if computer is local
                        if ($computer.IsLocalhost) {
                            $databaseFiles = Get-ChildItem -Path $result.AccessPath -Recurse | Where-Object { -not $_.PSIsContainer }
                        }
                        else {
                            $commandText = "Get-ChildItem -Path $($result.AccessPath) -Recurse | " + 'Where-Object {-not $_.PSIsContainer}'
                            $command = [ScriptBlock]::Create($commandText)
                            $databaseFiles = Invoke-PSFCommand -ComputerName $computer -ScriptBlock $command -Credential $Credential
                        }
                    }

                    # Setup the database filestructure
                    $dbFileStructure = New-Object System.Collections.Specialized.StringCollection

                    # Loop through each of the database files and add them to the file structure
                    foreach ($dbFile in $databaseFiles) {
                        $dbFileStructure.Add($dbFile.FullName) | Out-Null
                    }

                    Write-PSFMessage -Message "Mounting database from clone" -Level Verbose

                    # Mount the database using the config file
                    if ($PSCmdlet.ShouldProcess("Mounting database $($result.DatabaseName) to $($result.SQLInstance)")) {
                        try {
                            $null = Mount-DbaDatabase -SqlInstance $result.SQLInstance -Database $result.DatabaseName -FileStructure $dbFileStructure
                        }
                        catch {
                            Stop-PSFFunction -Message "Couldn't mount database $($result.DatabaseName)" -Target $result.DatabaseName -Continue
                        }
                    }
                }
            } # End for ech result
        } # End for each host
    } # End process

    end {
        # Test if there are any errors
        if (Test-PSFFunctionInterrupt) { return }

        Write-PSFMessage -Message "Finished repairing clones" -Level Verbose
    }

}