AutomatedLabFailover.psm1

#region Install-LabFailoverCluster
function Install-LabFailoverCluster
{
    [CmdletBinding()]
    param ( )

    # 1 Get-LabVM -Role FailoverNode, Count ge 2. Wenn Machine bereits installiert, FC aktivieren, sonst Start-LabVm, DomJoin, ...
    # Validator: DomJoin, min count 2, Role FailoverStorage in Lab

    $failoverNodes = Get-LabVm -Role FailoverNode -ErrorAction SilentlyContinue
    $clusters = $failoverNodes | Group-Object { ($PSItem.Roles | Where-Object -Property Name -eq 'FailoverNode').Properties['ClusterName'] }
    $useDiskWitness = $false

    Install-LabWindowsFeature -ComputerName $failoverNodes -FeatureName Failover-Clustering, RSAT-Clustering -IncludeAllSubFeature
    
    if (Get-LabVm -Role FailoverStorage)
    {
        Write-ScreenInfo -Message 'Waiting for failover storage server to complete installation'
        Install-LabFailoverStorage
        $useDiskWitness = $true
    }

    Write-Screeninfo -Message 'Waiting for failover nodes to complete installation'    

    foreach ($cluster in $clusters)
    {
        $firstNode = $cluster.Group | Select-Object -First 1
        $clusterDomains = $cluster.Group.DomainName | Sort-Object -Unique
        $clusterNodeNames = $cluster.Group | Select-Object -Skip 1 -ExpandProperty Name
        $clusterName = $cluster.Name
        $clusterIp = ($firstNode.Roles | Where-Object -Property Name -eq 'FailoverNode').Properties['ClusterIp']

        if (-not $clusterIp)
        {
            $adapterVirtualNetwork = Get-LabVirtualNetworkDefinition -Name $firstNode.NetworkAdapters[0].VirtualSwitch
            $clusterIp = $adapterVirtualNetwork.NextIpAddress().AddressAsString
        }

        if (-not $clusterName)
        {
            $clusterName = 'ALCluster'
        }

        if ($useDiskWitness -and -not ($firstNode.OperatingSystem.Version -lt 6.2))
        {
            Invoke-LabCommand -ComputerName $firstNode -ActivityName 'Preparing cluster storage' -ScriptBlock {
                if (-not (Get-ClusterAvailableDisk -ErrorAction SilentlyContinue))
                {
                    $offlineDisk = Get-Disk | Where-Object -Property OperationalStatus -eq Offline | Select-Object -First 1
                    if ($offlineDisk)
                    {
                        $offlineDisk | Set-Disk -IsOffline $false
                        $offlineDisk | Set-Disk -IsReadOnly $false
                    }
        
                    if (-not ($offlineDisk | Get-Partition | Get-Volume))
                    {
                        $offlineDisk | New-Volume -FriendlyName quorum -FileSystem NTFS
                    }
                }
            }

            Invoke-LabCommand -ComputerName $clusterNodeNames -ActivityName 'Preparing cluster storage on remaining nodes' -ScriptBlock {
                Get-Disk | Where-Object -Property OperationalStatus -eq Offline | Set-Disk -IsOffline $false
            }
        }
        else
        {
            Invoke-LabCommand -ComputerName $firstNode -ActivityName 'Preparing cluster storage' -ScriptBlock {
                $diskpartCmd = 'LIST DISK'

                $disks = $diskpartCmd | diskpart.exe

                foreach ($line in $disks)
                {
                    if ($line -match 'Disk (?<DiskNumber>\d) \s+(Offline)\s+(?<Size>\d+) GB\s+(?<Free>\d+) GB')
                    {
                        $nextDriveLetter = [char[]](67..90) | 
                            Where-Object { (Get-WmiObject -Class Win32_LogicalDisk | 
                                    Select-Object -ExpandProperty DeviceID) -notcontains "$($_):"} | 
                            Select-Object -First 1

                        $diskNumber = $Matches.DiskNumber

                        $diskpartCmd = "@
                        SELECT DISK $diskNumber
                        ATTRIBUTES DISK CLEAR READONLY
                        ONLINE DISK
                        CREATE PARTITION PRIMARY
                        ASSIGN LETTER=$nextDriveLetter
                        EXIT
                    @"

                        $diskpartCmd | diskpart.exe | Out-Null

                        Start-Sleep -Seconds 2

                        cmd.exe /c "echo y | format $($nextDriveLetter): /q /v:DataDisk$diskNumber"
                    }
                }
            }
            
            Invoke-LabCommand -ComputerName $clusterNodeNames -ActivityName 'Preparing cluster storage' -ScriptBlock {
                $diskpartCmd = 'LIST DISK'
    
                $disks = $diskpartCmd | diskpart.exe
    
                foreach ($line in $disks)
                {
                    if ($line -match 'Disk (?<DiskNumber>\d) \s+(Offline)\s+(?<Size>\d+) GB\s+(?<Free>\d+) GB')
                    {    
                        $diskNumber = $Matches.DiskNumber
    
                        $diskpartCmd = "@
                            SELECT DISK $diskNumber
                            ATTRIBUTES DISK CLEAR READONLY
                            ONLINE DISK
                            EXIT
                        @"

                        $diskpartCmd | diskpart.exe | Out-Null
                    }
                }
            }
        }
    

        $clusterAccessPoint = if ($clusterDomains.Count -ne 1)
        {
            'DNS'
        }
        else
        {
            'ActiveDirectoryAndDns'    
        }
        
        Invoke-LabCommand -ComputerName $firstNode -ActivityName 'Enabling clustering on first node' -ScriptBlock {
            Import-Module FailoverClusters -ErrorAction Stop

            $clusterParameters = @{
                Name                      = $clusterName
                Node                      = $env:COMPUTERNAME
                StaticAddress             = $clusterIp
                AdministrativeAccessPoint = $clusterAccessPoint
                ErrorAction               = 'Stop'
                WarningAction             = 'SilentlyContinue'
            }

            $clusterParameters = Sync-Parameter -Command (Get-Command New-Cluster) -Parameters $clusterParameters

            New-Cluster @clusterParameters

            while (-not (Get-Cluster -Name $clusterName -ErrorAction SilentlyContinue))
            {
                Start-Sleep -Seconds 1
            }

            Get-Cluster -Name $clusterName | Add-ClusterNode $clusterNodeNames
            
            if ($useDiskWitness)
            {
                $clusterDisk = Get-ClusterResource -Cluster $clusterName -ErrorAction SilentlyContinue | Where-object -Property ResourceType -eq 'Physical Disk'

                if ($clusterDisk)
                {
                    Get-Cluster -Name $clusterName | Set-ClusterQuorum -DiskWitness $clusterDisk
                }
            }
        } -Variable (Get-Variable clusterName, clusterNodeNames, clusterIp, useDiskWitness, clusterAccessPoint) -Function (Get-Command Sync-Parameter)
    }
}
#endregion

