Public/ConvertTo-LMUptimeDevice.ps1

<#
.SYNOPSIS
Migrates LogicMonitor website checks to LM Uptime devices.

.DESCRIPTION
ConvertTo-LMUptimeDevice consumes objects returned by Get-LMWebsite, translates their
configuration into the v3 Uptime payload shape, and provisions new Uptime devices by invoking
New-LMUptimeDevice. The cmdlet preserves alerting behaviour, polling thresholds, locations,
and scripted web steps whenever possible.

.PARAMETER Website
Website object returned by Get-LMWebsite. Accepts pipeline input.

.PARAMETER NamePrefix
Optional string prefixed to the generated Uptime device name.

.PARAMETER NameSuffix
Optional string appended to the generated Uptime device name.

.PARAMETER TargetHostGroupIds
Explicit host group identifiers for the new device.

.PARAMETER DisableSourceAlerting
When specified, disables alerting on the source website after the Uptime device is created successfully.

.EXAMPLE
Get-LMWebsite -Name "logicmonitor.com" | ConvertTo-LMUptimeDevice -NameSuffix "-uptime"

Migrates the logicmonitor.com website check to an Uptime device with a "-uptime" suffix.

.NOTES
You must run Connect-LMAccount prior to execution. The cmdlet honours -WhatIf/-Confirm
through ShouldProcess.

.INPUTS
PSObject. Website objects returned by Get-LMWebsite can be piped to this cmdlet.

.OUTPUTS
LogicMonitor.LMUptimeDevice

.LINK
Get-LMWebsite

.LINK
New-LMUptimeDevice

.LINK
Get-LMUptimeDevice
#>

