AzureArcReOnboardingAssistant.psm1

function Write-ColorOutput {
    param (
        [Parameter(Mandatory=$true)]
        [System.ConsoleColor]$ForegroundColor,
        
        [Parameter(Mandatory=$true)]
        [string]$Message
    )
    
    Write-Host -ForegroundColor $ForegroundColor -Object $Message
}

function Get-ServerInfo {
    param (
        [Parameter(Mandatory=$true)]
        [string]$ServerName,
        [Parameter(Mandatory=$true)]
        [string]$ResourceGroupName
    )
    
    try {
        $server = Get-AzConnectedMachine -Name $ServerName -ResourceGroupName $ResourceGroupName -ErrorAction Stop
        $serverInfo = [ordered]@{
            ServerName = $server.Name
            ResourceGroup = $server.ResourceGroupName
            SubscriptionId = (Get-AzContext).Subscription.Id
            Location = $server.Location
            Tags = @{}
            HasTags = $false
            Extensions = @()
            DataCollectionAssociations = @()
            AgentConfigurationConfigMode = $server.AgentConfigurationConfigMode.ToString()
        }

        # Handle Tags
        $tagProperty = if ($null -ne $server.Tags) { $server.Tags } else { $server.Tag }
        if ($null -ne $tagProperty -and $tagProperty.Count -gt 0) {
            $serverInfo.HasTags = $true
            foreach ($key in $tagProperty.Keys) {
                $serverInfo.Tags[$key] = $tagProperty[$key]
            }
        }

        # Get Extensions
        $extensions = Get-AzConnectedMachineExtension -ResourceGroupName $ResourceGroupName -MachineName $ServerName -ErrorAction Stop
        $serverInfo.Extensions = $extensions | Select-Object -Property Name, Publisher, ExtensionType, TypeHandlerVersion, Location

        # Get Data Collection Associations
        $dcas = Get-AzDataCollectionRuleAssociation -TargetResourceId $server.Id -ErrorAction Stop
        $serverInfo.DataCollectionAssociations = $dcas | Select-Object -Property Name, DataCollectionRuleId, DataCollectionEndpointId

        return $serverInfo
    }
    catch {
        Write-Error "Error getting server info: $($ERROR[0].Exception.Message)"
        return $null
    }
}

function Save-ServerInfo {
    param (
        [Parameter(Mandatory=$true)]
        [hashtable]$ServerInfo,
        [Parameter(Mandatory=$true)]
        [string]$OutputLocation
    )

    $Filename = Join-Path -Path $OutputLocation -ChildPath "$($ServerInfo.SubscriptionId)_$($ServerInfo.ServerName)_info.json"
    $MasterFilename = Join-Path -Path $OutputLocation -ChildPath "$($ServerInfo.SubscriptionId)_$($ServerInfo.ServerName)_info_master.json"

    $ServerInfo | ConvertTo-Json -Depth 10 | Out-File -FilePath $Filename -Encoding UTF8
    Write-ColorOutput -ForegroundColor Green -Message "Server information saved to $Filename"

    if (-not (Test-Path -Path $MasterFilename)) {
        $ServerInfo | ConvertTo-Json -Depth 10 | Out-File -FilePath $MasterFilename -Encoding UTF8
        Write-ColorOutput -ForegroundColor Green -Message "Master copy created at $MasterFilename"
    } else {
        Write-ColorOutput -ForegroundColor Yellow -Message "Master copy already exists at $MasterFilename (not overwritten)"
    }
}

function Wait-ForServerOffboarding {
    param (
        [Parameter(Mandatory=$true)]
        [string]$ServerName,
        [Parameter(Mandatory=$true)]
        [string]$ResourceGroupName
    )

    Write-ColorOutput -ForegroundColor Cyan -Message "Waiting for server to be offboarded..."
    while ($true) {
        try {
            Get-AzConnectedMachine -Name $ServerName -ResourceGroupName $ResourceGroupName -ErrorAction Stop | Out-Null
            Write-Host "." -NoNewline
            Start-Sleep -Seconds 10
        }
        catch {
            Write-Host ""
            Write-ColorOutput -ForegroundColor Green -Message "Server has been offboarded."
            break
        }
    }
}