#region Install-LabFailoverStorage
function Install-LabFailoverStorage
{
    [CmdletBinding()]
    param
    ( )

    $storageNode = Get-LabVm -Role FailoverStorage -ErrorAction SilentlyContinue
    $role = $storageNode.Roles | Where-Object Name -eq FailoverStorage

    $failoverNodes = Get-LabVm -Role FailoverNode -ErrorAction SilentlyContinue
    $clusters = @{}
    
    $failoverNodes | Foreach-Object {
        
        $name = ($PSItem.Roles | Where-Object -Property Name -eq 'FailoverNode').Properties['ClusterName']
        if (-not $name)
        {
            $name = 'ALCluster'
        }

        if (-not $clusters.ContainsKey($name))
        {
            $clusters[$name] = @()
        }
        $clusters[$name] += $_.Name
    }

    $lunDrive = $role.Properties['LunDrive'][0] # Select drive letter only

    foreach ($cluster in $clusters.Clone().GetEnumerator())
    {
        $machines = $cluster.Value
        $clusterName = $cluster.Key
        $initiatorIds = Invoke-LabCommand -ActivityName 'Retrieving IQNs' -ComputerName $machines -ScriptBlock {
            Set-Service -Name MSiSCSI -StartupType Automatic
            Start-Service -Name MSiSCSI
            "IQN:$((Get-WmiObject -Namespace root\wmi -Class MSiSCSIInitiator_MethodClass).iSCSINodeName)"
        } -PassThru -ErrorAction Stop

        $clusters[$clusterName] = $initiatorIds
    }

    Install-LabWindowsFeature -ComputerName $storageNode -FeatureName FS-iSCSITarget-Server

    Invoke-LabCommand -ActivityName 'Creating iSCSI target' -ComputerName $storageNode -ScriptBlock {
        if (-not $lunDrive)
        {
            $lunDrive = $env:SystemDrive[0]
        }

        $driveInfo = [System.IO.DriveInfo] [string] $lunDrive

        if (-not (Test-Path $driveInfo))
        {
            $offlineDisk = Get-Disk | Where-Object -Property OperationalStatus -eq Offline | Select-Object -First 1
            if ($offlineDisk)
            {
                $offlineDisk | Set-Disk -IsOffline $false
                $offlineDisk | Set-Disk -IsReadOnly $false
            }

            if (-not ($offlineDisk | Get-Partition | Get-Volume))
            {
                $offlineDisk | New-Volume -FriendlyName Luns -FileSystem ReFS -DriveLetter $lunDrive
            }
        }

        $lunFolder = New-Item -ItemType Directory -Path (Join-Path -Path $driveInfo -ChildPath LUNs) -ErrorAction SilentlyContinue
        $lunFolder = Get-Item -Path (Join-Path -Path $driveInfo -ChildPath LUNs) -ErrorAction Stop        
        
        foreach ($clu in $clusters.GetEnumerator())
        {
            New-IscsiServerTarget -TargetName $clu.Key -InitiatorIds $clu.Value
            $diskTarget = (Join-Path -Path $lunFolder.FullName -ChildPath "$($clu.Key).vhdx")
            New-IscsiVirtualDisk -Path $diskTarget -Size 1GB
            Add-IscsiVirtualDiskTargetMapping -TargetName $clu.Key -Path $diskTarget
        }
        
    } -Variable (Get-Variable -Name clusters, lunDrive) -ErrorAction Stop

    $targetAddress = $storageNode.IpV4Address

    Invoke-LabCommand -ActivityName 'Connecting iSCSI target' -ComputerName (Get-LabVm -Role FailoverNode) -ScriptBlock {
        if (-not (Get-Command New-IscsiTargetPortal -ErrorAction SilentlyContinue))
        {
            iscsicli.exe QAddTargetPortal $targetAddress
            $target = ((iscsicli.exe ListTargets) -match 'iqn.+target')[0].Trim()
            iscsicli.exe QLoginTarget $target
        }
        else
        {
            New-IscsiTargetPortal -TargetPortalAddress $targetAddress
            Get-IscsiTarget | Where-Object {-not $PSItem.IsConnected} | Connect-IscsiTarget -IsPersistent $true    
        }        
    } -Variable (Get-Variable targetAddress) -ErrorAction Stop
}
#endregion