RiverMeadow.Development.Source/SourceUtil/SourceUtil.psm1

using module '../../Common/Result'
Import-Module -Name @(Join-Path $PSScriptRoot .. | Join-Path -ChildPath .. | Join-Path -ChildPath Util | Join-Path -ChildPath Util)
Import-Module -Name @(Join-Path $PSScriptRoot .. | Join-Path -ChildPath .. | Join-Path -ChildPath Preflight | Join-Path -ChildPath Preflight)
function Get-RMSourcesForCurrentProject {
    param()

    $LoginStatus = Test-UserLoggedIn
    if ($LoginStatus.ReturnCode -eq [RMReturn]::ERROR) {
        return $LoginStatus
    }

    $CurrentProjectId = Get-Variable -Name "RMContext-CurrentProjectId" -ValueOnly

    $Result = @()
    $Response = Get-RMSourcesInternal -OrganizationId $CurrentProjectId -PageNumber 0
    $Result += $Response
    for ($index = 1; $index -lt $Response.page.totalPages; $index++) {
        $Result += Get-RMSourcesInternal -OrganizationId $CurrentProjectId -PageNumber $index
    }

    return $Result
}

function Get-RMSourcesInternal {
    param(
        [string] $OrganizationId,
        [int] $PageNumber
    )

    $RMLoginResult = Get-Variable -Name "RMContext-UserLogin"
    $Uri = Get-Variable -Name "RMContext-ReactorURI"

    $Headers = @{
        Accept = "application/rm+json"
        "X-Auth-Token" = $RMLoginResult.Value.token
    }

    $Params = @{
        Method = "Get"
        Uri = $Uri.Value + "/organizations/" + $OrganizationId + "/sources?size=25&page=" + $PageNumber + "&sort=created_at%2Cdesc"
        Headers = $Headers
    }
    return Invoke-RMRestMethod -Params $Params
}

function Get-RMSourceById {
    param(
        [string] $SourceId
    )

    $RMLoginResult = Get-Variable -Name "RMContext-UserLogin"
    $Uri = Get-Variable -Name "RMContext-ReactorURI"

    $Headers = @{
        Accept = "application/rm+json"
        "X-Auth-Token" = $RMLoginResult.Value.token
    }

    $Params = @{
        Method = "Get"
        Uri = $Uri.Value + "/sources/" + $SourceId
        Headers = $Headers
    }
    return Invoke-RMRestMethod -Params $Params
}

function Get-MountPoint {
    param(
        [System.Object] $Source
    )
    $MountPoints = @()
    if ("windows" -ieq $Source.os_type) {
        foreach ($Mount in $Source.attributes.storage.mounts.psobject.properties.value) {
            $MountPoints += @{mount_point = $Mount.path}
        }
    } else {
        foreach ($Mount in $Source.attributes.storage.mounts.psobject.properties.value) {
            if ("disk" -ieq $Mount.nature -or "subvolume" -ieq $Mount.nature -and "squashfs" -ine $Mount.fs_type) {
                $MountPoints += @{mount_point = $Mount.path}
            }
        }
    }

    return $MountPoints
}