function Wait-ForServerReonboarding {
    param (
        [Parameter(Mandatory=$true)]
        [string]$ServerName,
        [Parameter(Mandatory=$true)]
        [string]$ResourceGroupName
    )

    Write-ColorOutput -ForegroundColor Cyan -Message "Waiting for server to be re-onboarded..."
    while ($true) {
        try {
            $server = Get-AzConnectedMachine -Name $ServerName -ResourceGroupName $ResourceGroupName -ErrorAction Stop
            if ($server.Status -eq "Connected") {
                Write-ColorOutput -ForegroundColor Green -Message "Server has been re-onboarded."
                return $server
            }
        }
        catch {
            Write-Host "." -NoNewline
        }
        Start-Sleep -Seconds 10
    }
}

function Restore-ServerConfiguration {
    param (
        [Parameter(Mandatory=$true)]
        [PSCustomObject]$StoredServerInfo,
        [Parameter(Mandatory=$true)]
        [PSObject]$Server
    )

    if ($StoredServerInfo.HasTags) {
        try {
            $TagsHashtable = @{}
            foreach ($Tag in $StoredServerInfo.Tags.PSObject.Properties) {
                $TagsHashtable[$Tag.Name] = $Tag.Value
            }
            Update-AzConnectedMachine -Name $StoredServerInfo.ServerName -ResourceGroupName $StoredServerInfo.ResourceGroup -Tag $TagsHashtable -ErrorAction Stop | Out-Null
            Write-ColorOutput -ForegroundColor Green -Message "Tags restored successfully"
        } catch {
            Write-ColorOutput -ForegroundColor Red -Message "Error restoring tags: $($Error[0])"
        }
    }

    foreach ($dca in $StoredServerInfo.DataCollectionAssociations) {
        if ($dca.DataCollectionRuleId) {
            try {
                New-AzDataCollectionRuleAssociation -TargetResourceId $Server.Id -AssociationName $dca.Name -DataCollectionRuleId $dca.DataCollectionRuleId -ErrorAction Stop | Out-Null
                Write-ColorOutput -ForegroundColor Green -Message "Data Collection Rule Association '$($dca.Name)' restored"
            }
            catch {
                Write-ColorOutput -ForegroundColor Red -Message "Failed to restore Data Collection Rule Association '$($dca.Name)': $($Error[0])"
            }
        }
        elseif ($dca.DataCollectionEndpointId) {
            try {
                New-AzDataCollectionRuleAssociation -TargetResourceId $Server.Id -AssociationName $dca.Name -DataCollectionEndpointId $dca.DataCollectionEndpointId -ErrorAction Stop | Out-Null
                Write-ColorOutput -ForegroundColor Green -Message "Data Collection Endpoint Association '$($dca.Name)' restored"
            }
            catch {
                Write-ColorOutput -ForegroundColor Red -Message "Failed to restore Data Collection Endpoint Association '$($dca.Name)': $($Error[0])"
            }
        }
    }

    Write-ColorOutput -ForegroundColor Yellow -Message "Note: Stored AgentConfigurationConfigMode was $($StoredServerInfo.AgentConfigurationConfigMode). This setting cannot be automatically restored."
}