function ConvertTo-LMUptimeDevice {

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [PSObject]$Website,

        [String]$NamePrefix = '',

        [String]$NameSuffix = '',

        [Parameter(Mandatory)]
        [String[]]$TargetHostGroupIds,

        [Switch]$DisableSourceAlerting
    )

    begin {
        function Convert-LMWebsiteProperties {
            param ([Object]$Properties)

            $converted = @{}

            foreach ($entry in @($Properties)) {
                if (-not $entry) { continue }
                $name = $null
                $value = $null

                if ($entry -is [PSCustomObject]) {
                    $name = $entry.name
                    $value = $entry.value
                }
                elseif ($entry -is [Hashtable]) {
                    $name = $entry['name']
                    $value = $entry['value']
                }

                if ([string]::IsNullOrWhiteSpace($name)) { continue }
                $converted[[string]$name] = $value
            }

            return $converted
        }

        function Convert-PSObjectToHashtable {
            param ([Object]$InputObject)

            if ($null -eq $InputObject) { return $null }

            if ($InputObject -is [Hashtable]) {
                $result = @{}
                foreach ($key in $InputObject.Keys) {
                    $result[$key] = Convert-PSObjectToHashtable $InputObject[$key]
                }
                return $result
            }

            if ($InputObject -is [PSCustomObject]) {
                $hash = @{}
                foreach ($property in $InputObject.PSObject.Properties) {
                    $hash[$property.Name] = Convert-PSObjectToHashtable $property.Value
                }
                return $hash
            }

            if ($InputObject -is [System.Collections.IEnumerable] -and -not ($InputObject -is [String])) {
                $list = @()
                foreach ($item in $InputObject) {
                    $list += Convert-PSObjectToHashtable $item
                }
                return $list
            }

            return $InputObject
        }

        function Get-GlobalSmAlertCondString {
            param ($Value)

            switch ($Value) {
                0 { return 'all' }
                1 { return 'half' }
                2 { return 'moreThanOne' }
                3 { return 'any' }
                'all' { return 'all' }
                'half' { return 'half' }
                'morethanone' { return 'moreThanOne' }
                'any' { return 'any' }
                default { return 'all' }
            }
        }
    }

    process {
        if (-not $Script:LMAuth.Valid) {
            Write-Error 'Please ensure you are logged in before running any commands, use Connect-LMAccount to login and try again.'
            return
        }
        
        if (-not $Website) { return }

        $type = if ($null -ne $Website.type) { $Website.type } else { '' }
        if ($type -notin @('webcheck', 'pingcheck')) {
            Write-Warning "Skipping resource '$($Website.name)' because type '$type' is not supported."
            return
        }

        $isInternal = [bool]$Website.isInternal
        $targetName = "${NamePrefix}$($Website.name)${NameSuffix}"

        $parameters = @{
            Name         = $targetName
            HostGroupIds = $TargetHostGroupIds
            Description  = $Website.description
        }

        if ($Website.pollingInterval) { 
            $pollingValue = [int]$Website.pollingInterval
            if ($pollingValue -gt 10) {
                Write-Warning "Website '$($Website.name)' has PollingInterval of $pollingValue minutes. Uptime devices only support 1-10 minutes. Setting to 10 minutes."
                $pollingValue = 10
            }
            $parameters.PollingInterval = $pollingValue
        }
        if ($Website.transition) { $parameters.AlertTriggerInterval = [int]$Website.transition }

        $parameters.GlobalSmAlertCond = Get-GlobalSmAlertCondString -Value $Website.globalSmAlertCond

        if ($Website.overallAlertLevel) { $parameters.OverallAlertLevel = $Website.overallAlertLevel }
        if ($Website.individualAlertLevel) { $parameters.IndividualAlertLevel = $Website.individualAlertLevel }
        if ($Website.PSObject.Properties.Match('individualSmAlertEnable')) { $parameters.IndividualSmAlertEnable = [bool]$Website.individualSmAlertEnable }
        $useDefaultLocation = $false
        if ($Website.PSObject.Properties.Match('useDefaultLocationSetting')) {
            $useDefaultLocation = [bool]$Website.useDefaultLocationSetting
            $parameters.UseDefaultLocationSetting = $useDefaultLocation
        }

        $useDefaultAlerting = $false
        if ($Website.PSObject.Properties.Match('useDefaultAlertSetting')) {
            $useDefaultAlerting = [bool]$Website.useDefaultAlertSetting
            $parameters.UseDefaultAlertSetting = $useDefaultAlerting
        }

        $propertyTable = Convert-LMWebsiteProperties -Properties $Website.properties
        if ($propertyTable.Count -gt 0) {
            # Pass the hashtable directly - New-LMUptimeDevice handles conversion
            $parameters.Properties = $propertyTable
        }

        if ($Website.template) { $parameters.Template = $Website.template }

        $testLocation = $Website.testLocation
        $collectorIds = @($testLocation.collectorIds | Where-Object { $_ })
        $smgIds = @($testLocation.smgIds | Where-Object { $_ })
        $allFlag = [bool]$testLocation.all

        # If marked as internal but no collectors specified, treat as external
        if ($isInternal -and ($collectorIds.Count -eq 0)) {
            $isInternal = $false
        }

        if ($isInternal) {
            # Internal checks require collector IDs
            if ($collectorIds.Count -gt 0) {
                $parameters.TestLocationCollectorIds = $collectorIds
            }
        }
        else {
            # External checks - need to specify location and preserve the 'all' flag
            if ($smgIds.Count -gt 0) {
                $parameters.TestLocationSmgIds = $smgIds
                # Pass the all flag to preserve the source configuration
                # If all:false, the API will use only the specified SMG IDs
                # If all:true, the API will use all locations
                if (-not $allFlag) {
                    $parameters.TestLocationAll = $false
                }
            }
            elseif ($allFlag) {
                # Use default SMG IDs (all public locations) when 'all' flag is set
                # These are the standard LogicMonitor public checkpoint locations
                $parameters.TestLocationSmgIds = @(2, 3, 4, 5, 6)
            }
            else {
                # Fallback to all locations if no specific config
                $parameters.TestLocationSmgIds = @(2, 3, 4, 5, 6)
            }
        }

        if ($type -eq 'webcheck') {
            if ([string]::IsNullOrWhiteSpace($Website.domain)) {
                Write-Warning "Website '$($Website.name)' does not contain a domain. Skipping conversion."
                return
            }

            $parameters.Domain = $Website.domain
            if ($Website.schema) { $parameters.Schema = $Website.schema }
            if ($Website.PSObject.Properties.Match('ignoreSSL')) { $parameters.IgnoreSSL = [bool]$Website.ignoreSSL }
            if ($Website.pageLoadAlertTimeInMS) { $parameters.PageLoadAlertTimeInMS = [int]$Website.pageLoadAlertTimeInMS }
            if ($Website.alertExpr) { $parameters.AlertExpr = $Website.alertExpr }
            if ($Website.PSObject.Properties.Match('triggerSSLStatusAlert')) { $parameters.TriggerSSLStatusAlert = [bool]$Website.triggerSSLStatusAlert }
            if ($Website.PSObject.Properties.Match('triggerSSLExpirationAlert')) { $parameters.TriggerSSLExpirationAlert = [bool]$Website.triggerSSLExpirationAlert }

            $stepObjects = @()
            $authWarningShown = $false
            foreach ($step in @($Website.steps)) {
                $convertedStep = Convert-PSObjectToHashtable $step
                if ($convertedStep) { 
                    $stepObjects += $convertedStep
                    # Check if this step has authentication configured
                    if (-not $authWarningShown -and $convertedStep.requireAuth -eq $true) {
                        Write-Warning "Website '$($Website.name)' has authentication configured in step '$($step.name)'. The password cannot be retrieved via API and must be manually updated after migration."
                        $authWarningShown = $true
                    }
                }
            }
            if ($stepObjects.Count -gt 0) { $parameters.Steps = $stepObjects }

        }
        else {
            $hostname = $Website.host
            if ([string]::IsNullOrWhiteSpace($hostname)) {
                Write-Warning "Website '$($Website.name)' does not contain a host value. Skipping conversion."
                return
            }

            $parameters.Hostname = $hostname
            if ($Website.count) { $parameters.Count = [int]$Website.count }
            if ($Website.percentPktsNotReceiveInTime) { $parameters.PercentPktsNotReceiveInTime = [int]$Website.percentPktsNotReceiveInTime }
            if ($Website.timeoutInMSPktsNotReceive) { $parameters.TimeoutInMSPktsNotReceive = [int]$Website.timeoutInMSPktsNotReceive }

        }

        if ($PSCmdlet.ShouldProcess($targetName, 'Create LM Uptime Device')) {
            $commonParams = @{}
            foreach ($commonParam in 'Debug', 'Verbose', 'WhatIf', 'Confirm') {
                if ($PSBoundParameters.ContainsKey($commonParam)) {
                    $commonParams[$commonParam] = $PSBoundParameters[$commonParam]
                }
            }

            try {
                Write-Verbose "Creating Uptime device '$targetName' with parameters: $($parameters | ConvertTo-Json -Compress -Depth 10)"
                $result = New-LMUptimeDevice @parameters @commonParams
                if ($result -and $DisableSourceAlerting.IsPresent) {
                    try {
                        $setWebsiteParams = @{ Id = $Website.id; DisableAlerting = $true }
                        Set-LMWebsite @setWebsiteParams @commonParams | Out-Null
                    }
                    catch {
                        Write-Warning "Uptime device created but failed to disable alerting on website '$($Website.name)': $_"
                    }
                }
                if ($result) { Write-Output $result }
            }
            catch {
                Write-Error "Failed to create Uptime device for website '$($Website.name)': $_"
            }
        }
    }
}