function Get-RMSourceByIP {
    param(
        [string] $IPAddress
    )

    $CurrentProjectId = Get-Variable -Name "RMContext-CurrentProjectId" -ValueOnly

    $PageIndex = 0
    do {
        $Sources = Get-RMSourcesInternal -OrganizationId $CurrentProjectId -PageNumber $PageIndex
        foreach ($Source in $Sources.content) {
            if ($IPAddress -eq $Source.host) {
                return $Source
            }
        }
        $PageIndex++
    } while ($PageIndex -lt $Sources.page.totalPages)
    
    $ProjectName = Get-Variable -Name "RMContext-CurrentProjectName" -ValueOnly
    $OrgName = Get-Variable -Name "RMContext-CurrentOrganizationName" -ValueOnly
    throw [System.Management.Automation.ItemNotFoundException]::new(`
    "Source with IP address/VM name '$IPAddress' does not exist in project '$ProjectName' of organization '$OrgName', cannot start the migration. Please add the source in the project and try again.")
}

function Start-RMSourcePreflight {
    param (
        [System.Object] $Source
    )
    $RequestAttributes = @{
        "type" = "source"
        "resource_id"= $Source.id
        "overrides"= @{}
    }

    $SourceAttributesRequest = @($RequestAttributes)
    $SourceAttributesRequestJson = ConvertTo-Json $SourceAttributesRequest

    $RMLoginResult = Get-Variable -Name "RMContext-UserLogin" -ValueOnly
    $Uri = Get-Variable -Name "RMContext-ReactorURI" -ValueOnly

    $Headers = @{
        Accept = "application/rm+json"
        "X-Auth-Token" = $RMLoginResult.token
    }

    $Params = @{
        Method = "Post"
        Uri = $Uri + "/preflights"
        Body = $SourceAttributesRequestJson
        ContentType = "application/json"
        Headers = $Headers
    }

    Write-Output "Starting source preflight..." | Out-Host
    return Invoke-RMRestMethod -Params $Params
}

function Get-RMSourceWithAttribute {
    param(
        [System.Object] $Source,
        [System.Object] $CloudAccount,
        [bool] $IgnoreValidationErrors,
        [RMMigrationReturn] $RMMigrationReturn
    )
    if ($Source.attribute_state -eq "running") {
        throw "Source attribute collection state is 'running', please wait for the attribute collection to complete."
    }

    Update-RMSourceWithAppliance -SourceId $Source.id -ApplianceId $CloudAccount.appliance.id
    $Response = Start-RMSourcePreflight -Source $Source
    Write-Output "Waiting for source preflight to complete..." | Out-Host
    $PreflightResult = Watch-RMPreflightStatus -PreflightId $Response.preflights[0].id `
        -TimeOutMessage "Timed out waiting for collection to complete"
    $Source = Get-RMSourceById -SourceId $Source.id
    if (![string]::IsNullOrEmpty($Source.attribute_error)) {
        $PreflightID = $Response.preflights[0].id
        $AttributeCollectionError = $Source.attribute_error
        throw "Source attribute collection with ID: $PreflightID has failed with error $AttributeCollectionError"
    }

    $ShouldExit, $PreflightWarning, $PreflightError = Out-RMPreflight -PreflightResult $PreflightResult -IgnoreValidationErrors $IgnoreValidationErrors -RMMigrationReturn $RMMigrationReturn
    $OverrideExistingMigrationWarning = $false
    $OverrideExistingMigrationError = $false
    if ($PreflightWarning.keys -contains "Conflicting Migration Attempts") {
        $OverrideExistingMigrationWarning = $true
    }
    
    if ($PreflightError.keys  -contains "Conflicting Migration Attempts") {
        $OverrideExistingMigrationError = $true
        if (1 -eq $PreflightError.Count) {
            $ShouldExit = $false
        }
    }
    return $Source, $ShouldExit, $OverrideExistingMigrationWarning, $OverrideExistingMigrationError
}

function Update-RMSourceWithAppliance {
    param (
        [System.Object] $SourceId,
        [string] $ApplianceId
    )
    
    $RequestAttributes = @{
        "data_only_migration" = $false
        "appliance_id" = $ApplianceId
    }

    $RequestAttributesJson = ConvertTo-Json $RequestAttributes

    $RMLoginResult = Get-Variable -Name "RMContext-UserLogin" -ValueOnly
    $Uri = Get-Variable -Name "RMContext-ReactorURI" -ValueOnly

    $Headers = @{
        Accept = "application/rm+json"
        "X-Auth-Token" = $RMLoginResult.token
    }

    $Params = @{
        Method = "Put"
        Uri = $Uri + "/sources/" + $SourceId
        Body = $RequestAttributesJson
        ContentType = "application/json"
        Headers = $Headers
    }

    Invoke-RMRestMethod -Params $Params | Out-Null
}

function Read-RMSource {
    param(
        [string] $UserMessage,
        [string] $ParameterName,
        [bool] $IsRequired
    )
    while ($true) {
        $SourceIP =  Read-RMIPAddress -UserMessage $UserMessage -ParameterName $ParameterName -IsRequired $IsRequired
        try {
            $Source = Get-RMSourceByIP -IPAddress $SourceIP
        } catch [System.Management.Automation.ItemNotFoundException] {
            $ProjectName = Get-Variable -Name "RMContext-CurrentProjectName" -ValueOnly
            $OrgName = Get-Variable -Name "RMContext-CurrentOrganizationName" -ValueOnly
            
            Write-RMError -Message "Source with IP address '$SourceIP' does not exist in project '$ProjectName' of organization '$OrgName'."
            continue
        }

        return $Source
    }
}

function Read-RMVMBasedSource {
    param(
        [string] $UserMessage,
        [string] $ParameterName,
        [bool] $IsRequired
    )
    while ($true) {
        $SourceVMName =  Read-RMString -UserMessage $UserMessage -ParameterName $ParameterName -IsRequired $IsRequired
        try {
            $Source = Get-RMSourceByIP -IPAddress $SourceVMName
            if ($Source.collection_type -ine "VM") {
                Write-RMError -Message "Source '$SourceVMName' is not a VM-Based source, to migrate this source, please use the cmdlet 'Start-RMVSphereOSBasedMigration'."
                continue
            }
        } catch [System.Management.Automation.ItemNotFoundException] {
            $ProjectName = Get-Variable -Name "RMContext-CurrentProjectName" -ValueOnly
            $OrgName = Get-Variable -Name "RMContext-CurrentOrganizationName" -ValueOnly
            Write-RMError -Message "Source with VM name '$SourceVMName' does not exist in project '$ProjectName' of organization '$OrgName'."
            continue
        }

        return $Source
    }
}

function Get-RMVMBasedSelectedDisk {
    param(
        [System.Object] $Source,
        [string[]] $ExcludedDiskLabel
    )
    $MountPoints = @()
    for ($Index = 0; $Index -lt $Source.attributes.storage.vm_disks.Count; $Index++) {
        if ($ExcludedDiskLabel -contains $Source.attributes.storage.vm_disks[$Index].label) {
            continue
        }
        $MountPoints += @{mount_point = $Index.ToString()}
    }

    return $MountPoints
}

function Get-RMSelectedDiskByDiskLabel {
    param(
        [string[]] $DiskLabel,
        [System.Object] $Source
    )
    $MountPoints = @()
    for ($Index = 0; $Index -lt $Source.attributes.storage.vm_disks.Count; $Index++) {
        if ($DiskLabel -contains $Source.attributes.storage.vm_disks[$Index].label) {
            $MountPoints += @{mount_point = $Index.ToString()}
        }
    }

    return $MountPoints
}

function Get-RMVMDiskProperty {
    param (
        [System.Object] $Source
    )
    $Result = @()
    foreach ($VMDisk in $Source.attributes.storage.vm_disks) {
        $Size = [math]::round($VMDisk.size_kb/(1024 * 1024), 2)
        $Result += $VMDisk.label + " (Size: $Size GiB)"
    }

    return $Result
}

function Get-RMSelectedDisk {
    param(
        [System.Object] $Source
    )
    $DiskLabels = @()
    $Source.attributes.storage.vm_disks | ForEach-Object -Process { $DiskLabels += $_.label }
    while ($true) {
        $ReadTokens = Read-RMToken -UserMessage "Enter labels of the disks to be excluded, separated by commas" `
            -DefaultValue "None" -ParameterName "Disk label(s)" -Separator "," -IsRequired $false
        # Read-RMToken returns empty string when default None is used by the user, not ideal, should return empty array
        if ([string]::IsNullOrWhiteSpace($ReadTokens)) {
            return Get-RMVMBasedSelectedDisk -Source $Source -ExcludedDiskLabel $DisksToExclude
        }
        $DisksToExclude = @()
        foreach ($Token in $ReadTokens) {
            $Token = $Token.Trim('"')
            $Token = $Token.Trim("'")
            $DisksToExclude += $Token
        }
        $Result = Test-RMNonExistentMountPoints -SourceMountPoints $DiskLabels -UserInputMountPoints $DisksToExclude
        if ($Result.Count -gt 0) {
            $ResultAsString = $Result -join ", "
            Write-RMError -Message "Disk labels '$ResultAsString' does not exist on source and hence cannot be excluded, please try again."
            continue
        }

        if ($DisksToExclude.Count -eq $Source.attributes.storage.vm_disks.Count) {
            Write-RMError -Message "Cannot exclude all the disks, please try again."
            continue
        }

        return Get-RMVMBasedSelectedDisk -Source $Source -ExcludedDiskLabel $DisksToExclude
    }

}

function Test-RMSourceHasDynamicDisks {
    param(
        [System.Object] $Source
    )

    if ($Source.os_type -ine "windows") {
        return $false
    }

    foreach ($Disk in $Source.attributes.storage.disks.psobject.Properties.Value) {
        if ($null -ne $Disk.flags -and $Disk.flags -contains "dynamic_disk") {
            return $true
        }
    }
    return $false
}

function Confirm-RMSource {
    param (
       [string] $SourceId
    )

    $ErrorString = ""
    $Source = Get-RMSourceById -SourceId $SourceId
    if ($Source.is_deleted) {
        $ProjectName = Get-Variable -Name "RMContext-CurrentProjectName" -ValueOnly
        $OrgName = Get-Variable -Name "RMContext-CurrentOrganizationName" -ValueOnly
        $IPAddress = $Source.host
        $ErrorString = "Source with IP address '$IPAddress' does not exist in project '$ProjectName' of organization '$OrgName', cannot start the differential migration."
    }
    return $Source, $ErrorString
    
}

# No Export-ModuleMember is being used which will automatically export all the functions
# of this module and we want all the functions to be exported.