function Invoke-AzureArcNodeOffboarding {
<#
.SYNOPSIS
Manages the offboarding and re-onboarding process for Azure Arc-enabled servers.
 
.DESCRIPTION
The Invoke-AzureArcNodeOffboarding function facilitates the process of offboarding an Azure Arc-enabled server and then re-onboarding it. It can also be used to restore a previously offboarded server's configuration.
 
This function performs the following tasks:
1. Retrieves and stores the current server configuration.
2. Waits for the server to be manually offboarded.
3. Waits for the server to be re-onboarded.
4. Restores the server's original configuration (tags, data collection rules, etc.).
 
.PARAMETER ServerName
The name of the Azure Arc-enabled server to offboard and re-onboard.
 
.PARAMETER ResourceGroupName
The name of the resource group containing the Azure Arc-enabled server.
 
.PARAMETER SubscriptionId
The ID of the Azure subscription containing the Azure Arc-enabled server.
 
.PARAMETER OutputLocation
The file path where backup files of the server configuration will be stored.
 
.PARAMETER RestoreOnly
Switch parameter to indicate that only the restoration process should be performed, skipping the offboarding steps.
 
.PARAMETER RestoreFile
The file path of a specific backup file to use for restoration. Required when using the RestoreOnly switch.
 
.EXAMPLE
Invoke-AzureArcNodeOffboarding -ServerName SERVER1 -ResourceGroupName "RG-ARC-SRV" -SubscriptionId "XXXXXX-XXXX-XXXX-XXXX-XXXXXX" -OutputLocation "C:\AzureArcServerBackups"
 
This example initiates the full offboarding and re-onboarding process for the server named SERVER1.
 
.EXAMPLE
Invoke-AzureArcNodeOffboarding -ServerName SERVER1 -ResourceGroupName "RG-ARC-SRV" -SubscriptionId "XXXXXX-XXXX-XXXX-XXXX-XXXXXX" -RestoreOnly -RestoreFile "C:\AzureArcServerBackups\XXXXXXXXXXXXXXXXXXXXXXXXX_SERVER1_info_master.json" -OutputLocation "C:\AzureArcServerBackups"
 
This example performs only the restoration process for the server named SERVER1, using a specific backup file.
 
.NOTES
Author: Kaido Järvemets
Website: KaidoJarvemets.com
Version: 1.0.0
Requires: Azure PowerShell modules (Az.Accounts, Az.ConnectedMachine, Az.Monitor)
 
Before running this function, ensure you have the necessary permissions and are connected to the correct Azure subscription.
You may need to run Set-AzContext -SubscriptionId $SubscriptionId before invoking this function.
 
.LINK
https://kaidojarvemets.com/projects/azurearcreonboardingassistant
#>

    param(
        [Parameter(Mandatory=$true)]
        [string]$ServerName,

        [Parameter(Mandatory=$true)]
        [string]$ResourceGroupName,

        [Parameter(Mandatory=$true)]
        [string]$SubscriptionId,

        [Parameter(Mandatory=$true)]
        [string]$OutputLocation,

        [Parameter(Mandatory=$false)]
        [switch]$RestoreOnly,

        [Parameter(Mandatory=$false)]
        [string]$RestoreFile
    )

    if ($RestoreOnly -and -not $RestoreFile) {
        Write-ColorOutput -ForegroundColor Red -Message "RestoreFile parameter is required when using RestoreOnly switch."
        return
    }

    if (-not (Test-Path -Path $OutputLocation)) {
        New-Item -ItemType Directory -Path $OutputLocation -Force | Out-Null
        Write-ColorOutput -ForegroundColor Yellow -Message "Created output directory: $OutputLocation"
    }

    if (-not $RestoreOnly) {
        Write-ColorOutput -ForegroundColor Cyan -Message "Starting Azure Arc Node offboarding process for $ServerName"
        $serverInfo = Get-ServerInfo -ServerName $ServerName -ResourceGroupName $ResourceGroupName
        if ($null -eq $serverInfo) { return }

        Save-ServerInfo -ServerInfo $serverInfo -OutputLocation $OutputLocation

        Write-ColorOutput -ForegroundColor Yellow -Message "Please proceed with manual offboarding of the server."
        Wait-ForServerOffboarding -ServerName $ServerName -ResourceGroupName $ResourceGroupName
    }

    if ($RestoreOnly) {
        $fileToRestore = $RestoreFile
        Write-ColorOutput -ForegroundColor Yellow -Message "Restoring from specified file: $fileToRestore"
    } else {
        $masterFilename = Join-Path -Path $OutputLocation -ChildPath "$($SubscriptionId)_$($ServerName)_info_master.json"
        $latestFilename = Join-Path -Path $OutputLocation -ChildPath "$($SubscriptionId)_$($ServerName)_info.json"
        
        if (Test-Path -Path $masterFilename) {
            $fileToRestore = $masterFilename
            Write-ColorOutput -ForegroundColor Green -Message "Restoring from master copy: $fileToRestore"
        } elseif (Test-Path -Path $latestFilename) {
            $fileToRestore = $latestFilename
            Write-ColorOutput -ForegroundColor Yellow -Message "Master copy not found. Restoring from latest backup: $fileToRestore"
        } else {
            Write-ColorOutput -ForegroundColor Red -Message "No backup files found. Cannot proceed with restoration."
            return
        }

        $server = Wait-ForServerReonboarding -ServerName $ServerName -ResourceGroupName $ResourceGroupName
    }

    $storedServerInfo = Get-Content -Path $fileToRestore | ConvertFrom-Json

    if ($RestoreOnly) {
        try {
            $server = Get-AzConnectedMachine -Name $ServerName -ResourceGroupName $ResourceGroupName -ErrorAction Stop
        }
        catch {
            Write-ColorOutput -ForegroundColor Red -Message "Unable to find the server $ServerName. Ensure it's onboarded before restoration: $($Error[0])"
            return
        }
    }

    Write-ColorOutput -ForegroundColor Cyan -Message "Restoring server configuration..."
    Restore-ServerConfiguration -StoredServerInfo $storedServerInfo -Server $server

    Write-ColorOutput -ForegroundColor Green -Message "Azure Arc Node offboarding and restoration process completed successfully."
}